diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml new file mode 100644 index 0000000..daa685f --- /dev/null +++ b/.github/workflows/azure-dev.yml @@ -0,0 +1,63 @@ +name: Azure Dev Deploy + +on: + workflow_dispatch: + push: + branches: + - main + - feature/auth-upgrade + paths-ignore: + - '**.md' + - '.gitignore' + - '.github/**' + - '!.github/workflows/**' + +permissions: + id-token: write + contents: read + +jobs: + deploy: + runs-on: ubuntu-latest + env: + AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }} + AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }} + AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} + AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} + AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install azd + uses: Azure/setup-azd@v1.0.0 + + - name: Log in with Azure (Federated Credentials) + if: ${{ env.AZURE_CLIENT_ID != '' }} + run: | + azd auth login \ + --client-id "$Env:AZURE_CLIENT_ID" \ + --federated-credential-provider "github" \ + --tenant-id "$Env:AZURE_TENANT_ID" + shell: pwsh + + - name: Log in with Azure (Client Credentials) + if: ${{ env.AZURE_CLIENT_ID == '' }} + run: | + azd auth login --client-id "${{ secrets.AZURE_CLIENT_ID }}" --client-secret "${{ secrets.AZURE_CLIENT_SECRET }}" --tenant-id "${{ secrets.AZURE_TENANT_ID }}" + + - name: Azure Dev Provision + run: azd provision --no-prompt + env: + AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} + AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} + AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} + AZURE_APP_SERVICE_SKU: ${{ vars.AZURE_APP_SERVICE_SKU || 'F1' }} + AZURE_ENABLE_AUTH: ${{ vars.AZURE_ENABLE_AUTH || 'false' }} + + - name: Azure Dev Deploy + run: azd deploy --no-prompt + env: + AZURE_SUBSCRIPTION_ID: ${{ vars.AZURE_SUBSCRIPTION_ID }} + AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4fea8c0..64f824f 100644 --- a/.gitignore +++ b/.gitignore @@ -352,3 +352,11 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ + +# Azure Developer CLI environment files +.azdo/.env +.env +*.env.local + +# Azure CLI cache +.azure/ diff --git a/README-azd.md b/README-azd.md new file mode 100644 index 0000000..f32aa26 --- /dev/null +++ b/README-azd.md @@ -0,0 +1,183 @@ +# Azure Developer CLI Deployment + +This EventGrid Viewer Blazor application can be deployed using the Azure Developer CLI (azd) for a streamlined development and deployment experience. + +## Prerequisites + +- [Azure Developer CLI](https://aka.ms/azure-dev/install) +- [.NET 9.0 SDK](https://dotnet.microsoft.com/download) +- [Azure CLI](https://docs.microsoft.com/cli/azure/install-azure-cli) (optional, for additional Azure operations) + +## Quick Start + +1. **Clone and navigate to the repository:** + ```bash + git clone https://github.com/Azure-Samples/eventgrid-viewer-blazor.git + cd eventgrid-viewer-blazor + ``` + +2. **Initialize the Azure Developer CLI environment:** + ```bash + azd auth login + azd init + ``` + +3. **Deploy the application:** + ```bash + azd up + ``` + + This single command will: + - Provision Azure resources (App Service, Application Insights, Log Analytics) + - Build and deploy the .NET application + - Configure all necessary settings + +4. **Get the webhook URL:** + ```bash + azd env get-values + ``` + Look for `EVENTGRID_WEBHOOK_URL` in the output. + +## Configuration + +### Environment Variables + +You can customize the deployment by setting environment variables: + +```bash +# Set the Azure region +azd env set AZURE_LOCATION eastus2 + +# Set the App Service SKU +azd env set AZURE_APP_SERVICE_SKU B1 + +# Enable authentication (requires additional setup) +azd env set AZURE_ENABLE_AUTH true +azd env set AZURE_KEYVAULT_NAME your-keyvault-name +azd env set AZURE_AD_DOMAIN your-domain.onmicrosoft.com +``` + +### Available Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `AZURE_LOCATION` | `eastus2` | Azure region for deployment | +| `AZURE_APP_SERVICE_SKU` | `F1` | App Service plan SKU | +| `AZURE_ENABLE_AUTH` | `false` | Enable Azure AD authentication | +| `AZURE_KEYVAULT_NAME` | | Key Vault name for auth secrets | +| `AZURE_AD_DOMAIN` | | Azure AD domain for authentication | +| `AZURE_REPO_URL` | Current repo | GitHub repository URL | +| `AZURE_REPO_BRANCH` | `main` | GitHub branch to deploy | + +## Authentication Setup + +To enable Azure AD authentication: + +1. Create an Azure AD app registration +2. Create a Key Vault and store the client ID: + ```bash + az keyvault secret set --vault-name --name "sct-egvb-azad-client-id" --value "" + ``` +3. Set the authentication environment variables: + ```bash + azd env set AZURE_ENABLE_AUTH true + azd env set AZURE_KEYVAULT_NAME your-keyvault-name + azd env set AZURE_AD_DOMAIN your-domain.onmicrosoft.com + ``` +4. Redeploy: + ```bash + azd deploy + ``` + +## Development Workflow + +### Local Development + +```bash +# Run the application locally +dotnet run --project src/Blazor.EventGridViewer.ServerApp + +# Or use the VS Code task +# Press Ctrl+Shift+P, then "Tasks: Run Task" > "run" +``` + +### Continuous Deployment + +The project includes a GitHub Actions workflow (`.github/workflows/azure-dev.yml`) that automatically deploys on pushes to main. To set this up: + +1. Fork the repository +2. Set up GitHub variables for your Azure environment: + - `AZURE_CLIENT_ID` + - `AZURE_TENANT_ID` + - `AZURE_SUBSCRIPTION_ID` + - `AZURE_ENV_NAME` + - `AZURE_LOCATION` + +## Common Commands + +```bash +# Deploy infrastructure and application +azd up + +# Deploy application only (faster for code changes) +azd deploy + +# View environment variables and resource URLs +azd env get-values + +# View deployment logs +azd monitor + +# Clean up all resources +azd down +``` + +## Resource Naming + +Resources are automatically named using the pattern: +- App Service: `app-{env-name}-{random-suffix}` +- App Service Plan: `plan-{env-name}-{random-suffix}` +- Application Insights: `ai-{app-name}-{env-name}` +- Log Analytics: `law-{app-name}-{env-name}` + +## Infrastructure + +The infrastructure is defined in Bicep templates under the `infra/` directory: + +- `main.bicep` - Main template orchestrating all resources +- `modules/monitoring.bicep` - Application Insights and Log Analytics +- `modules/appservice.bicep` - App Service and App Service Plan + +## Troubleshooting + +### Common Issues + +1. **Deployment fails with SKU not available**: Try a different Azure region or SKU: + ```bash + azd env set AZURE_LOCATION westus2 + azd env set AZURE_APP_SERVICE_SKU B1 + ``` + +2. **Resource names already exist**: Change the environment name: + ```bash + azd env set AZURE_ENV_NAME myapp-dev-v2 + ``` + +3. **Authentication not working**: Ensure the Key Vault exists and contains the client ID secret + +### Getting Help + +- View detailed logs: `azd deploy --debug` +- Check Azure resources: Visit the [Azure Portal](https://portal.azure.com) +- Azure Developer CLI docs: [aka.ms/azure-dev](https://aka.ms/azure-dev) + +## Next Steps + +After deployment, you can: + +1. Configure Event Grid subscriptions to point to: `{your-app-url}/api/eventgrid` +2. View real-time events in the web application +3. Monitor application performance in Application Insights +4. Scale the App Service plan as needed + +For more advanced scenarios, see the original [README.md](README.md). \ No newline at end of file diff --git a/README.md b/README.md index 4cc4783..3732393 100644 --- a/README.md +++ b/README.md @@ -5,52 +5,169 @@ languages: products: - aspnet - azure-event-grid -description: The EventGrid Viewer Blazor application can be used to view Azure EventGrid messages in realtime using ASP.Net Core Blazor and SignalR. +description: A modern Blazor application for viewing Azure EventGrid messages in real-time using ASP.NET Core Blazor and SignalR. urlFragment: eventgrid-viewer-blazor --- -# EventGrid Viewer Blazor application +# 🌐 EventGrid Viewer Blazor -The EventGrid Viewer Blazor application can be used to view Azure EventGrid messages in realtime using ASP.Net Core Blazor and SignalR. For those who would like to secure the application, the EventGrid Viewer Blazor application can be easily configured via appsettings to use Entra ID authentication, Azure KeyVault & Managed Identities. +[![Build](https://github.com/Azure-Samples/eventgrid-viewer-blazor/workflows/Build/badge.svg)](https://github.com/Azure-Samples/eventgrid-viewer-blazor/actions) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![.NET](https://img.shields.io/badge/.NET-9.0-purple.svg)](https://dotnet.microsoft.com/download) +[![Azure](https://img.shields.io/badge/Azure-EventGrid-blue.svg)](https://azure.microsoft.com/services/event-grid/) -![Build](https://github.com/Azure-Samples/eventgrid-viewer-blazor/workflows/Build/badge.svg) +> A modern, real-time Azure EventGrid message viewer built with ASP.NET Core Blazor and SignalR ![overview diagram](./docs/images/overview.drawio.svg) -Building upon some of the ideas of the [azure-event-grid-viewer](https://github.com/Azure-Samples/azure-event-grid-viewer), the EventGrid Viewer Blazor application was written in [Blazor](https://dotnet.microsoft.com/apps/aspnet/web-apps/blazor) and offers the following features & enhancements: +## πŸ“‹ Table of Contents -1. View all Azure EventGrid messages in json format -1. View formatted & highlighted json -1. Copy json messages to the clipboard -1. Enable Entra ID authentication to secure the application +- [✨ Features](#-features) +- [πŸš€ Quick Start](#-quick-start) +- [πŸ”§ Prerequisites](#-prerequisites) +- [πŸ“¦ Deployment](#-deployment) +- [πŸ“Έ Screenshots](#-screenshots) +- [🎯 Usage](#-usage) +- [πŸ”’ Security](#-security) +- [🀝 Contributing](#-contributing) -## Screenshot +## ✨ Features + +Building upon the ideas of [azure-event-grid-viewer](https://github.com/Azure-Samples/azure-event-grid-viewer), this Blazor application offers: + +- βœ… **Real-time Event Viewing** - View Azure EventGrid messages as they arrive +- βœ… **JSON Formatting** - Beautifully formatted and syntax-highlighted JSON +- βœ… **Copy to Clipboard** - One-click copying of event data +- βœ… **Modern UI** - Clean, responsive Blazor interface +- βœ… **SignalR Integration** - Real-time updates without page refresh +- πŸ”„ **Entra ID Authentication** - *Coming Soon* + +## πŸš€ Quick Start + +Get up and running in under 2 minutes: + +```bash +# 1️⃣ Clone the repository +git clone https://github.com/Azure-Samples/eventgrid-viewer-blazor.git +cd eventgrid-viewer-blazor + +# 2️⃣ Deploy to Azure (one command!) +azd auth login +azd up +``` + +> πŸŽ‰ **That's it!** Your EventGrid viewer will be deployed and ready to use. + +## πŸ”§ Prerequisites + +Before you begin, ensure you have: + +- **Azure Subscription** - [Get a free account](https://azure.microsoft.com/free/) +- **Azure Developer CLI** - [Install azd](https://aka.ms/azure-dev/install) +- **.NET 9.0 SDK** - [Download here](https://dotnet.microsoft.com/download) *(for local development)* +- **Modern Browser** - Chrome, Firefox, Safari, or Edge + +## πŸ“¦ Deployment + +### 🌟 Option 1: Azure Developer CLI (Recommended) + +The fastest way to deploy with full Azure resource provisioning: + +```bash +# Install azd if you haven't already +winget install microsoft.azd # Windows +brew tap azure/azd && brew install azd # macOS +curl -fsSL https://aka.ms/install-azd.sh | bash # Linux + +# Clone and deploy +git clone https://github.com/Azure-Samples/eventgrid-viewer-blazor.git +cd eventgrid-viewer-blazor +azd auth login +azd up +``` + +πŸ“– **For detailed azd instructions, environment configuration, and troubleshooting, see [README-azd.md](README-azd.md).** + +### πŸ”— Option 2: Deploy to Azure Button + +Quick deployment using ARM template: + +[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure-Samples%2Feventgrid-viewer-blazor%2Frefs%2Fheads%2Fmain%2Finfra%2Farm%2Fazuredeploy.json) + +**Steps:** +1. βœ… Create or select a Resource Group +2. βœ… Enter a unique Site Name +3. βœ… Enter a Hosting Plan Name +4. βœ… Click **Review + Create** to deploy + +## πŸ“Έ Screenshots ![eventgrid-viewer-blazor Screenshot](docs/images/eventgrid-viewer-blazor-screenshot.png) -## Usage +## 🎯 Usage + +### Setting up EventGrid Subscription + +Once deployed, use your webhook endpoint to subscribe to EventGrid events: + +``` +https://{{your-site-name}}.azurewebsites.net/api/eventgrid +``` + +πŸ“š **Learn more:** [Subscribe to EventGrid events](https://docs.microsoft.com/en-us/azure/event-grid/subscribe-through-portal) + +### Local Development + +
+πŸ”§ Click to expand local development setup -To quickly deploy the EventGrid Viewer Blazor application to Azure, hit the button below *(for examples on how to deploy with Entra ID authentication, see the [examples section](#examples))*: +```bash +# Clone the repository +git clone https://github.com/Azure-Samples/eventgrid-viewer-blazor.git +cd eventgrid-viewer-blazor -[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure-Samples%2Feventgrid-viewer-blazor%2Fmain%2Finfrastructure%2Farm%2Fazuredeploy.json) +# Restore packages +dotnet restore src/Blazor.EventGridViewer.sln -You will be presented with a screen similar to the image below: +# Run the application +dotnet run --project src/Blazor.EventGridViewer.ServerApp -![Deploy to Azure](docs/images/deploy-to-azure-no-auth.png) +# Open browser to +# https://localhost:5001 (or the URL shown in terminal) +``` -Do the following: +
-1. Create or Select a Resource Group -1. Enter a Site Name -1. Enter a Hosting Plan Name -1. Hit the *Review + Create* button to validate & deploy the EventGrid Viewer Blazor application +## πŸ”’ Security -Use the Webhook endpoint: +> **⚠️ Important:** This application currently runs without authentication. For production use with sensitive data, consider implementing authentication controls. + +**Planned Security Features:** +- πŸ”„ Entra ID authentication integration +- πŸ”„ Azure Key Vault for secrets management +- πŸ”„ Managed Identity support + +## 🀝 Contributing + +We welcome contributions! Here's how you can help: + +- πŸ› **Report bugs** - [Open an issue](https://github.com/Azure-Samples/eventgrid-viewer-blazor/issues) +- πŸ’‘ **Suggest features** - [Start a discussion](https://github.com/Azure-Samples/eventgrid-viewer-blazor/discussions) +- πŸ”§ **Submit PRs** - Fork, create feature branch, submit pull request + +### Development Stack + +- **Frontend:** Blazor Server (.NET 9.0) +- **Real-time:** SignalR +- **Hosting:** Azure App Service +- **Monitoring:** Application Insights + +--- - ```https://{{site-name}}.azurewebsites.net/api/eventgrid``` +
- to [subscribe to EventGrid events](https://docs.microsoft.com/en-us/azure/event-grid/subscribe-through-portal). +**⭐ If this project helped you, please give it a star!** -## Examples +Made with ❀️ by the Azure Samples team -In the *examples* folder, examples have been created to demonstrate how to automate the deployment of the EventGrid Viewer Blazor application with or without authentication. You can read more [here](examples). +
diff --git a/azure.yaml b/azure.yaml new file mode 100644 index 0000000..771f5b8 --- /dev/null +++ b/azure.yaml @@ -0,0 +1,31 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: eventgrid-viewer-blazor +metadata: + template: eventgrid-viewer-blazor@0.0.1-beta + +infra: + provider: bicep + path: infra + +services: + web: + project: ./src/Blazor.EventGridViewer.ServerApp + language: dotnet + host: appservice + +pipeline: + provider: github + +hooks: + prebuild: + shell: sh + run: | + echo "Building .NET application..." + + postdeploy: + shell: sh + run: | + echo "EventGrid Viewer Blazor deployed successfully!" + echo "Application URL: $WEB_APP_URL" + echo "Webhook endpoint: $WEB_APP_URL/api/eventgrid" \ No newline at end of file diff --git a/docs/images/deploy-to-azure-auth.png b/docs/images/deploy-to-azure-auth.png deleted file mode 100644 index 7c8d2f8..0000000 Binary files a/docs/images/deploy-to-azure-auth.png and /dev/null differ diff --git a/docs/images/deploy-to-azure-no-auth.png b/docs/images/deploy-to-azure-no-auth.png deleted file mode 100644 index 8024616..0000000 Binary files a/docs/images/deploy-to-azure-no-auth.png and /dev/null differ diff --git a/docs/images/examples-azure-ad-appsettings.png b/docs/images/examples-azure-ad-appsettings.png deleted file mode 100644 index 13e1548..0000000 Binary files a/docs/images/examples-azure-ad-appsettings.png and /dev/null differ diff --git a/docs/images/examples-configure-auth-script-output.png b/docs/images/examples-configure-auth-script-output.png deleted file mode 100644 index 7cee44a..0000000 Binary files a/docs/images/examples-configure-auth-script-output.png and /dev/null differ diff --git a/examples/.dockerignore b/examples/.dockerignore deleted file mode 100644 index 77c74ce..0000000 --- a/examples/.dockerignore +++ /dev/null @@ -1,2 +0,0 @@ -**/*.md -Dockerfile \ No newline at end of file diff --git a/examples/Dockerfile b/examples/Dockerfile deleted file mode 100644 index b4b84d7..0000000 --- a/examples/Dockerfile +++ /dev/null @@ -1,37 +0,0 @@ -FROM node:10 - -RUN apt-get update && \ - apt-get install -y curl \ - unzip \ - bsdmainutils - -# https://docs.microsoft.com/en-us/cli/azure/install-azure-cli-apt?view=azure-cli-latest#install -RUN curl -sL https://aka.ms/InstallAzureCLIDeb | bash - -# https://www.terraform.io/downloads.html -RUN curl https://releases.hashicorp.com/terraform/0.12.24/terraform_0.12.24_linux_amd64.zip --output terraform.zip - -# unzip terraform -RUN unzip terraform.zip - -# move to usr/local/bin directory -RUN mv terraform usr/local/bin - -# clean up -RUN rm terraform.zip - -WORKDIR /samples - -# terraform-no-auth -COPY terraform-no-auth/ terraform-no-auth/ - -# bash-auth -COPY shared bash-auth -RUN chmod +x bash-auth/configure-auth.sh - -# terraform-auth -COPY terraform-auth/ terraform-auth/ -COPY shared terraform-auth -RUN chmod +x terraform-auth/configure-auth.sh - -CMD ["/bin/bash"] \ No newline at end of file diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index af9c774..0000000 --- a/examples/README.md +++ /dev/null @@ -1,69 +0,0 @@ -# Overview - -The purpose of this document is to provide examples on how to automate the deployment of the EventGrid Viewer Blazor application - -## How to Deploy with no Authentication - -### Bash - -The ```azuredeploy.sh``` bash script was used to deploy the EventGrid Viewer Blazor application during development. The README on how this script works can be found [here](../infrastructure/README.md#azuredeploy.sh_Script). - -### Azure DevOps - -Azure DevOps pipelines were used during the development of the EventGrid Viewer Blazor application. The README on how to either build or deploy the EventGrid Viewer Blazor using Azure DevOps pipelines can be found [here](../infrastructure/azure-pipelines/README.md). - -### Terraform - -To view an example of how to deploy the EventGrid Viewer Blazor application using terraform, see the README found [here](terraform-no-auth/README.md). - -## How to Deploy with Entra ID Authentication - -The information inside of EventGrid messages can contain sensitive information. If a bad actor were to obtain the EventGrid Viewer Blazor url, they could view this information. The EventGrid Viewer Blazor application has been designed in a way that Entra ID authentication can be enabled through appsettings. - -EventGrid Viewer Blazor will use a [Azure Keyvault](https://azure.microsoft.com/en-us/services/key-vault/) Secret, [Entra ID App Registration](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-how-applications-are-added) & a [System-assigned managed Identity](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview) to secure the application. - -**EventGrid Viewer Blazor Entra ID Appsettings:** - -1. EntraId:Instance - hardcoded in the ARM Template, value is ```https://login.microsoftonline.com/``` -1. EntraId:Domain - the primary Azure Domain, ie {youraccount}.onmicrosoft.com -1. EntraId:TenantId - the Azure TenantId -1. EntraId:ClientId - the Entra ID App Registration ClientId. This value is set to a [Keyvault reference](https://docs.microsoft.com/en-us/azure/app-service/app-service-key-vault-references), for security & so the application can be re-deployed without having to set the ClientId for each deployment *(ARM Template deployments will reset appesettings, if there were changes after deployment)*. -1. EntraId:CallbackPath - hardcoded in the ARM Template, value is "/signin-oidc" -1. EntraId:SignedOutCallbackPath - hardcoded in the ARM Template, value is "/signout-callback-oidc" -1. EnableAuth - a flag that determines whether the application will enable/disable Entra ID authentication - -**Note:** The Keyvault secret name that will contain the EntraId:ClientId value is set by the ARM Template & the name is: *sct-egvb-azad-client-id* - -**How it works:** - -1. When deploying the application, using the ARM Template (ie Bash, Terraform etc etc): - 1. Set EnableAuth = true - 1. Set azAdDomain = {youraccount}.onmicrosoft.com - 1. Set keyvaultName = your_existing_keyvault_name - -When the application is deployed, the application will have the appsettings similar to the picture below: - -![Entra ID appsettings](../docs/images/examples-azure-ad-appsettings.png) - -1. Once the EventGrid Viewer Blazor application is deployed, a script needs to be run (manual or automated) to: - 1. Create a Entra ID App Registration - 1. Store the Entra ID App Registration ClientId in a Azure KeyVault secret named **sct-egvb-azad-client-id** - 1. Stop/start the the EventGrid Viewer Blazor application - -The [configure-auth.sh](shared/configure-auth.sh) used in the examples performs these tasks & can be used. To find out how to use this bash script, execute the following command for help: - -```bash - chmod +x configure-auth.sh && ./configure-auth.sh -h -``` - -**Note:** The [configure-auth-manifest.json](shared/configure-auth-manifest.json) file needs to be in the same directory as the ```configure-auth.sh``` script, when it is executed. - -Once you have deployed the application & run ```configure-auth.sh``` (or your own script), users should now be challenged with credentials when trying to access the EventGrid Viewer Blazor application. - -### Bash - -The README on how to configure the EventGrid Viewer Blazor application with Entra ID authentication using bash can be found [here](bash-auth/README.md). - -### Terraform - -To view an example of how to deploy the EventGrid Viewer Blazor application with Entra ID authentication using terraform, the guide can be found [here](terraform-auth/README.md). diff --git a/examples/bash-auth/README.md b/examples/bash-auth/README.md deleted file mode 100644 index e083d1c..0000000 --- a/examples/bash-auth/README.md +++ /dev/null @@ -1,68 +0,0 @@ -# Overview - -The purpose of this document is to demonstrate how to configure the EventGrid Viewer Blazor application with Entra ID authentication using bash, once the application has been deployed. You could use the bash script as part of an automated process. - -## Prerequisites - -1. Azure Account -1. Elevated permissions to create Azure Service Principals -1. An existing [Azure Keyvault](https://docs.microsoft.com/en-us/azure/key-vault/secrets/quick-create-portal) -1. Docker *(optional)* - -Instructions: - -1. Create an Azure Keyvault, if one does not exist. - -1. Use the *Deploy to Azure* button below to quickly deploy the application. Use the image below as a guide. - -[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FAzure-Samples%2Feventgrid-viewer-blazor%2Fmain%2Finfrastructure%2Farm%2Fazuredeploy.json) - -You will be presented with a screen similar to the image below: - -![Deploy to Azure](../../docs/images/deploy-to-azure-auth.png) - -Do the following: - -1. Create or Select a Resource Group -1. Enter a Site Name -1. Enter a Hosting Plan Name -1. Enable Auth -1. Enter a Keyvault Name -1. Enter a Entra ID Domain ie {youraccount}.onmicrosoft.com -1. Hit the *Review + Create* button to validate & deploy the EventGrid Viewer Blazor application - -After the application is deployed: - -1. You will need to set the Terraform environment variables when you run the docker container. You will need to provide: - - A azure subscription - - A resource group name - - A web app name - - A keyvault name -1. Run the following bash commands in order: - -```bash - # build image - docker build -t egvb-samples . - - # Replace the environment variables - docker run -it -e AZURE_SUBSCRIPTION_ID= -e resource_group_name= -e web_app_name= -e key_vault_name= egvb-samples - - # switch to bash-auth directory - cd bash-auth - - # for script usage example - # ./configure-auth.sh -h - - # execute script with parameters used with deploy to azure button to configure deployed app with az ad authentication - ./configure-auth.sh -s $AZURE_SUBSCRIPTION_ID -g $resource_group_name -a $web_app_name -k $key_vault_name -``` - -## Teardown - -Copy the App Registration AppId from the output of the ```configure-auth.sh``` script. - -![App Registration AppId](../../docs/images/examples-configure-auth-script-output.png) - -```bash - az group delete -n $resource_group_name && az ad app delete --id -``` diff --git a/examples/shared/configure-auth-manifest.json b/examples/shared/configure-auth-manifest.json deleted file mode 100644 index 7006f90..0000000 --- a/examples/shared/configure-auth-manifest.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "requiredResourceAccess": [ - { - "resourceAppId": "00000002-0000-0000-c000-000000000000", - "resourceAccess": [ - { - "id": "311a71cc-e848-46a1-bdf8-97ff7156d8e6", - "type": "Scope" - } - ] - } - ], - "oauth2AllowIdTokenImplicitFlow": true, - "oauth2AllowImplicitFlow": false, - "oauth2Permissions": [ - { - "isEnabled": true, - "lang": null, - "origin": "Application", - "type": "User", - "value": "user_impersonation" - } - ] -} \ No newline at end of file diff --git a/examples/shared/configure-auth.sh b/examples/shared/configure-auth.sh deleted file mode 100755 index af7decd..0000000 --- a/examples/shared/configure-auth.sh +++ /dev/null @@ -1,130 +0,0 @@ -#!/bin/bash - -################################################################################################ -#- Purpose: Script is used to create an Azure AD App Registration, set Azure -#- Keyvault secrets & restart the EventGrid Viewer Blazor Azure Web App Service. -#- Parameters are: -#- [-s] azure subscription - The Azure subscription to use (required)" -#- [-g] resource group name - The name of the Azure resource group ie rg-cse-egvb-dev (required)" -#- [-a] site name - The name of the Azure web app ie as-cse-egvb-dev (required)" -#- [-k] keyvault name - The name of the Azure Keyvault to store Azure AD secrets (required) ie kv-cse-egvb-dev" -#- [-h] help - Help (optional)" -################################################################################################ - -set -eu - -############################################################### -#- function used to print out script usage -############################################################### -function usage() { - echo - echo "Arguments:" - echo -e "\t-s \t The Azure subscription to use (required)" - echo -e "\t-g \t The name of the Azure resource group ie rg-cse-egvb-dev (required)" - echo -e "\t-a \t The name of the Azure web app ie as-cse-egvb-dev (required)" - echo -e "\t-k \t The name of the Azure Keyvault to store Azure AD secrets ie kv-cse-egvb-dev (required)" - echo -e "\t-h \t Help (optional)" - echo - echo "Example:" - echo -e "./configure-auth.sh -s 00000000-1111-2222-3333-444444444444 -g rg-cse-egvb-dev -a as-cse-egvb-dev -k kv-cse-egvb-dev" -} - -####################################################### -#- function used to print messages -####################################################### -function print() { - echo "$1" -} - -############################################################### -#- function used to create a random password -############################################################### -function generateRandomPassword() { - local password=$(openssl rand -base64 16 | colrm 17 | sed 's/$/!/') - - echo "#$password!2" -} - -parent_path=$( - cd "$(dirname "${BASH_SOURCE[0]}")" - pwd -P -) - -cd "$parent_path" - -# Loop, get parameters & remove any spaces from input -while getopts "s:g:a:k:h" opt; do - case $opt in - s) - # subscription - subscription=$OPTARG - ;; - g) - # resource group - resourceGroup=$OPTARG - ;; - a) - # site name - siteName=$OPTARG - ;; - k) - # keyvault name - keyvaultName=$OPTARG - ;; - :) - echo "Error: -${OPTARG} requires a value" - exit 1 - ;; - *) - usage - exit 1 - ;; - esac -done - -# validate parameters -if [[ $# -eq 0 || -z $subscription || -z $resourceGroup || -z $siteName || -z $keyvaultName ]]; then - error "Required parameters are missing" - usage - exit 1 -fi - -# login to azure -az login -o none -az account set -s $subscription - -# get configuration values -managedIdentityPrincipalId=$(az webapp identity assign -n $siteName -g $resourceGroup --query 'principalId' -o tsv) -signedInUserPrincipalName=$(az ad signed-in-user show --query 'userPrincipalName' -o tsv) -secret=$(generateRandomPassword) - -# create app registration -print "Creating app registration for $siteName" -appId=$(az ad app create --display-name $siteName \ - --identifier-uris https://$siteName.azurewebsites.net \ - --reply-urls https://$siteName.azurewebsites.net/signin-oidc \ - --password $secret \ - --required-resource-accesses @configure-auth-manifest.json \ - --query 'appId' -o tsv) - -# set access policies -print "Setting the keyvault access policies" -az keyvault set-policy --name $keyvaultName --upn $signedInUserPrincipalName --secret-permissions set get list delete -o none -az keyvault set-policy --name $keyvaultName --object-id $managedIdentityPrincipalId --secret-permissions set get list -o none - -# setting keyvault secrets -print "Setting keyvault secrets" -az keyvault secret set --vault-name $keyvaultName -n 'sct-egvb-azad-client-id' --value $appId -o none - -# restart the app -print "Stopping and starting the app" -az webapp stop -n $siteName -g $resourceGroup -o none -az webapp start -n $siteName -g $resourceGroup -o none - -# output -print "#######################---OUTPUT---##################################" -print "Signed-In User: $signedInUserPrincipalName" -print "Managed Identity: $managedIdentityPrincipalId" -print "App Registration Secret: $secret" -print "App Registration AppId: $appId" -print "#####################################################################" \ No newline at end of file diff --git a/examples/terraform-auth/README.md b/examples/terraform-auth/README.md deleted file mode 100644 index 24651fa..0000000 --- a/examples/terraform-auth/README.md +++ /dev/null @@ -1,72 +0,0 @@ -# Overview - -## Prerequisites - -1. Azure Account -1. Elevated permissions to create Azure Service Principals -1. Docker *(optional)* -1. Basic knowledge of [Terraform](https://www.terraform.io/) - -### Azure Resources - -- Resource Group -- App Service Plan -- Web App Service -- Keyvault -- eventgrid-viewer-blazor code -- App Registration - -Instructions: - -**Note:** If you have already have terraform & azure-cli installed, you just need to login to azure using azure-cli & set the default subscription. - -1. Open a terminal and switch to the *examples* directory -1. Run ```docker build -t egvb-samples .``` -1. You will need to set the Terraform environment variables when you run the docker container. You will need to provide: - - A azure subscription - - A resource group name - - A resource group region - - A app service plan name - - A web app name - - A keyvault name - - A entra id domain -1. Run the following bash commands in order: - -```bash - # Replace the environment variables - docker run -it -e AZURE_SUBSCRIPTION_ID= -e TF_VAR_resource_group_name= -e TF_VAR_resource_group_region= -e TF_VAR_web_app_name= -e TF_VAR_app_service_plan_name= -e TF_VAR_key_vault_name= -e TF_VAR_az_ad_domain= egvb-samples - - # login into azure - az login - - # set the default azure subscription - az account set -s $AZURE_SUBSCRIPTION_ID - - # switch to the terraform-auth directory - cd terraform-auth - - # init terraform - terraform init - - # terraform plan to view plan - terraform plan - - # terraform apply to deploy - terraform apply -``` - -1. Once the application has been deployed, configure Entra ID authentication by running the following: - -```bash - ./configure-auth.sh -s $AZURE_SUBSCRIPTION_ID -g $TF_VAR_resource_group_name -a $TF_VAR_web_app_name -k $TF_VAR_key_vault_name -``` - -## Teardown - -Copy the App Registration AppId from the output of the ```configure-auth.sh``` script. - -![App Registration AppId](../../docs/images/examples-configure-auth-script-output.png) - -```bash - terraform destroy && az ad app delete --id -``` diff --git a/examples/terraform-auth/main.tf b/examples/terraform-auth/main.tf deleted file mode 100644 index 5a49e70..0000000 --- a/examples/terraform-auth/main.tf +++ /dev/null @@ -1,58 +0,0 @@ -provider "azurerm" { - version = "=1.36.0" -} - -variable "resource_group_name" { - type = string -} - -variable "resource_group_region" { - type = string -} - -variable "web_app_name" { - type = string -} - -variable "app_service_plan_name" { - type = string -} - -variable "key_vault_name" { - type = string -} - -variable "az_ad_domain" { - type = string -} - -data "azurerm_client_config" "current" {} - -module "resourcegroup" { - source = "./modules/resource_group" - - name = var.resource_group_name - region = var.resource_group_region -} - -module "keyvault" { - source = "./modules/key_vault" - - name = var.key_vault_name - resource_group_name = module.resourcegroup.name - region = var.resource_group_region - tenant_id = data.azurerm_client_config.current.tenant_id - resource_group_id = module.resourcegroup.id -} - -module "egvb" { - source = "./modules/egvb" - create_resource = true - web_app_name = var.web_app_name - app_service_plan_name = var.app_service_plan_name - resource_group_name = module.resourcegroup.name - resource_group_id = module.resourcegroup.id - key_vault_id = module.keyvault.id - key_vault_name = var.key_vault_name - az_ad_domain = var.az_ad_domain -} \ No newline at end of file diff --git a/examples/terraform-auth/modules/egvb/egvb.tf b/examples/terraform-auth/modules/egvb/egvb.tf deleted file mode 100644 index bc741f3..0000000 --- a/examples/terraform-auth/modules/egvb/egvb.tf +++ /dev/null @@ -1,55 +0,0 @@ -variable "resource_group_name" { - type = string -} - -variable "app_service_plan_name" { - type = string -} - -variable "web_app_name" { - type = string -} - -variable "create_resource" { - type = bool -} - -variable "resource_group_id" {} - -variable "key_vault_id" {} - -variable "key_vault_name" { - type = string -} - -variable "az_ad_domain" { - type = string -} - -resource "random_string" "random" { - length = 12 - upper = false - special = false -} - -data "http" "deployjson" { - url = "https://raw.githubusercontent.com/Azure-Samples/eventgrid-viewer-blazor/main/infrastructure/arm/azuredeploy.json" -} - -resource "azurerm_template_deployment" "web_app_deploy" { - count = var.create_resource ? 1 : 0 - name = random_string.random.result - resource_group_name = var.resource_group_name - deployment_mode = "Incremental" - template_body = data.http.deployjson.body - - parameters = { - siteName = var.web_app_name - hostingPlanName = var.app_service_plan_name - enableAuth = "true" - keyvaultName = var.key_vault_name - azAdDomain = var.az_ad_domain - } - - depends_on = [var.resource_group_id, var.key_vault_id] -} diff --git a/examples/terraform-auth/modules/key_vault/keyvault.tf b/examples/terraform-auth/modules/key_vault/keyvault.tf deleted file mode 100644 index c135f18..0000000 --- a/examples/terraform-auth/modules/key_vault/keyvault.tf +++ /dev/null @@ -1,33 +0,0 @@ -variable "resource_group_name" { - type = string -} - -variable "name" { - type = string -} - -variable "region" { - type = string -} - -variable "tenant_id" { - type = string -} - -variable "resource_group_id" {} - -resource "azurerm_key_vault" "key_vault" { - name = var.name - location = var.region - resource_group_name = var.resource_group_name - enabled_for_disk_encryption = true - tenant_id = var.tenant_id - - sku_name = "standard" - - depends_on = [var.resource_group_id] -} - -output "id" { - value = azurerm_key_vault.key_vault.id -} \ No newline at end of file diff --git a/examples/terraform-auth/modules/resource_group/resourcegroup.tf b/examples/terraform-auth/modules/resource_group/resourcegroup.tf deleted file mode 100644 index ff7aae5..0000000 --- a/examples/terraform-auth/modules/resource_group/resourcegroup.tf +++ /dev/null @@ -1,24 +0,0 @@ -variable "name" { - type = string -} - -variable "region" { - type = string -} - -resource "azurerm_resource_group" "resource_group" { - name = var.name - location = var.region - - tags = { - Project = "EventGridViewerBlazor" - } -} - -output "id" { - value = azurerm_resource_group.resource_group.id -} - -output "name" { - value = var.name -} \ No newline at end of file diff --git a/examples/terraform-no-auth/README.md b/examples/terraform-no-auth/README.md deleted file mode 100644 index c800bbe..0000000 --- a/examples/terraform-no-auth/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# Overview - -## Prerequisites - -1. Azure Account -1. Docker *(optional)* -1. Basic knowledge of [Terraform](https://www.terraform.io/) - -### Azure Resources - -- Resource Group -- App Service Plan -- Web App Service -- eventgrid-viewer-blazor code - -Instructions: - -**Note:** If you have already have terraform & azure-cli installed, you just need to login to azure using azure-cli & set the default subscription. - -1. Open a terminal and switch to the *examples* directory -1. Run ```docker build -t egvb-samples .``` -1. You will need to set the Terraform environment variables when you run the docker container. You will need to provide: - - A azure subscription - - A resource group name - - A resource group region - - A app service plan name - - A web app name -1. Run the following bash commands in order: - -```bash - # Replace the environment variables - docker run -it -e AZURE_SUBSCRIPTION_ID= -e TF_VAR_resource_group_name= -e TF_VAR_resource_group_region= -e TF_VAR_web_app_name= -e TF_VAR_app_service_plan_name= egvb-samples - - # login into azure - az login - - # set the default azure subscription - az account set -s $AZURE_SUBSCRIPTION_ID - - # switch to the terraform-auth directory - cd terraform-no-auth - - # init terraform - terraform init - - # terraform plan to view plan - terraform plan - - # terraform apply to deploy - terraform apply -``` - -## Teardown - -```bash - terraform destroy -``` diff --git a/examples/terraform-no-auth/main.tf b/examples/terraform-no-auth/main.tf deleted file mode 100644 index 3c74367..0000000 --- a/examples/terraform-no-auth/main.tf +++ /dev/null @@ -1,35 +0,0 @@ -provider "azurerm" { - version = "=1.36.0" -} - -variable "resource_group_name" { - type = string -} - -variable "resource_group_region" { - type = string -} - -variable "web_app_name" { - type = string -} - -variable "app_service_plan_name" { - type = string -} - -module "resourcegroup" { - source = "./modules/resource_group" - - name = var.resource_group_name - region = var.resource_group_region -} - -module "egvb" { - source = "./modules/egvb" - create_resource = true - web_app_name = var.web_app_name - app_service_plan_name = var.app_service_plan_name - resource_group_name = module.resourcegroup.name - resource_group_id = module.resourcegroup.id -} \ No newline at end of file diff --git a/examples/terraform-no-auth/modules/egvb/egvb.tf b/examples/terraform-no-auth/modules/egvb/egvb.tf deleted file mode 100644 index 78c8844..0000000 --- a/examples/terraform-no-auth/modules/egvb/egvb.tf +++ /dev/null @@ -1,42 +0,0 @@ -variable "resource_group_name" { - type = string -} - -variable "app_service_plan_name" { - type = string -} - -variable "web_app_name" { - type = string -} - -variable "create_resource" { - type = bool -} - -variable "resource_group_id" {} - -resource "random_string" "random" { - length = 12 - upper = false - special = false -} - -data "http" "deployjson" { - url = "https://raw.githubusercontent.com/Azure-Samples/eventgrid-viewer-blazor/main/infrastructure/arm/azuredeploy.json" -} - -resource "azurerm_template_deployment" "web_app_deploy" { - count = var.create_resource ? 1 : 0 - name = random_string.random.result - resource_group_name = var.resource_group_name - deployment_mode = "Incremental" - template_body = data.http.deployjson.body - - parameters = { - siteName = var.web_app_name - hostingPlanName = var.app_service_plan_name - } - - depends_on = [var.resource_group_id] -} diff --git a/examples/terraform-no-auth/modules/resource_group/resourcegroup.tf b/examples/terraform-no-auth/modules/resource_group/resourcegroup.tf deleted file mode 100644 index f659e6c..0000000 --- a/examples/terraform-no-auth/modules/resource_group/resourcegroup.tf +++ /dev/null @@ -1,20 +0,0 @@ -variable "name" {} - -variable "region" {} - -resource "azurerm_resource_group" "resource_group" { - name = var.name - location = var.region - - tags = { - Project = "EventGridViewerBlazor" - } -} - -output "id" { - value = azurerm_resource_group.resource_group.id -} - -output "name" { - value = var.name -} \ No newline at end of file diff --git a/infrastructure/arm/azuredeploy.json b/infra/arm/azuredeploy.json similarity index 57% rename from infrastructure/arm/azuredeploy.json rename to infra/arm/azuredeploy.json index 0862b29..19632b5 100644 --- a/infrastructure/arm/azuredeploy.json +++ b/infra/arm/azuredeploy.json @@ -69,44 +69,48 @@ "metadata": { "description": "Location for all resources." } - }, - "enableAuth": { - "type": "string", - "allowedValues": [ - "false", - "true" - ], - "defaultValue": "false", - "metadata": { - "description": "Determines if Azure AD authentication will be enabled." - } - }, - "keyvaultName": { - "type": "string", - "defaultValue": "none", - "metadata": { - "description": "The name of the Azure Keyvault to store secrets if enabling authentication." - } - }, - "azAdDomain": { - "type": "string", - "defaultValue": "none", - "metadata": { - "description": "The Azure AD Primary domain if enabling authentication ie youraccount.onmicrosoft.com." - } } }, "variables": { "projectFile": "src/Blazor.EventGridViewer.ServerApp/Blazor.EventGridViewer.ServerApp.csproj", - "azAdTenantId": "[subscription().tenantId]", - "azAdInstance": "https://login.microsoftonline.com/", - "azAdCallbackPath": "/signin-oidc", - "azAdSignedOutCallbackPath": "/signout-callback-oidc", - "azAdClientIdSecretName": "sct-egvb-azad-client-id", - "azAdClientId": "[concat('@Microsoft.KeyVault(SecretUri=https://', parameters('keyvaultName'), '.vault.azure.net/secrets/', variables('azAdClientIdSecretName'), '/)')]", - "netFrameworkVersion": "v9.0" + "netFrameworkVersion": "v9.0", + "logAnalyticsWorkspaceName": "[concat('law-', parameters('siteName'))]", + "applicationInsightsName": "[concat('ai-', parameters('siteName'))]" }, "resources": [ + { + "type": "Microsoft.OperationalInsights/workspaces", + "apiVersion": "2022-10-01", + "name": "[variables('logAnalyticsWorkspaceName')]", + "location": "[parameters('location')]", + "properties": { + "sku": { + "name": "PerGB2018" + }, + "retentionInDays": 30, + "features": { + "enableLogAccessUsingOnlyResourcePermissions": true + } + } + }, + { + "type": "Microsoft.Insights/components", + "apiVersion": "2020-02-02", + "name": "[variables('applicationInsightsName')]", + "location": "[parameters('location')]", + "kind": "web", + "dependsOn": [ + "[resourceId('Microsoft.OperationalInsights/workspaces', variables('logAnalyticsWorkspaceName'))]" + ], + "properties": { + "Application_Type": "web", + "WorkspaceResourceId": "[resourceId('Microsoft.OperationalInsights/workspaces', variables('logAnalyticsWorkspaceName'))]", + "SamplingPercentage": 5, + "RetentionInDays": 90, + "DisableIpMasking": false, + "DisableLocalAuth": false + } + }, { "apiVersion": "2015-08-01", "name": "[parameters('hostingPlanName')]", @@ -126,7 +130,8 @@ "type": "Microsoft.Web/sites", "location": "[parameters('location')]", "dependsOn": [ - "[resourceId('Microsoft.Web/serverfarms', parameters('hostingPlanName'))]" + "[resourceId('Microsoft.Web/serverfarms', parameters('hostingPlanName'))]", + "[resourceId('Microsoft.Insights/components', variables('applicationInsightsName'))]" ], "properties": { "serverFarmId": "[parameters('hostingPlanName')]", @@ -139,32 +144,16 @@ "value": "[variables('projectFile')]" }, { - "name": "EnableAuth", - "value": "[parameters('enableAuth')]" - }, - { - "name": "AzureAd:TenantId", - "value": "[if(equals(parameters('enableAuth'), 'false'), '', variables('azAdTenantId'))]" - }, - { - "name": "AzureAd:Instance", - "value": "[if(equals(parameters('enableAuth'), 'false'), '', variables('azAdInstance'))]" - }, - { - "name": "AzureAd:CallbackPath", - "value": "[if(equals(parameters('enableAuth'), 'false'), '', variables('azAdCallbackPath'))]" - }, - { - "name": "AzureAd:SignedOutCallbackPath", - "value": "[if(equals(parameters('enableAuth'), 'false'), '', variables('azAdSignedOutCallbackPath'))]" + "name": "ASPNETCORE_ENVIRONMENT", + "value": "Production" }, { - "name": "AzureAd:Domain", - "value": "[if(equals(parameters('enableAuth'), 'false'), '', parameters('azAdDomain'))]" + "name": "APPLICATIONINSIGHTS_CONNECTION_STRING", + "value": "[reference(resourceId('Microsoft.Insights/components', variables('applicationInsightsName')), '2020-02-02').ConnectionString]" }, { - "name": "AzureAd:ClientId", - "value": "[if(equals(parameters('enableAuth'), 'false'), '', variables('azAdClientId'))]" + "name": "APPINSIGHTS_INSTRUMENTATIONKEY", + "value": "[reference(resourceId('Microsoft.Insights/components', variables('applicationInsightsName')), '2020-02-02').InstrumentationKey]" } ] } @@ -185,5 +174,27 @@ } ] } - ] + ], + "outputs": { + "webAppUrl": { + "type": "string", + "value": "[concat('https://', reference(resourceId('Microsoft.Web/sites', parameters('siteName'))).defaultHostName)]" + }, + "applicationInsightsName": { + "type": "string", + "value": "[variables('applicationInsightsName')]" + }, + "applicationInsightsInstrumentationKey": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Insights/components', variables('applicationInsightsName')), '2020-02-02').InstrumentationKey]" + }, + "applicationInsightsConnectionString": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Insights/components', variables('applicationInsightsName')), '2020-02-02').ConnectionString]" + }, + "logAnalyticsWorkspaceName": { + "type": "string", + "value": "[variables('logAnalyticsWorkspaceName')]" + } + } } \ No newline at end of file diff --git a/infra/main.bicep b/infra/main.bicep new file mode 100644 index 0000000..9dcf643 --- /dev/null +++ b/infra/main.bicep @@ -0,0 +1,113 @@ +targetScope = 'resourceGroup' + +metadata name = 'EventGrid Viewer Blazor' +metadata description = 'EventGrid Viewer Blazor application infrastructure' +metadata author = 'Azure Samples' + +@minLength(2) +@maxLength(50) +@description('The azure web app name.') +param siteName string + +@minLength(2) +@maxLength(50) +@description('The azure web hosting plan name.') +param hostingPlanName string + +@allowed([ + 'F1' + 'D1' + 'B1' + 'B2' + 'B3' + 'S1' + 'S2' + 'S3' + 'P1' + 'P2' + 'P3' + 'P1V2' + 'P2V2' + 'P3V2' + 'P1V3' + 'P2V3' + 'P3V3' + 'EP1' + 'EP2' + 'EP3' + 'I1' + 'I2' + 'I3' + 'I1V2' + 'I2V2' + 'I3V2' +]) +@description('The pricing tier for the hosting plan.') +param sku string = 'F1' + +@description('The URL for the GitHub repository that contains the project to deploy.') +param repoURL string = 'https://github.com/Azure-Samples/eventgrid-viewer-blazor.git' + +@description('The branch of the GitHub repository to use.') +param branch string = 'main' + +@description('Location for all resources.') +param location string = resourceGroup().location + +@description('Environment name for resource naming.') +param environmentName string = 'dev' + +@description('Deploy source control configuration (set to false if already configured).') +param deploySourceControl bool = false + +// Common tags for all resources +var commonTags = { + 'azd-env-name': environmentName +} + +// Deploy monitoring resources +module monitoring 'modules/monitoring.bicep' = { + name: 'monitoring' + params: { + location: location + namePrefix: siteName + environmentName: environmentName + tags: commonTags + } +} + +// Deploy app service resources +module appService 'modules/appservice.bicep' = { + name: 'appservice' + params: { + location: location + siteName: siteName + hostingPlanName: hostingPlanName + sku: sku + applicationInsightsConnectionString: monitoring.outputs.applicationInsights.connectionString + applicationInsightsInstrumentationKey: monitoring.outputs.applicationInsights.instrumentationKey + repoURL: repoURL + branch: branch + deploySourceControl: deploySourceControl + tags: commonTags + } +} + +// Outputs +@description('The URL of the deployed web application.') +output webAppUrl string = appService.outputs.webApp.url + +@description('The name of the Application Insights instance.') +output applicationInsightsName string = monitoring.outputs.applicationInsights.name + +@description('The instrumentation key of the Application Insights instance.') +output applicationInsightsInstrumentationKey string = monitoring.outputs.applicationInsights.instrumentationKey + +@description('The connection string of the Application Insights instance.') +output applicationInsightsConnectionString string = monitoring.outputs.applicationInsights.connectionString + +@description('The name of the Log Analytics workspace.') +output logAnalyticsWorkspaceName string = monitoring.outputs.logAnalyticsWorkspace.name + +@description('The EventGrid webhook endpoint URL.') +output eventGridWebhookUrl string = '${appService.outputs.webApp.url}/api/eventgrid' \ No newline at end of file diff --git a/infra/main.json b/infra/main.json new file mode 100644 index 0000000..cc25ec8 --- /dev/null +++ b/infra/main.json @@ -0,0 +1,638 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "17377841943328396753" + }, + "name": "EventGrid Viewer Blazor", + "description": "EventGrid Viewer Blazor application infrastructure", + "author": "Azure Samples" + }, + "parameters": { + "siteName": { + "type": "string", + "minLength": 2, + "maxLength": 50, + "metadata": { + "description": "The azure web app name." + } + }, + "hostingPlanName": { + "type": "string", + "minLength": 2, + "maxLength": 50, + "metadata": { + "description": "The azure web hosting plan name." + } + }, + "sku": { + "type": "string", + "defaultValue": "F1", + "allowedValues": [ + "F1", + "D1", + "B1", + "B2", + "B3", + "S1", + "S2", + "S3", + "P1", + "P2", + "P3", + "P1V2", + "P2V2", + "P3V2", + "P1V3", + "P2V3", + "P3V3", + "EP1", + "EP2", + "EP3", + "I1", + "I2", + "I3", + "I1V2", + "I2V2", + "I3V2" + ], + "metadata": { + "description": "The pricing tier for the hosting plan." + } + }, + "repoURL": { + "type": "string", + "defaultValue": "https://github.com/Azure-Samples/eventgrid-viewer-blazor.git", + "metadata": { + "description": "The URL for the GitHub repository that contains the project to deploy." + } + }, + "branch": { + "type": "string", + "defaultValue": "main", + "metadata": { + "description": "The branch of the GitHub repository to use." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Location for all resources." + } + }, + "enableAuth": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Determines if Azure AD authentication will be enabled." + } + }, + "keyvaultName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The name of the Azure Keyvault to store secrets if enabling authentication." + } + }, + "azAdDomain": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The Azure AD Primary domain if enabling authentication ie youraccount.onmicrosoft.com." + } + }, + "environmentName": { + "type": "string", + "defaultValue": "dev", + "metadata": { + "description": "Environment name for resource naming." + } + }, + "deploySourceControl": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Deploy source control configuration (set to false if already configured)." + } + } + }, + "variables": { + "commonTags": { + "azd-env-name": "[parameters('environmentName')]" + } + }, + "resources": [ + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "monitoring", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "namePrefix": { + "value": "[parameters('siteName')]" + }, + "environmentName": { + "value": "[parameters('environmentName')]" + }, + "tags": { + "value": "[variables('commonTags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "4182947068713190629" + } + }, + "parameters": { + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Location for all resources." + } + }, + "namePrefix": { + "type": "string", + "metadata": { + "description": "The name prefix for monitoring resources." + } + }, + "environmentName": { + "type": "string", + "metadata": { + "description": "Environment name for resource naming." + } + }, + "retentionInDays": { + "type": "int", + "defaultValue": 30, + "metadata": { + "description": "Log Analytics workspace retention in days." + } + }, + "samplingPercentage": { + "type": "int", + "defaultValue": 5, + "metadata": { + "description": "Application Insights sampling percentage." + } + }, + "appInsightsRetentionInDays": { + "type": "int", + "defaultValue": 90, + "metadata": { + "description": "Application Insights retention in days." + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Tags to apply to all resources." + } + } + }, + "variables": { + "logAnalyticsWorkspaceName": "[format('law-{0}-{1}', parameters('namePrefix'), parameters('environmentName'))]", + "applicationInsightsName": "[format('ai-{0}-{1}', parameters('namePrefix'), parameters('environmentName'))]" + }, + "resources": [ + { + "type": "Microsoft.OperationalInsights/workspaces", + "apiVersion": "2023-09-01", + "name": "[variables('logAnalyticsWorkspaceName')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "properties": { + "sku": { + "name": "PerGB2018" + }, + "retentionInDays": "[parameters('retentionInDays')]", + "features": { + "enableLogAccessUsingOnlyResourcePermissions": true + } + } + }, + { + "type": "Microsoft.Insights/components", + "apiVersion": "2020-02-02", + "name": "[variables('applicationInsightsName')]", + "location": "[parameters('location')]", + "kind": "web", + "tags": "[parameters('tags')]", + "properties": { + "Application_Type": "web", + "WorkspaceResourceId": "[resourceId('Microsoft.OperationalInsights/workspaces', variables('logAnalyticsWorkspaceName'))]", + "SamplingPercentage": "[parameters('samplingPercentage')]", + "RetentionInDays": "[parameters('appInsightsRetentionInDays')]", + "DisableIpMasking": false, + "DisableLocalAuth": false + }, + "dependsOn": [ + "[resourceId('Microsoft.OperationalInsights/workspaces', variables('logAnalyticsWorkspaceName'))]" + ] + } + ], + "outputs": { + "applicationInsights": { + "type": "object", + "metadata": { + "description": "The Application Insights instance." + }, + "value": { + "id": "[resourceId('Microsoft.Insights/components', variables('applicationInsightsName'))]", + "name": "[variables('applicationInsightsName')]", + "instrumentationKey": "[reference(resourceId('Microsoft.Insights/components', variables('applicationInsightsName')), '2020-02-02').InstrumentationKey]", + "connectionString": "[reference(resourceId('Microsoft.Insights/components', variables('applicationInsightsName')), '2020-02-02').ConnectionString]" + } + }, + "logAnalyticsWorkspace": { + "type": "object", + "metadata": { + "description": "The Log Analytics workspace instance." + }, + "value": { + "id": "[resourceId('Microsoft.OperationalInsights/workspaces', variables('logAnalyticsWorkspaceName'))]", + "name": "[variables('logAnalyticsWorkspaceName')]" + } + } + } + } + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2025-04-01", + "name": "appservice", + "properties": { + "expressionEvaluationOptions": { + "scope": "inner" + }, + "mode": "Incremental", + "parameters": { + "location": { + "value": "[parameters('location')]" + }, + "siteName": { + "value": "[parameters('siteName')]" + }, + "hostingPlanName": { + "value": "[parameters('hostingPlanName')]" + }, + "sku": { + "value": "[parameters('sku')]" + }, + "applicationInsightsConnectionString": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'monitoring'), '2025-04-01').outputs.applicationInsights.value.connectionString]" + }, + "applicationInsightsInstrumentationKey": { + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'monitoring'), '2025-04-01').outputs.applicationInsights.value.instrumentationKey]" + }, + "repoURL": { + "value": "[parameters('repoURL')]" + }, + "branch": { + "value": "[parameters('branch')]" + }, + "deploySourceControl": { + "value": "[parameters('deploySourceControl')]" + }, + "enableAuth": { + "value": "[parameters('enableAuth')]" + }, + "keyvaultName": { + "value": "[parameters('keyvaultName')]" + }, + "azAdDomain": { + "value": "[parameters('azAdDomain')]" + }, + "tags": { + "value": "[variables('commonTags')]" + } + }, + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "metadata": { + "_generator": { + "name": "bicep", + "version": "0.38.33.27573", + "templateHash": "4645136516621972443" + } + }, + "parameters": { + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Location for all resources." + } + }, + "siteName": { + "type": "string", + "metadata": { + "description": "The azure web app name." + } + }, + "hostingPlanName": { + "type": "string", + "metadata": { + "description": "The azure web hosting plan name." + } + }, + "sku": { + "type": "string", + "allowedValues": [ + "F1", + "D1", + "B1", + "B2", + "B3", + "S1", + "S2", + "S3", + "P1", + "P2", + "P3", + "P1V2", + "P2V2", + "P3V2", + "P1V3", + "P2V3", + "P3V3", + "EP1", + "EP2", + "EP3", + "I1", + "I2", + "I3", + "I1V2", + "I2V2", + "I3V2" + ], + "metadata": { + "description": "The pricing tier for the hosting plan." + } + }, + "applicationInsightsConnectionString": { + "type": "string", + "metadata": { + "description": "Application Insights connection string." + } + }, + "applicationInsightsInstrumentationKey": { + "type": "string", + "metadata": { + "description": "Application Insights instrumentation key." + } + }, + "repoURL": { + "type": "string", + "metadata": { + "description": "The URL for the GitHub repository that contains the project to deploy." + } + }, + "branch": { + "type": "string", + "metadata": { + "description": "The branch of the GitHub repository to use." + } + }, + "deploySourceControl": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Deploy source control configuration (set to false if already configured)." + } + }, + "enableAuth": { + "type": "bool", + "defaultValue": false, + "metadata": { + "description": "Determines if Azure AD authentication will be enabled." + } + }, + "keyvaultName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The name of the Azure Keyvault to store secrets if enabling authentication." + } + }, + "azAdDomain": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The Azure AD Primary domain if enabling authentication." + } + }, + "tags": { + "type": "object", + "defaultValue": {}, + "metadata": { + "description": "Tags to apply to all resources." + } + } + }, + "variables": { + "projectFile": "src/Blazor.EventGridViewer.ServerApp/Blazor.EventGridViewer.ServerApp.csproj", + "azAdTenantId": "[tenant().tenantId]", + "azAdInstance": "[environment().authentication.loginEndpoint]", + "azAdCallbackPath": "/signin-oidc", + "azAdSignedOutCallbackPath": "/signout-callback-oidc", + "azAdClientIdSecretName": "sct-egvb-azad-client-id", + "azAdClientId": "[if(and(parameters('enableAuth'), not(empty(parameters('keyvaultName')))), format('@Microsoft.KeyVault(SecretUri=https://{0}.vault.azure.net/secrets/{1}/)', parameters('keyvaultName'), variables('azAdClientIdSecretName')), '')]", + "netFrameworkVersion": "v9.0" + }, + "resources": [ + { + "type": "Microsoft.Web/serverfarms", + "apiVersion": "2023-12-01", + "name": "[parameters('hostingPlanName')]", + "location": "[parameters('location')]", + "tags": "[parameters('tags')]", + "sku": { + "name": "[parameters('sku')]", + "capacity": "[if(equals(parameters('sku'), 'F1'), 0, 1)]" + }, + "properties": { + "reserved": false + } + }, + { + "type": "Microsoft.Web/sites", + "apiVersion": "2023-12-01", + "name": "[parameters('siteName')]", + "location": "[parameters('location')]", + "tags": "[union(parameters('tags'), createObject('azd-service-name', 'web'))]", + "properties": { + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('hostingPlanName'))]", + "httpsOnly": true, + "siteConfig": { + "webSocketsEnabled": true, + "netFrameworkVersion": "[variables('netFrameworkVersion')]", + "metadata": [ + { + "name": "CURRENT_STACK", + "value": "dotnet" + } + ], + "appSettings": [ + { + "name": "PROJECT", + "value": "[variables('projectFile')]" + }, + { + "name": "ASPNETCORE_ENVIRONMENT", + "value": "Production" + }, + { + "name": "APPLICATIONINSIGHTS_CONNECTION_STRING", + "value": "[parameters('applicationInsightsConnectionString')]" + }, + { + "name": "APPINSIGHTS_INSTRUMENTATIONKEY", + "value": "[parameters('applicationInsightsInstrumentationKey')]" + }, + { + "name": "EnableAuth", + "value": "[string(parameters('enableAuth'))]" + }, + { + "name": "AzureAd:TenantId", + "value": "[if(parameters('enableAuth'), variables('azAdTenantId'), '')]" + }, + { + "name": "AzureAd:Instance", + "value": "[if(parameters('enableAuth'), variables('azAdInstance'), '')]" + }, + { + "name": "AzureAd:CallbackPath", + "value": "[if(parameters('enableAuth'), variables('azAdCallbackPath'), '')]" + }, + { + "name": "AzureAd:SignedOutCallbackPath", + "value": "[if(parameters('enableAuth'), variables('azAdSignedOutCallbackPath'), '')]" + }, + { + "name": "AzureAd:Domain", + "value": "[if(parameters('enableAuth'), parameters('azAdDomain'), '')]" + }, + { + "name": "AzureAd:ClientId", + "value": "[if(parameters('enableAuth'), variables('azAdClientId'), '')]" + } + ] + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms', parameters('hostingPlanName'))]" + ] + }, + { + "condition": "[parameters('deploySourceControl')]", + "type": "Microsoft.Web/sites/sourcecontrols", + "apiVersion": "2023-12-01", + "name": "[format('{0}/{1}', parameters('siteName'), 'web')]", + "properties": { + "repoUrl": "[parameters('repoURL')]", + "branch": "[parameters('branch')]", + "isManualIntegration": true + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', parameters('siteName'))]" + ] + } + ], + "outputs": { + "webApp": { + "type": "object", + "metadata": { + "description": "The Web App instance." + }, + "value": { + "id": "[resourceId('Microsoft.Web/sites', parameters('siteName'))]", + "name": "[parameters('siteName')]", + "defaultHostName": "[reference(resourceId('Microsoft.Web/sites', parameters('siteName')), '2023-12-01').defaultHostName]", + "url": "[format('https://{0}', reference(resourceId('Microsoft.Web/sites', parameters('siteName')), '2023-12-01').defaultHostName)]" + } + }, + "appServicePlan": { + "type": "object", + "metadata": { + "description": "The App Service Plan instance." + }, + "value": { + "id": "[resourceId('Microsoft.Web/serverfarms', parameters('hostingPlanName'))]", + "name": "[parameters('hostingPlanName')]" + } + } + } + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Resources/deployments', 'monitoring')]" + ] + } + ], + "outputs": { + "webAppUrl": { + "type": "string", + "metadata": { + "description": "The URL of the deployed web application." + }, + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'appservice'), '2025-04-01').outputs.webApp.value.url]" + }, + "applicationInsightsName": { + "type": "string", + "metadata": { + "description": "The name of the Application Insights instance." + }, + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'monitoring'), '2025-04-01').outputs.applicationInsights.value.name]" + }, + "applicationInsightsInstrumentationKey": { + "type": "string", + "metadata": { + "description": "The instrumentation key of the Application Insights instance." + }, + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'monitoring'), '2025-04-01').outputs.applicationInsights.value.instrumentationKey]" + }, + "applicationInsightsConnectionString": { + "type": "string", + "metadata": { + "description": "The connection string of the Application Insights instance." + }, + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'monitoring'), '2025-04-01').outputs.applicationInsights.value.connectionString]" + }, + "logAnalyticsWorkspaceName": { + "type": "string", + "metadata": { + "description": "The name of the Log Analytics workspace." + }, + "value": "[reference(resourceId('Microsoft.Resources/deployments', 'monitoring'), '2025-04-01').outputs.logAnalyticsWorkspace.value.name]" + }, + "eventGridWebhookUrl": { + "type": "string", + "metadata": { + "description": "The EventGrid webhook endpoint URL." + }, + "value": "[format('{0}/api/eventgrid', reference(resourceId('Microsoft.Resources/deployments', 'appservice'), '2025-04-01').outputs.webApp.value.url)]" + } + } +} \ No newline at end of file diff --git a/infra/main.parameters.json b/infra/main.parameters.json new file mode 100644 index 0000000..506c4d3 --- /dev/null +++ b/infra/main.parameters.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "siteName": { + "value": "${AZURE_APP_SERVICE_NAME}" + }, + "hostingPlanName": { + "value": "${AZURE_APP_SERVICE_PLAN_NAME}" + }, + "sku": { + "value": "${AZURE_APP_SERVICE_SKU=F1}" + }, + "repoURL": { + "value": "${AZURE_REPO_URL=https://github.com/Azure-Samples/eventgrid-viewer-blazor.git}" + }, + "branch": { + "value": "${AZURE_REPO_BRANCH=main}" + }, + "location": { + "value": "${AZURE_LOCATION}" + }, + "enableAuth": { + "value": "${AZURE_ENABLE_AUTH=false}" + }, + "keyvaultName": { + "value": "${AZURE_KEYVAULT_NAME=}" + }, + "azAdDomain": { + "value": "${AZURE_AD_DOMAIN=}" + }, + "environmentName": { + "value": "${AZURE_ENV_NAME}" + }, + "deploySourceControl": { + "value": "${AZURE_DEPLOY_SOURCE_CONTROL=false}" + } + } +} \ No newline at end of file diff --git a/infra/modules/appservice.bicep b/infra/modules/appservice.bicep new file mode 100644 index 0000000..d17bab7 --- /dev/null +++ b/infra/modules/appservice.bicep @@ -0,0 +1,135 @@ +@description('Location for all resources.') +param location string = resourceGroup().location + +@description('The azure web app name.') +param siteName string + +@description('The azure web hosting plan name.') +param hostingPlanName string + +@allowed([ + 'F1' + 'D1' + 'B1' + 'B2' + 'B3' + 'S1' + 'S2' + 'S3' + 'P1' + 'P2' + 'P3' + 'P1V2' + 'P2V2' + 'P3V2' + 'P1V3' + 'P2V3' + 'P3V3' + 'EP1' + 'EP2' + 'EP3' + 'I1' + 'I2' + 'I3' + 'I1V2' + 'I2V2' + 'I3V2' +]) +@description('The pricing tier for the hosting plan.') +param sku string + +@description('Application Insights connection string.') +param applicationInsightsConnectionString string + +@description('Application Insights instrumentation key.') +param applicationInsightsInstrumentationKey string + +@description('The URL for the GitHub repository that contains the project to deploy.') +param repoURL string + +@description('The branch of the GitHub repository to use.') +param branch string + +@description('Deploy source control configuration (set to false if already configured).') +param deploySourceControl bool = false + +@description('Tags to apply to all resources.') +param tags object = {} + +// Variables + +// App Service Plan +resource appServicePlan 'Microsoft.Web/serverfarms@2023-12-01' = { + name: hostingPlanName + location: location + tags: tags + sku: { + name: sku + capacity: sku == 'F1' ? 0 : 1 + } + properties: { + reserved: true // Linux App Service + } +} + +// Web App +resource webApp 'Microsoft.Web/sites@2023-12-01' = { + name: siteName + location: location + tags: union(tags, { + 'azd-service-name': 'web' + }) + properties: { + serverFarmId: appServicePlan.id + httpsOnly: true + siteConfig: { + webSocketsEnabled: true + linuxFxVersion: 'DOTNETCORE|9.0' + metadata: [ + { + name: 'CURRENT_STACK' + value: 'dotnetcore' + } + ] + appSettings: [ + { + name: 'ASPNETCORE_ENVIRONMENT' + value: 'Production' + } + { + name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' + value: applicationInsightsConnectionString + } + { + name: 'APPINSIGHTS_INSTRUMENTATIONKEY' + value: applicationInsightsInstrumentationKey + } + ] + } + } +} + +// Source Control (GitHub deployment) - only deploy if requested +resource sourceControl 'Microsoft.Web/sites/sourcecontrols@2023-12-01' = if (deploySourceControl) { + name: 'web' + parent: webApp + properties: { + repoUrl: repoURL + branch: branch + isManualIntegration: true + } +} + +@description('The Web App instance.') +output webApp object = { + id: webApp.id + name: webApp.name + defaultHostName: webApp.properties.defaultHostName + url: 'https://${webApp.properties.defaultHostName}' +} + +@description('The App Service Plan instance.') +output appServicePlan object = { + id: appServicePlan.id + name: appServicePlan.name +} diff --git a/infra/modules/monitoring.bicep b/infra/modules/monitoring.bicep new file mode 100644 index 0000000..ab14de1 --- /dev/null +++ b/infra/modules/monitoring.bicep @@ -0,0 +1,69 @@ +@description('Location for all resources.') +param location string = resourceGroup().location + +@description('The name prefix for monitoring resources.') +param namePrefix string + +@description('Environment name for resource naming.') +param environmentName string + +@description('Log Analytics workspace retention in days.') +param retentionInDays int = 30 + +@description('Application Insights sampling percentage.') +param samplingPercentage int = 5 + +@description('Application Insights retention in days.') +param appInsightsRetentionInDays int = 90 + +@description('Tags to apply to all resources.') +param tags object = {} + +var logAnalyticsWorkspaceName = 'law-${namePrefix}-${environmentName}' +var applicationInsightsName = 'ai-${namePrefix}-${environmentName}' + +// Log Analytics Workspace +resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { + name: logAnalyticsWorkspaceName + location: location + tags: tags + properties: { + sku: { + name: 'PerGB2018' + } + retentionInDays: retentionInDays + features: { + enableLogAccessUsingOnlyResourcePermissions: true + } + } +} + +// Application Insights +resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { + name: applicationInsightsName + location: location + kind: 'web' + tags: tags + properties: { + Application_Type: 'web' + WorkspaceResourceId: logAnalyticsWorkspace.id + SamplingPercentage: samplingPercentage + RetentionInDays: appInsightsRetentionInDays + DisableIpMasking: false + DisableLocalAuth: false + } +} + +@description('The Application Insights instance.') +output applicationInsights object = { + id: applicationInsights.id + name: applicationInsights.name + instrumentationKey: applicationInsights.properties.InstrumentationKey + connectionString: applicationInsights.properties.ConnectionString +} + +@description('The Log Analytics workspace instance.') +output logAnalyticsWorkspace object = { + id: logAnalyticsWorkspace.id + name: logAnalyticsWorkspace.name +} \ No newline at end of file diff --git a/infrastructure/README.md b/infrastructure/README.md deleted file mode 100644 index 84bbf2a..0000000 --- a/infrastructure/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# Overview - -The purpose of this document is to provide documentation about the ARM Template & Bash script used to deploy the eventgrid-viewer-blazor application to Azure. For detailed documentation about the Azure Pipelines, visit [this](azure-pipelines/README.md) document. - -## Helpers scripts - -- ```utils.sh``` - Script contains utility functions that are shared across all deployment scripts. - -## ```azuredeploy.sh Script``` - -### Purpose - -The *azuredeploy* script is used to deploy the eventgrid-viewer-blazor application to Azure directly from a Git repository. You can run the script locally & it is also called by the [eventgrid_viewer_blazor_trigger_main Pipeline](azure-pipelines/README.md#eventgrid_viewer_blazor_trigger_main-pipeline) - -### ARM Template - -The *azuredeploy* script uses the *azuredeploy.json* ARM Template to deploy resources to Azure. - -### Azure Resources - -- Resource Group -- App Service Plan -- Web App Service -- eventgrid-viewer-blazor code - -### Script Parameters - -- g - Resource group name - The name of the Azure resource group ie rg-cse-egvb-dev (required) -- s - Site name - The name of the Azure web app ie as-cse-egvb-dev (required) -- p - Hosting plan name - The name of the Azure app service plan ie asp-cse-egvb-dev (required) -- e - Environment - Environment ie dev, test (required) -- l - Location - Azure location ie eastus, westus (required) -- r - Repo url - The git repository where the codebase is located ie ```https://github.com/Azure-Samples/eventgrid-viewer-blazor.git``` (required) -- b - Repo branch - The git repository branch to use ie main (required) -- a - Enable auth - The flag to enable Entra ID authentication. Default is false (optional) -- k - Keyvault name - The name of the Azure Keyvault to store Entra ID secrets if enabling authentication. Default is none (optional) -- d - Entra ID domain - The Entra ID Primary Domain if enabling authentication ie ```{youraccount}.onmicrosoft.com```. Default is none (optional) -- h - Help - Help (optional) - -**Sample Usage:** - -```bash - # log into azure & set the default subscription - az login - az account set -s {SUBSCRIPTION_NAME_OR_ID} - - # grant execute permissions & run the script - chmod +x azuredeploy.sh && - ./azuredeploy.sh -g rg-cse-egvb-dev -s as-cse-egvb-dev -p asp-cse-egvb-dev -l eastus -r https://github.com/Azure-Samples/eventgrid-viewer-blazor.git -b main -``` diff --git a/infrastructure/azure-pipelines/README.md b/infrastructure/azure-pipelines/README.md deleted file mode 100644 index 6b07343..0000000 --- a/infrastructure/azure-pipelines/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# Overview - -The purpose of this document is to provide information about the Azure Pipelines used to deploy this project. - -## eventgrid_viewer_blazor_pr_main Pipeline - -The *eventgrid_viewer_blazor_pr_main* Azure Pipeline is used to build the eventgrid-viewer-blazor application upon a Pull Request. The pipeline uses the *build-test-report-steps-template* yaml as template to aid in the build process. - -### Pipeline Variables - -None. - -## eventgrid_viewer_blazor_trigger_main Pipeline - -The *eventgrid_viewer_blazor_trigger_main* Azure Pipeline is used to deploy the eventgrid-viewer-blazor application to Azure. The pipeline uses the *deploy-steps-template* yaml as template to aid in the deployment process. - -### Pipeline Variables - -1. branch - Branch ie main -1. hostingPlanName - Azure App Service Plan name -1. resourceGroup - Azure Resource Group name -1. siteName - Azure Web App name diff --git a/infrastructure/azure-pipelines/eventgrid_viewer_blazor_pr_main.yml b/infrastructure/azure-pipelines/eventgrid_viewer_blazor_pr_main.yml deleted file mode 100644 index 7de17bd..0000000 --- a/infrastructure/azure-pipelines/eventgrid_viewer_blazor_pr_main.yml +++ /dev/null @@ -1,18 +0,0 @@ -trigger: none -pr: - branches: - include: - - main - -variables: - buildConfiguration: 'Release' - artifactName: 'Blazor.EventGridViewer.ServerApp' - workingDirectory: '$(Build.SourcesDirectory)/src/$(artifactName)' - blazorServerAppProjectName: '$(artifactName).csproj' - testWorkingDirectory: '$(Build.SourcesDirectory)/src/Blazor.EventGridViewer.Unit.Tests' - -pool: - vmImage: 'ubuntu-latest' - -steps: -- template: templates/steps/build-test-steps-template.yml \ No newline at end of file diff --git a/infrastructure/azure-pipelines/eventgrid_viewer_blazor_trigger_main.yml b/infrastructure/azure-pipelines/eventgrid_viewer_blazor_trigger_main.yml deleted file mode 100644 index 6dbda61..0000000 --- a/infrastructure/azure-pipelines/eventgrid_viewer_blazor_trigger_main.yml +++ /dev/null @@ -1,12 +0,0 @@ -trigger: - branches: - include: - - main -pool: - vmImage: 'ubuntu-latest' - -steps: -- template: templates/steps/deploy-steps-template.yml - parameters: - location: 'eastus' - repoURL: 'https://github.com/Azure-Samples/eventgrid-viewer-blazor.git' \ No newline at end of file diff --git a/infrastructure/azure-pipelines/templates/steps/build-test-steps-template.yml b/infrastructure/azure-pipelines/templates/steps/build-test-steps-template.yml deleted file mode 100644 index 0b48977..0000000 --- a/infrastructure/azure-pipelines/templates/steps/build-test-steps-template.yml +++ /dev/null @@ -1,47 +0,0 @@ -parameters: - - name: 'showFiles' - default: false - type: boolean - -steps: -- task: UseDotNet@2 - displayName: 'Use .NET Core sdk 5.0' - inputs: - packageType: sdk - version: 5.0.x - -# *** DEBUG *** -- script: 'find .' - condition: eq('${{ parameters.showFiles }}', true) - displayName: 'List all files before build.' - workingDirectory: $(Build.SourcesDirectory) - -- task: DotNetCoreCLI@2 - displayName: 'dotnet restore' - inputs: - command: 'restore' - projects: '**/*$(blazorServerAppProjectName)' - workingDirectory: $(workingDirectory) - -- task: DotNetCoreCLI@2 - displayName: 'dotnet build blazor server app' # If name changes here, reflect changes on warning gate filter below - inputs: - command: build - projects: '**/*$(blazorServerAppProjectName)' - arguments: '--configuration $(BuildConfiguration)' - workingDirectory: $(workingDirectory) - modifyOutputPath: true - -- task: DotNetCoreCLI@2 - displayName: 'dotnet build test projects' # If name changes here, reflect changes on warning gate filter below - inputs: - command: build - arguments: '--configuration $(BuildConfiguration)' - workingDirectory: $(testWorkingDirectory) - -- task: DotNetCoreCLI@2 - displayName: 'Run unit tests - $(BuildConfiguration)' - inputs: - command: 'test' - arguments: '--no-build --configuration $(BuildConfiguration)' - workingDirectory: $(testWorkingDirectory) \ No newline at end of file diff --git a/infrastructure/azure-pipelines/templates/steps/deploy-steps-template.yml b/infrastructure/azure-pipelines/templates/steps/deploy-steps-template.yml deleted file mode 100644 index e6f40ed..0000000 --- a/infrastructure/azure-pipelines/templates/steps/deploy-steps-template.yml +++ /dev/null @@ -1,17 +0,0 @@ -parameters: - - name: 'location' - type: string - - name: 'repoURL' - type: string - -steps: -- task: AzureCLI@2 - displayName: Azure CLI - inputs: - azureSubscription: {your_service_connection} - scriptType: bash - scriptLocation: inlineScript - inlineScript: | - chmod +x infrastructure/azuredeploy.sh && - bash infrastructure/azuredeploy.sh -g $(resourceGroup) -s $(siteName) -p $(hostingPlanName) \ - -l ${{ parameters.location }} -r ${{ parameters.repoURL }} -b $(branch) \ No newline at end of file diff --git a/infrastructure/azuredeploy.sh b/infrastructure/azuredeploy.sh deleted file mode 100755 index c7cdf64..0000000 --- a/infrastructure/azuredeploy.sh +++ /dev/null @@ -1,129 +0,0 @@ -#!/bin/bash - -############################################################################################################################################# -#- Purpose: Script is used to deploy the EventGrid Viewer Blazor application to Azure. -#- Parameters are: -#- [-g] resource group name - The name of the Azure resource group ie rg-cse-egvb-dev (required)" -#- [-s] site name - The name of the Azure web app ie as-cse-egvb-dev (required)" -#- [-p] hosting plan name - The name of the Azure app service plan ie asp-cse-egvb-dev (required)" -#- [-l] location - Azure location ie eastus, westus (required)" -#- [-r] repo url - The git repository where the codebase is located ie https://github.com/Azure-Samples/eventgrid-viewer-blazor.git (required)" -#- [-b] repo branch - The git repository branch to use ie main (required)" -#- [-a] enable auth - The flag to enable Azure AD authentication. Default is false (optional)" -#- [-k] keyvault name - The name of the Azure Keyvault to store Azure AD secrets if enabling authentication. Default is none (optional)" -#- [-d] azure ad domain - The Azure AD Primary Domain if enabling authentication ie youraccount.onmicrosoft.com. Default is none (optional)" -#- [-h] help - Help (optional)" -############################################################################################################################################# - -set -eu - -############################################################### -#- function used to print out script usage -############################################################### -function usage() { - echo - echo "Arguments:" - echo -e "\t-g \t The name of the Azure resource group ie rg-cse-egvb-dev (required)" - echo -e "\t-s \t The name of the Azure web app ie as-cse-egvb-dev (required)" - echo -e "\t-p \t The name of the Azure app service plan ie asp-cse-egvb-dev (required)" - echo -e "\t-l \t Azure location ie eastus, westus (required)" - echo -e "\t-r \t The git repository where the codebase is located ie https://github.com/Azure-Samples/eventgrid-viewer-blazor.git (required)" - echo -e "\t-b \t The git repository branch to use ie main (required)" - echo -e "\t-a \t The flag to enable Azure AD authentication. Default is false (optional)" - echo -e "\t-k \t The name of the Azure Keyvault to store Azure AD secrets if enabling authentication. Default is none (optional)" - echo -e "\t-d \t The azure ad domain - The Azure AD Primary Domain if enabling authentication ie youraccount.onmicrosoft.com. Default is none (optional)" - echo -e "\t-h \t Help (optional)" - echo - echo "Example:" - echo -e "./azuredeploy.sh -g rg-cse-egvb-dev -s as-cse-egvb-dev -p asp-cse-egvb-dev -l eastus -r https://github.com/Azure-Samples/eventgrid-viewer-blazor.git -b main -k kv-cse-egvb-dev -d microsoft.onmicrosoft.com -a" -} - -parent_path=$( - cd "$(dirname "${BASH_SOURCE[0]}")" - pwd -P -) - -cd "$parent_path" - -# add utility.sh script -source helpers/utils.sh - -# defaults -enableAuth=false -keyvaultName=none -azAdDomain=none - -# Loop, get parameters & remove any spaces from input -while getopts "g:s:p:l:r:b:a:k:d:h" opt; do - case $opt in - g) - # resource group - resourceGroup=$OPTARG - ;; - s) - # site name - siteName=$OPTARG - ;; - p) - # hosting plan name - hostingPlanName=$OPTARG - ;; - l) - # location - location=$OPTARG - ;; - r) - # repo url - repoURL=$OPTARG - ;; - b) - # repo branch - repoBranch=$OPTARG - ;; - a) - # enable auth - enableAuth=true - ;; - k) - # keyvault name - keyvaultName=$OPTARG - ;; - k) - # azure directory - azAdDomain=$OPTARG - ;; - :) - echo "Error: -${OPTARG} requires a value" - exit 1 - ;; - *) - usage - exit 1 - ;; - esac -done - -# validate parameters -if [[ $# -eq 0 || -z $resourceGroup || -z $siteName || -z $hostingPlanName || -z $location || -z $repoURL || -z $repoBranch ]]; then - error "Required parameters are missing" - usage - exit 1 -fi - -print "Generate random phrase to be apart of the group deployment name" -group_deployment_name=$(generateRandomPhrase) - -#create the azure resource group - print "Creating resource group $resourceGroup" - az group create --l $location \ - -n $resourceGroup \ - --tags "Project=EventGridViewerBlazor" \ - -o none - -# create the azure deployment -print "Creating the $group_deployment_name deployment group" -az deployment group create -g $resourceGroup \ - -n $group_deployment_name \ - -p siteName=$siteName hostingPlanName=$hostingPlanName repoURL=$repoURL branch=$repoBranch enableAuth=$enableAuth keyvaultName=$keyvaultName azAdDomain=$azAdDomain \ - -f arm/azuredeploy.json \ - -o none \ No newline at end of file diff --git a/infrastructure/helpers/utils.sh b/infrastructure/helpers/utils.sh deleted file mode 100644 index 7821d24..0000000 --- a/infrastructure/helpers/utils.sh +++ /dev/null @@ -1,15 +0,0 @@ -####################################################### -#- function used to print messages -####################################################### -function print() { - echo "$1...." -} - -############################################################### -#- function used to create a random phrase -############################################################### -function generateRandomPhrase() { - local phrase=$(openssl rand -hex 12) - - echo $phrase -} \ No newline at end of file diff --git a/src/Blazor.EventGridViewer.ServerApp/App.razor b/src/Blazor.EventGridViewer.ServerApp/App.razor index ece7489..bf6c773 100644 --- a/src/Blazor.EventGridViewer.ServerApp/App.razor +++ b/src/Blazor.EventGridViewer.ServerApp/App.razor @@ -1,13 +1,11 @@ ο»Ώ - - - - - - - -

Sorry, there's nothing at this address.

-
-
-
-
+ + + + + + +

Sorry, there's nothing at this address.

+
+
+
diff --git a/src/Blazor.EventGridViewer.ServerApp/Blazor.EventGridViewer.ServerApp.csproj b/src/Blazor.EventGridViewer.ServerApp/Blazor.EventGridViewer.ServerApp.csproj index 6d5d2b0..30d9527 100644 --- a/src/Blazor.EventGridViewer.ServerApp/Blazor.EventGridViewer.ServerApp.csproj +++ b/src/Blazor.EventGridViewer.ServerApp/Blazor.EventGridViewer.ServerApp.csproj @@ -8,8 +8,8 @@ - + diff --git a/src/Blazor.EventGridViewer.ServerApp/Controllers/EventGridController.cs b/src/Blazor.EventGridViewer.ServerApp/Controllers/EventGridController.cs index 25bda7e..867d6e9 100644 --- a/src/Blazor.EventGridViewer.ServerApp/Controllers/EventGridController.cs +++ b/src/Blazor.EventGridViewer.ServerApp/Controllers/EventGridController.cs @@ -1,31 +1,39 @@ ο»Ώusing System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; +using System.Linq; using System.Text; using System.Threading.Tasks; using Blazor.EventGridViewer.Core.Models; using Blazor.EventGridViewer.Services.Interfaces; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.EventGrid; using Microsoft.Azure.EventGrid.Models; +using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; namespace Blazor.EventGridViewer.ServerApp.Controllers { [Route("api/[controller]")] [ApiController] - [AllowAnonymous] public class EventGridController : ControllerBase { private readonly IEventGridService _eventGridService; private readonly IAdapter> _eventGridSchemaAdapter; + private readonly ILogger _logger; - public EventGridController(IEventGridService eventGridService, IAdapter> eventGridSchemaAdapter) + public EventGridController( + IEventGridService eventGridService, + IAdapter> eventGridSchemaAdapter, + ILogger logger) { - _eventGridService = eventGridService; - _eventGridSchemaAdapter = eventGridSchemaAdapter; + _eventGridService = eventGridService ?? throw new ArgumentNullException(nameof(eventGridService)); + _eventGridSchemaAdapter = eventGridSchemaAdapter ?? throw new ArgumentNullException(nameof(eventGridSchemaAdapter)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + _logger.LogDebug("EventGridController initialized"); } /// @@ -35,7 +43,8 @@ public EventGridController(IEventGridService eventGridService, IAdapter @@ -45,49 +54,146 @@ public string Get() [HttpPost] public async Task Post() { - IActionResult result = Ok(); + var stopwatch = Stopwatch.StartNew(); + var correlationId = Guid.NewGuid().ToString(); + + _logger.LogInformation("EventGrid POST request started. CorrelationId: {CorrelationId}", correlationId); try { + // Log request headers for debugging + _logger.LogDebug("Request headers: {@Headers}", Request.Headers.Select(h => new { Key = h.Key, Value = h.Value })); + + string json; // using StreamReader due to changes in .Net Core 3 serializer ie ValueKind using (var reader = new StreamReader(Request.Body, Encoding.UTF8)) { - var json = await reader.ReadToEndAsync(); - var eventGridEventModels = _eventGridSchemaAdapter.Convert(json); + json = await reader.ReadToEndAsync(); + } + + _logger.LogInformation("Received EventGrid payload. Length: {Length}, CorrelationId: {CorrelationId}", + json?.Length ?? 0, correlationId); + + if (string.IsNullOrEmpty(json)) + { + _logger.LogWarning("Received empty payload. CorrelationId: {CorrelationId}", correlationId); + return BadRequest("Empty payload received"); + } + + // Log the raw payload in development for debugging + if (Request.Headers.ContainsKey("aeg-event-type") && Request.Headers["aeg-event-type"] == "SubscriptionValidation") + { + _logger.LogInformation("Subscription validation request detected. CorrelationId: {CorrelationId}", correlationId); + } + else + { + _logger.LogDebug("Raw EventGrid payload: {Payload}", json); + } + + List eventGridEventModels; + try + { + eventGridEventModels = _eventGridSchemaAdapter.Convert(json); + _logger.LogInformation("Successfully parsed {EventCount} events. CorrelationId: {CorrelationId}", + eventGridEventModels?.Count ?? 0, correlationId); + } + catch (Exception adapterEx) + { + _logger.LogError(adapterEx, "Failed to convert EventGrid payload. CorrelationId: {CorrelationId}, Payload: {Payload}", + correlationId, json); + return StatusCode(StatusCodes.Status500InternalServerError, "Failed to parse EventGrid payload"); + } + + if (eventGridEventModels == null || eventGridEventModels.Count == 0) + { + _logger.LogWarning("No events found in payload. CorrelationId: {CorrelationId}", correlationId); + return Ok(); + } + + foreach (var model in eventGridEventModels) + { + _logger.LogInformation("Processing event: {EventType}, Subject: {Subject}, Id: {EventId}, CorrelationId: {CorrelationId}", + model.EventType, model.Subject, model.Id, correlationId); - foreach (EventGridEventModel model in eventGridEventModels) + // EventGrid validation message + if (model.EventType == EventTypes.EventGridSubscriptionValidationEvent) { - // EventGrid validation message - if (model.EventType == EventTypes.EventGridSubscriptionValidationEvent) + try { var eventData = ((JObject)(model.EventData)).ToObject(); var responseData = new SubscriptionValidationResponse() { ValidationResponse = eventData.ValidationCode }; + + _logger.LogInformation("Returning validation response. CorrelationId: {CorrelationId}", correlationId); return Ok(responseData); } - // handle all other events + catch (Exception validationEx) + { + _logger.LogError(validationEx, "Failed to process validation event. CorrelationId: {CorrelationId}", correlationId); + return StatusCode(StatusCodes.Status500InternalServerError, "Failed to process validation event"); + } + } + + // handle all other events + try + { this.HandleEvent(model); - return result; + _logger.LogInformation("Successfully handled event: {EventType}. CorrelationId: {CorrelationId}", + model.EventType, correlationId); + } + catch (Exception handleEx) + { + _logger.LogError(handleEx, "Failed to handle event: {EventType}, Subject: {Subject}. CorrelationId: {CorrelationId}", + model.EventType, model.Subject, correlationId); + return StatusCode(StatusCodes.Status500InternalServerError, "Failed to handle event"); } } + + stopwatch.Stop(); + _logger.LogInformation("EventGrid POST request completed successfully. Duration: {Duration}ms, CorrelationId: {CorrelationId}", + stopwatch.ElapsedMilliseconds, correlationId); + + return Ok(); } - catch (Exception) + catch (Exception ex) { - result = new StatusCodeResult(StatusCodes.Status500InternalServerError); - } + stopwatch.Stop(); + _logger.LogError(ex, "Unhandled exception in EventGrid POST endpoint. Duration: {Duration}ms, CorrelationId: {CorrelationId}", + stopwatch.ElapsedMilliseconds, correlationId); - return result; + return StatusCode(StatusCodes.Status500InternalServerError, "Internal server error occurred"); + } } /// /// Handle EventGrid Event /// - /// + /// EventGridEventModel private void HandleEvent(EventGridEventModel model) { - _eventGridService.RaiseEventReceivedEvent(model); + try + { + _logger.LogDebug("Handling event: {EventType}, Subject: {Subject}, Id: {EventId}", + model.EventType, model.Subject, model.Id); + + if (_eventGridService == null) + { + _logger.LogError("EventGrid service is null - cannot handle event"); + throw new InvalidOperationException("EventGrid service is not available"); + } + + var result = _eventGridService.RaiseEventReceivedEvent(model); + + _logger.LogDebug("Event handling result: {Result} for event: {EventId}", result, model.Id); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error handling event: {EventType}, Subject: {Subject}, Id: {EventId}", + model.EventType, model.Subject, model.Id); + throw; // Re-throw to be caught by the calling method + } } } } diff --git a/src/Blazor.EventGridViewer.ServerApp/Pages/Error.razor b/src/Blazor.EventGridViewer.ServerApp/Pages/Error.razor index 79929d7..ce8dcee 100644 --- a/src/Blazor.EventGridViewer.ServerApp/Pages/Error.razor +++ b/src/Blazor.EventGridViewer.ServerApp/Pages/Error.razor @@ -1,9 +1,17 @@ ο»Ώ@page "/error" -

Error.

An error occurred while processing your request.

+
+

Troubleshooting Tips

+
    +
  • Check the application logs for detailed error information
  • +
  • Verify the API endpoint: /api/eventgrid
  • +
  • Ensure all required configuration values are set
  • +
+
+

Development Mode

Swapping to Development environment will display more detailed information about the error that occurred. diff --git a/src/Blazor.EventGridViewer.ServerApp/Program.cs b/src/Blazor.EventGridViewer.ServerApp/Program.cs index 12e1229..80e5d2b 100644 --- a/src/Blazor.EventGridViewer.ServerApp/Program.cs +++ b/src/Blazor.EventGridViewer.ServerApp/Program.cs @@ -1,5 +1,7 @@ +using System; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; namespace Blazor.EventGridViewer.ServerApp { @@ -7,11 +9,27 @@ public class Program { public static void Main(string[] args) { - CreateHostBuilder(args).Build().Run(); + try + { + CreateHostBuilder(args).Build().Run(); + } + catch (Exception ex) + { + // Use Console.WriteLine since logger isn't configured yet + Console.WriteLine($"Application terminated unexpectedly: {ex}"); + throw; + } } public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) + .ConfigureLogging(logging => + { + logging.ClearProviders(); + logging.AddConsole(); + logging.AddDebug(); + // Application Insights will be added automatically in Startup.cs when configured + }) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); diff --git a/src/Blazor.EventGridViewer.ServerApp/Shared/LoginDisplay.razor b/src/Blazor.EventGridViewer.ServerApp/Shared/LoginDisplay.razor deleted file mode 100644 index 341205b..0000000 --- a/src/Blazor.EventGridViewer.ServerApp/Shared/LoginDisplay.razor +++ /dev/null @@ -1,14 +0,0 @@ -ο»Ώ - - Hello, @context.User.Identity.Name! - Log out - - - @{ - if (EnableAuth()) - { - Log in - } - } - - diff --git a/src/Blazor.EventGridViewer.ServerApp/Shared/LoginDisplay.razor.cs b/src/Blazor.EventGridViewer.ServerApp/Shared/LoginDisplay.razor.cs deleted file mode 100644 index ad5e3e6..0000000 --- a/src/Blazor.EventGridViewer.ServerApp/Shared/LoginDisplay.razor.cs +++ /dev/null @@ -1,25 +0,0 @@ -ο»Ώusing Microsoft.AspNetCore.Components; -using Microsoft.Extensions.Configuration; - -namespace Blazor.EventGridViewer.ServerApp.Shared -{ - public partial class LoginDisplay - { - [Inject] - private IConfiguration _configuration { get; set; } - - ///

- /// Check EnableAuth value - /// - /// boolean - private bool EnableAuth() - { - var enableAuth = _configuration["EnableAuth"]; - if (enableAuth == "true") - return true; - else - return false; - } - - } -} diff --git a/src/Blazor.EventGridViewer.ServerApp/Shared/MainLayout.razor b/src/Blazor.EventGridViewer.ServerApp/Shared/MainLayout.razor index 11d6096..328e06e 100644 --- a/src/Blazor.EventGridViewer.ServerApp/Shared/MainLayout.razor +++ b/src/Blazor.EventGridViewer.ServerApp/Shared/MainLayout.razor @@ -6,7 +6,6 @@
- GitHub
diff --git a/src/Blazor.EventGridViewer.ServerApp/Startup.cs b/src/Blazor.EventGridViewer.ServerApp/Startup.cs index b6128b6..61ca7f8 100644 --- a/src/Blazor.EventGridViewer.ServerApp/Startup.cs +++ b/src/Blazor.EventGridViewer.ServerApp/Startup.cs @@ -1,27 +1,28 @@ +using System.Collections.Generic; +using Blazor.EventGridViewer.Core.Models; +using Blazor.EventGridViewer.Services; +using Blazor.EventGridViewer.Services.Adapters; +using Blazor.EventGridViewer.Services.Interfaces; +using BlazorStrap; +using Microsoft.ApplicationInsights.Extensibility; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.Azure.EventGrid.Models; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using BlazorStrap; -using Blazor.EventGridViewer.Services.Interfaces; -using Blazor.EventGridViewer.Services; -using Microsoft.AspNetCore.Authentication.OpenIdConnect; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc.Authorization; -using Microsoft.AspNetCore.Authentication; -using Blazor.EventGridViewer.Services.Adapters; -using Microsoft.Azure.EventGrid.Models; -using Blazor.EventGridViewer.Core.Models; -using System.Collections.Generic; +using Microsoft.Extensions.Logging; namespace Blazor.EventGridViewer.ServerApp { public class Startup { - public Startup(IConfiguration configuration) + private readonly IWebHostEnvironment _env; + + public Startup(IConfiguration configuration, IWebHostEnvironment env) { Configuration = configuration; + _env = env; } public IConfiguration Configuration { get; } @@ -30,24 +31,31 @@ public Startup(IConfiguration configuration) // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { - // Check to see if AzureAD Auth is enabled - if (EnableAuth()) - { - services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme) - .AddOpenIdConnect(options => - { - Configuration.Bind("AzureAd", options); - }); + // Add Application Insights (only if connection string is provided) + var aiConnectionString = Configuration.GetConnectionString("ApplicationInsights") + ?? Configuration["ApplicationInsights:ConnectionString"]; - services.AddControllersWithViews(options => + if (!string.IsNullOrEmpty(aiConnectionString)) + { + services.AddApplicationInsightsTelemetry(options => { - var policy = new AuthorizationPolicyBuilder() - .RequireAuthenticatedUser() - .Build(); - options.Filters.Add(new AuthorizeFilter(policy)); + options.ConnectionString = aiConnectionString; }); + + // Configure sampling only in Production to reduce costs + if (_env.IsProduction()) + { + services.Configure(telemetryConfig => + { + telemetryConfig.DefaultTelemetrySink.TelemetryProcessorChainBuilder + .UseSampling(5.0) + .Build(); + }); + } } + services.AddControllersWithViews(); + services.AddRazorPages(); services.AddServerSideBlazor(); services.AddBlazorStrap(); @@ -56,35 +64,39 @@ public void ConfigureServices(IServiceCollection services) services.AddSingleton, EventGridEventModelAdapter>(); services.AddSingleton(); services.AddScoped(); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger logger) { + logger.LogInformation("Configuring application pipeline. Environment: {Environment}", env.EnvironmentName); + if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); + logger.LogInformation("Development environment detected - using developer exception page"); } else { - app.UseExceptionHandler("/Error"); + // Temporarily enable detailed errors for troubleshooting + if (Configuration.GetValue("DetailedErrors")) + { + app.UseDeveloperExceptionPage(); + logger.LogWarning("TEMPORARY: Detailed errors enabled in production for troubleshooting"); + } + else + { + app.UseExceptionHandler("/Error"); + } + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); app.UseHttpsRedirection(); + logger.LogInformation("Production environment detected - using standard error handling"); } - app.UseStaticFiles(); app.UseRouting(); - // Check to see if AzureAD Auth is enabled - if (EnableAuth()) - { - app.UseAuthentication(); - app.UseAuthorization(); - } - app.UseEndpoints(endpoints => { endpoints.MapBlazorHub(); @@ -93,19 +105,8 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); }); - } - /// - /// Check EnableAuth value - /// - /// boolean - private bool EnableAuth() - { - var enableAuth = Configuration["EnableAuth"]; - if (enableAuth == "true") - return true; - else - return false; + logger.LogInformation("Application pipeline configuration completed"); } } } diff --git a/src/Blazor.EventGridViewer.ServerApp/_Imports.razor b/src/Blazor.EventGridViewer.ServerApp/_Imports.razor index 7162b1e..8b27e0d 100644 --- a/src/Blazor.EventGridViewer.ServerApp/_Imports.razor +++ b/src/Blazor.EventGridViewer.ServerApp/_Imports.razor @@ -1,6 +1,4 @@ ο»Ώ@using System.Net.Http -@using Microsoft.AspNetCore.Authorization -@using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web diff --git a/src/Blazor.EventGridViewer.ServerApp/appsettings.Development.json b/src/Blazor.EventGridViewer.ServerApp/appsettings.Development.json index ed93089..55b3ae4 100644 --- a/src/Blazor.EventGridViewer.ServerApp/appsettings.Development.json +++ b/src/Blazor.EventGridViewer.ServerApp/appsettings.Development.json @@ -1,19 +1,11 @@ { - "AzureAd": { - "Instance": "", - "Domain": "", - "TenantId": "", - "ClientId": "", - "CallbackPath": "", - "SignedOutCallbackPath": "" - }, "DetailedErrors": true, "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" + "Microsoft.Hosting.Lifetime": "Information", + "Blazor.EventGridViewer": "Debug" } - }, - "EnableAuth": "false" + } } \ No newline at end of file diff --git a/src/Blazor.EventGridViewer.ServerApp/appsettings.Production.json b/src/Blazor.EventGridViewer.ServerApp/appsettings.Production.json new file mode 100644 index 0000000..135fe3c --- /dev/null +++ b/src/Blazor.EventGridViewer.ServerApp/appsettings.Production.json @@ -0,0 +1,14 @@ +{ + "ApplicationInsights": { + "ConnectionString": "" + }, + "Logging": { + "LogLevel": { + "Default": "Warning", + "Microsoft": "Error", + "Microsoft.Hosting.Lifetime": "Information", + "Blazor.EventGridViewer": "Information" + } + }, + "DetailedErrors": false +} \ No newline at end of file diff --git a/src/Blazor.EventGridViewer.ServerApp/appsettings.json b/src/Blazor.EventGridViewer.ServerApp/appsettings.json index d9d9a9b..7959bd5 100644 --- a/src/Blazor.EventGridViewer.ServerApp/appsettings.json +++ b/src/Blazor.EventGridViewer.ServerApp/appsettings.json @@ -6,5 +6,8 @@ "Microsoft.Hosting.Lifetime": "Information" } }, + "ApplicationInsights": { + "ConnectionString": "" + }, "AllowedHosts": "*" -} +} \ No newline at end of file diff --git a/src/Blazor.EventGridViewer.ServerApp/scripts/setup-log-security.sh b/src/Blazor.EventGridViewer.ServerApp/scripts/setup-log-security.sh new file mode 100755 index 0000000..dbb05b9 --- /dev/null +++ b/src/Blazor.EventGridViewer.ServerApp/scripts/setup-log-security.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +################################################################################################ +#- Purpose: Script to set secure permissions on log files and directories +#- This script should be run after deployment to ensure log files have restricted access +################################################################################################ + +set -eu + +# Create logs directory if it doesn't exist +LOG_DIR="logs" +if [ ! -d "$LOG_DIR" ]; then + echo "Creating logs directory..." + mkdir -p "$LOG_DIR" +fi + +# Set secure permissions on logs directory +echo "Setting secure permissions on logs directory..." +chmod 750 "$LOG_DIR" + +# Set secure permissions on existing log files +if [ -n "$(find "$LOG_DIR" -name "*.txt" 2>/dev/null)" ]; then + echo "Setting secure permissions on existing log files..." + find "$LOG_DIR" -name "*.txt" -exec chmod 640 {} \; +else + echo "No existing log files found." +fi + +# Set ownership if running as root (for production deployment) +if [ "$EUID" -eq 0 ]; then + echo "Setting ownership for production deployment..." + # Adjust these user/group names based on your deployment + chown -R www-data:www-data "$LOG_DIR" 2>/dev/null || echo "Warning: Could not set www-data ownership" +fi + +echo "Log file security setup completed!" +echo "Directory permissions: $(ls -ld "$LOG_DIR")" + +if [ -n "$(find "$LOG_DIR" -name "*.txt" 2>/dev/null)" ]; then + echo "File permissions:" + find "$LOG_DIR" -name "*.txt" -exec ls -l {} \; +fi \ No newline at end of file diff --git a/src/Blazor.EventGridViewer.Services/EventGridIdentifySchemaService.cs b/src/Blazor.EventGridViewer.Services/EventGridIdentifySchemaService.cs index 8d8f7d0..fe560b6 100644 --- a/src/Blazor.EventGridViewer.Services/EventGridIdentifySchemaService.cs +++ b/src/Blazor.EventGridViewer.Services/EventGridIdentifySchemaService.cs @@ -1,7 +1,8 @@ -ο»Ώusing Blazor.EventGridViewer.Core; +ο»Ώusing System; +using Blazor.EventGridViewer.Core; using Blazor.EventGridViewer.Services.Interfaces; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using System; namespace Blazor.EventGridViewer.Services { @@ -39,9 +40,10 @@ private bool IsCloudEvent(string json) var version = eventData["specversion"].Value(); if (!string.IsNullOrEmpty(version)) return true; } - catch (Exception e) + catch (JsonReaderException) { - Console.WriteLine(e); + // Expected when parsing EventGrid events (which come as arrays) + // This is not an error - just means it's not a CloudEvent } return false;