diff --git a/.azdo/pipelines/azure-dev.yml b/.azdo/pipelines/azure-dev.yml new file mode 100644 index 0000000..8ae0d8f --- /dev/null +++ b/.azdo/pipelines/azure-dev.yml @@ -0,0 +1,56 @@ +# Run when commits are pushed to mainline branch (main or master) +# Set this to the mainline branch you are using +trigger: + - main + - master + +# Azure Pipelines workflow to deploy to Azure using azd +# To configure required secrets and service connection for connecting to Azure, simply run `azd pipeline config --provider azdo` +# Task "Install azd" needs to install setup-azd extension for azdo - https://marketplace.visualstudio.com/items?itemName=ms-azuretools.azd +# See below for alternative task to install azd if you can't install above task in your organization + +pool: + vmImage: ubuntu-latest + +steps: + - task: setup-azd@0 + displayName: Install azd + + # If you can't install above task in your organization, you can comment it and uncomment below task to install azd + # - task: Bash@3 + # displayName: Install azd + # inputs: + # targetType: 'inline' + # script: | + # curl -fsSL https://aka.ms/install-azd.sh | bash + + # azd delegate auth to az to use service connection with AzureCLI@2 + - pwsh: | + azd config set auth.useAzCliAuth "true" + displayName: Configure AZD to Use AZ CLI Authentication. + + - task: AzureCLI@2 + displayName: Provision Infrastructure + inputs: + azureSubscription: azconnection + scriptType: bash + scriptLocation: inlineScript + inlineScript: | + azd provision --no-prompt + env: + AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID) + AZURE_ENV_NAME: $(AZURE_ENV_NAME) + AZURE_LOCATION: $(AZURE_LOCATION) + + - task: AzureCLI@2 + displayName: Deploy Application + inputs: + azureSubscription: azconnection + scriptType: bash + scriptLocation: inlineScript + inlineScript: | + azd deploy --no-prompt + env: + AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID) + AZURE_ENV_NAME: $(AZURE_ENV_NAME) + AZURE_LOCATION: $(AZURE_LOCATION) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..babcee3 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,44 @@ +{ + "name": "Azure Developer CLI", + "image": "mcr.microsoft.com/devcontainers/typescript-node:18-bookworm", + "features": { + "ghcr.io/azure/azure-dev/azd:latest": {}, + "ghcr.io/devcontainers/features/azure-cli:latest": { + "extensions": "apic-extension,staticwebapp" + }, + "ghcr.io/devcontainers/features/common-utils:latest": {}, + "ghcr.io/devcontainers/features/docker-in-docker:latest": {}, + "ghcr.io/devcontainers/features/github-cli:latest": {}, + "ghcr.io/devcontainers/features/node:1": {}, + "ghcr.io/devcontainers/features/powershell:latest": {} + }, + "overrideFeatureInstallOrder": [ + "ghcr.io/devcontainers/features/common-utils" + ], + "customizations": { + "vscode": { + "extensions": [ + "GitHub.vscode-github-actions", + "ms-azuretools.azure-dev", + "ms-azuretools.vscode-azurefunctions", + "ms-azuretools.vscode-azurestaticwebapps", + "ms-azuretools.vscode-bicep", + "ms-azuretools.vscode-docker", + "ms-vscode.vscode-node-azure-pack", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "ms-vscode.azurecli", + "ms-vscode.js-debug", + "ms-vscode.powershell" + ] + } + }, + "forwardPorts": [ + 4280, 5173 + ], + "onCreateCommand": "/bin/bash ./.devcontainer/on-create.sh > ~/on-create.log", + "remoteUser": "vscode", + "hostRequirements": { + "memory": "8gb" + } +} diff --git a/.devcontainer/on-create.sh b/.devcontainer/on-create.sh new file mode 100755 index 0000000..4c94c52 --- /dev/null +++ b/.devcontainer/on-create.sh @@ -0,0 +1,17 @@ +## Install additional apt packages +sudo apt-get update \ + && sudo apt-get install -y dos2unix libsecret-1-0 xdg-utils \ + && sudo apt-get clean -y && sudo rm -rf /var/lib/apt/lists/* + +## Configure git +git config --global pull.rebase false +git config --global core.autocrlf input + +## AZURE BICEP CLI ## +az bicep install + +## AZURE FUNCTIONS CORE TOOLS ## +npm i -g azure-functions-core-tools@4 --unsafe-perm true + +## AZURE STATIC WEB APPS CLI ## +npm install -g @azure/static-web-apps-cli diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..82afe66 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = crlf +charset = utf-8 +trim_trailing_whitespace = false +insert_final_newline = false + +[*.{bicep,yml,yaml}] +indent_size = 2 diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml new file mode 100644 index 0000000..0b3a157 --- /dev/null +++ b/.github/workflows/azure-dev.yml @@ -0,0 +1,104 @@ +name: AZD Deploy + +on: + workflow_dispatch: + push: + # Run when commits are pushed to mainline branch (main or master) + # Set this to the mainline branch you are using + branches: + - main + +# GitHub Actions workflow to deploy to Azure using azd +# To configure required secrets for connecting to Azure, simply run `azd pipeline config` + +# Set up permissions for deploying with secretless Azure federated credentials +# https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure?tabs=azure-portal%2Clinux#set-up-azure-login-with-openid-connect-authentication +permissions: + id-token: write + contents: read + +jobs: + build: + 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 }} + AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }} + AZD_INITIAL_ENVIRONMENT_CONFIG: ${{ secrets.AZD_INITIAL_ENVIRONMENT_CONFIG }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup node.js + uses: actions/setup-node@v4 + with: + node-version: 'lts/Iron' + + - name: Install azd + uses: Azure/setup-azd@v1.0.0 + + - name: Log in with Azure Dev CLI (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: Login to Azure CLI (Federated Credentials) + if: ${{ env.AZURE_CLIENT_ID != '' }} + uses: Azure/login@v2 + with: + client-id: ${{ env.AZURE_CLIENT_ID }} + subscription-id: ${{ env.AZURE_SUBSCRIPTION_ID }} + tenant-id: ${{ env.AZURE_TENANT_ID }} + + - name: Log in with Azure Dev CLI (Client Credentials) + if: ${{ env.AZURE_CREDENTIALS != '' }} + run: | + $info = $Env:AZURE_CREDENTIALS | ConvertFrom-Json -AsHashtable; + Write-Host "::add-mask::$($info.clientSecret)" + + azd auth login ` + --client-id "$($info.clientId)" ` + --client-secret "$($info.clientSecret)" ` + --tenant-id "$($info.tenantId)" + shell: pwsh + env: + AZURE_CREDENTIALS: ${{ env.AZURE_CREDENTIALS }} + + - name: Login to Azure CLI (Client Credentials) + if: ${{ env.AZURE_CREDENTIALS != '' }} + uses: Azure/login@v2 + with: + creds: ${{ env.AZURE_CREDENTIALS }} + + - name: Provision Infrastructure + if: ${{ env.AZURE_CLIENT_ID != '' || env.AZURE_CREDENTIALS != '' }} + run: azd provision --no-prompt + env: + AZURE_ENV_NAME: ${{ env.AZURE_ENV_NAME }} + AZURE_LOCATION: ${{ env.AZURE_LOCATION }} + AZURE_SUBSCRIPTION_ID: ${{ env.AZURE_SUBSCRIPTION_ID }} + AZD_INITIAL_ENVIRONMENT_CONFIG: ${{ env.AZD_INITIAL_ENVIRONMENT_CONFIG }} + + - name: Build artifact + shell: bash + run: | + npm install + npm run build + + - name: Deploy Application + if: ${{ env.AZURE_CLIENT_ID != '' || env.AZURE_CREDENTIALS != '' }} + run: azd deploy --no-prompt + env: + AZURE_ENV_NAME: ${{ env.AZURE_ENV_NAME }} + AZURE_LOCATION: ${{ env.AZURE_LOCATION }} + AZURE_SUBSCRIPTION_ID: ${{ env.AZURE_SUBSCRIPTION_ID }} + AZD_INITIAL_ENVIRONMENT_CONFIG: ${{ env.AZD_INITIAL_ENVIRONMENT_CONFIG }} diff --git a/.gitignore b/.gitignore index 1000108..8025e8f 100644 --- a/.gitignore +++ b/.gitignore @@ -350,3 +350,4 @@ dist-ssr *.njsproj *.sln *.sw? +.azure diff --git a/README.md b/README.md index c896a2e..d3ac04a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ [![Open Source Love](https://firstcontributions.github.io/open-source-badges/badges/open-source-v1/open-source.svg)](https://github.com/firstcontributions/open-source-badges) - # Self-host Your Azure API Center Portal ## Overview @@ -9,6 +8,9 @@ - [What is the Azure API Center Portal?](./#azure-api-center-portal) - [Prerequisites](./#prerequisites) - [Quick Start](./#quick-start) + - [Automated deployment using `azd`](./#automated-deployment-using-azd) + - [Running the portal locally](./#running-the-portal-locally) + - [Manual deployment to Azure Static Web Apps](./#manual-deployment-to-azure-static-web-apps) - [Contributing](./#contributing) - [Bugs & Issues & Feedback](./#bugs--issues--feedback) - [Code of Conduct](./#code-of-conduct) @@ -25,6 +27,7 @@ **API Center portal** is a website that empowers developers and stakeholders to seamlessly discover and engage with APIs. Our reference implementation of the API Center portal enables API platform teams to provide a web-based API discovery and consumption experience to API consumers. The API Center portal reference implementation provides: + - A framework for publishing and maintaining a customer-managed API portal. - A portal platform that customers can modify or extend to meet their needs. - Flexibility to host on different infrastructures, including deployment to Azure Static Web Apps or Azure App Service. @@ -32,66 +35,134 @@ The API Center portal reference implementation provides: ## Prerequisites Before you begin, ensure you have met the following requirements: -1. :white_check_mark: You have installed the latest version of [Node.js and npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). -2. :white_check_mark: [Vite package](https://www.npmjs.com/package/vite). -3. :white_check_mark: [Configured app registration in your Microsoft Entra ID tenant](https://learn.microsoft.com/azure/api-center/enable-api-center-portal#create-microsoft-entra-app-registration) with the right API permission scope and Redirect URI. -4. :white_check_mark: Portal sign-in enabled with the [right role assignment](https://learn.microsoft.com/azure/api-center/enable-api-center-portal#enable-sign-in-to-portal-by-microsoft-entra-users-and-groups) +1. :white_check_mark: You have installed the latest version of [Node.js and npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). +1. :white_check_mark: [Vite package](https://www.npmjs.com/package/vite). ## Quick Start -### Getting Started +You have two options to deploy this self-hosted API Center Portal: + +- **Automated deployment** – Use the Azure developer CLI (`azd`) for one-step deployment of the portal app. This option is recommended for a streamlined deployment process. +- **Manual deployment** – Follow step-by-step guidance to deploy the Azure Functions app and configure the event subscription. This option is recommended if you prefer to deploy and manage the resources manually. + +### Automated deployment using `azd` + +> **NOTE**: You will need the additional prerequisites for this option +> +> - :white_check_mark: Azure Developer CLI (`azd`) +> - :white_check_mark: Azure CLI +> - :white_check_mark: GitHub CLI + +1. Log in with the following command. Then, you will be able to use the `azd` cli to quickly provision and deploy the application. + + ```bash + # Authenticate with Azure Developer CLI + azd auth login + + # Authenticate with Azure CLI + az login + ``` + +1. Run `azd up` to provision all the resources to Azure and deploy the code to those resources. + + ```bash + azd up + ``` + + Enter an environment name and select your desired `subscription` and `location`. Then, you will be asked to enter a few more values: + + 1. Choose whether to use an existing API Center instance or not (`apiCenterExisted`). + 1. Pass the values for `apiCenterName`, `apiCenterRegion` and `apiCenterResourceGroupName`, if you choose to use the existing API center resource (`apiCenterExisted` value to `true`). + 1. Leave them blank, if you choose to create a new API center resource (`apiCenterExisted` value to `false`). + 1. Pass `staticAppLocation` value for the Azure Static Web Apps instance. Wait a moment for the resource deployment to complete. + + > There are two scenarios: + > + > 1. Portal with new API Center – You need to give `False` to `apiCenterExisted` and leave `apiCenterName`, `apiCenterRegion` and `apiCenterResourceGroupName` blank. + > 1. Portal with existing API Center – You need to give `True` to `apiCenterExisted` and pass values to `apiCenterName`, `apiCenterRegion` and `apiCenterResourceGroupName`. + +1. If you want to integrate the CI/CD pipeline with GitHub Actions, you can use the following command to create a GitHub repository and push the code to the repository. First of all, log in to GitHub. + + ```bash + # Authenticate with GitHub CLI + gh auth login + ``` + +1. Run the following commands to update your GitHub repository variables. + + ```bash + # Bash + AZURE_CLIENT_ID=$(./infra/scripts/get-azdvariable.sh --key AZURE_CLIENT_ID) + azd pipeline config --principal-id $AZURE_CLIENT_ID + + # PowerShell + $AZURE_CLIENT_ID = $(./infra/scripts/Get-AzdVariable.ps1 -Key AZURE_CLIENT_ID) + azd pipeline config --principal-id $AZURE_CLIENT_ID + ``` + +1. Now, you're good to go! Push the code to the GitHub repository or manually run the GitHub Actions workflow to get your portal deployed. + +### Running the portal locally + +> **NOTE**: You will need the additional prerequisites for this option +> +> - :white_check_mark: [Configured app registration in your Microsoft Entra ID tenant](https://learn.microsoft.com/azure/api-center/enable-api-center-portal#create-microsoft-entra-app-registration) with the right API permission scope and Redirect URI. +> - :white_check_mark: Portal sign-in enabled with the [right role assignment](https://learn.microsoft.com/azure/api-center/enable-api-center-portal#enable-sign-in-to-portal-by-microsoft-entra-users-and-groups) + Follow these steps to get your development environment set up: 1. Clone the repository -```bash -git clone https://github.com/Azure/APICenter-Portal-Starter.git -``` + ```bash + git clone https://github.com/Azure/APICenter-Portal-Starter.git + ``` + +1. Switch to main branch: + ```bash + git checkout main + ``` -2. Switch to main branch: +1. Copy or rename the `public/config.example` file to the `public/config.json`. +1. Configure the `public/config.json` file to point to your Azure API Center service. Here’s an example configuration: -```bash -git checkout main -``` + ```JSON + { + "dataApiHostName": ".data..azure-apicenter.ms/workspaces/default", + "title": "API portal", + "authentication": { + "clientId": "", + "tenantId": "", + "scopes": ["https://azure-apicenter.net/user_impersonation"], + "authority": "https://login.microsoftonline.com/" + } + } + ``` -3. Configure the `public/config.json` file to point to your Azure API Center service. Here’s an example configuration: +1. Install the required packages. -```JSON -{ - "dataApiHostName": ".data..azure-apicenter.ms/workspaces/default", - "title": "API portal", - "authentication": { - "clientId": "", - "tenantId": "", - "scopes": ["https://azure-apicenter.net/user_impersonation"], - "authority": "https://login.microsoftonline.com/" - } -} -``` -4. Install the required packages. + ```bash + npm install + ``` -```bash -npm install -``` +1. Start the development server - This command will start the portal in development mode running locally: -5. Start the development server - This command will start the portal in development mode running locally: + ```bash + npm start + ``` -```bash -npm start -``` +### Manual deployment to Azure Static Web Apps -### Deploy to Azure Static Web Apps -[Azure Static Web Apps](https://learn.microsoft.com/en-us/azure/static-web-apps/overview) is a service that automatically builds and deploys full stack web apps to Azure from a code repository. This tutorial uses GitHub Actions to deploy to Azure Static Web Apps. +[Azure Static Web Apps](https://learn.microsoft.com/azure/static-web-apps/overview) is a service that automatically builds and deploys full stack web apps to Azure from a code repository. This tutorial uses GitHub Actions to deploy to Azure Static Web Apps. 1. Create a new **Static Web App**. -2. Select **GitHub** as the **Source**. -3. Select the **GitHub organization, repository, and branch** containing the API Center portal. Note: You must fork the API Center portal repository to your own personal account or organization and select this repository. -4. Select **React** as the **Build Presets**. -5. Enter **/** as the **App location**. -6. Enter **dist** as the **Output location**. -7. Click **Create**. A GitHub workflow file will be committed to the repository selected in Step #3, and deployment to your Static Web App with GitHub Actions will begin. It may take up to five minutes to see your changes published. +1. Select **GitHub** as the **Source**. +1. Select the **GitHub organization, repository, and branch** containing the API Center portal. Note: You must fork the API Center portal repository to your own personal account or organization and select this repository. +1. Select **React** as the **Build Presets**. +1. Enter **/** as the **App location**. +1. Enter **dist** as the **Output location**. +1. Click **Create**. A GitHub workflow file will be committed to the repository selected in Step #3, and deployment to your Static Web App with GitHub Actions will begin. It may take up to five minutes to see your changes published. To view your API Center portal running on Static Web Apps, click **View app in browser** from the **Overview** tab in the Static Web App resource you created in Azure portal. diff --git a/azure.yaml b/azure.yaml new file mode 100644 index 0000000..9c0c79a --- /dev/null +++ b/azure.yaml @@ -0,0 +1,79 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +# This is an example starter azure.yaml file containing several example services in comments below. +# Make changes as needed to describe your application setup. +# To learn more about the azure.yaml file, visit https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/azd-schema + +# Name of the application. +name: apicenter-portal-starter + +workflows: + up: + steps: + - azd: provision + - azd: package --all + - azd: deploy --all + +services: + staticapp-portal: + language: ts + project: ./ + host: staticwebapp + dist: dist + +hooks: + preup: + posix: + shell: sh + run: infra/hooks/preup.sh + continueOnError: false + interactive: true + windows: + shell: pwsh + run: infra/hooks/preup.ps1 + continueOnError: false + interactive: true + preprovision: + posix: + shell: sh + run: infra/hooks/preprovision.sh + continueOnError: false + interactive: true + windows: + shell: pwsh + run: infra/hooks/preprovision.ps1 + continueOnError: false + interactive: true + postprovision: + posix: + shell: sh + run: infra/hooks/postprovision.sh + continueOnError: false + interactive: true + windows: + shell: pwsh + run: infra/hooks/postprovision.ps1 + continueOnError: false + interactive: true + predeploy: + posix: + shell: sh + run: infra/hooks/predeploy.sh + continueOnError: false + interactive: true + windows: + shell: pwsh + run: infra/hooks/predeploy.ps1 + continueOnError: false + interactive: true + predown: + posix: + shell: sh + continueOnError: false + interactive: true + run: infra/hooks/predown.sh + windows: + shell: pwsh + continueOnError: false + interactive: true + run: infra/hooks/predown.ps1 diff --git a/infra/abbreviations.json b/infra/abbreviations.json new file mode 100644 index 0000000..292beef --- /dev/null +++ b/infra/abbreviations.json @@ -0,0 +1,136 @@ +{ + "analysisServicesServers": "as", + "apiManagementService": "apim-", + "appConfigurationStores": "appcs-", + "appManagedEnvironments": "cae-", + "appContainerApps": "ca-", + "authorizationPolicyDefinitions": "policy-", + "automationAutomationAccounts": "aa-", + "blueprintBlueprints": "bp-", + "blueprintBlueprintsArtifacts": "bpa-", + "cacheRedis": "redis-", + "cdnProfiles": "cdnp-", + "cdnProfilesEndpoints": "cdne-", + "cognitiveServicesAccounts": "cog-", + "cognitiveServicesFormRecognizer": "cog-fr-", + "cognitiveServicesTextAnalytics": "cog-ta-", + "computeAvailabilitySets": "avail-", + "computeCloudServices": "cld-", + "computeDiskEncryptionSets": "des", + "computeDisks": "disk", + "computeDisksOs": "osdisk", + "computeGalleries": "gal", + "computeSnapshots": "snap-", + "computeVirtualMachines": "vm", + "computeVirtualMachineScaleSets": "vmss-", + "containerInstanceContainerGroups": "ci", + "containerRegistryRegistries": "cr", + "containerServiceManagedClusters": "aks-", + "databricksWorkspaces": "dbw-", + "dataFactoryFactories": "adf-", + "dataLakeAnalyticsAccounts": "dla", + "dataLakeStoreAccounts": "dls", + "dataMigrationServices": "dms-", + "dBforMySQLServers": "mysql-", + "dBforPostgreSQLServers": "psql-", + "devicesIotHubs": "iot-", + "devicesProvisioningServices": "provs-", + "devicesProvisioningServicesCertificates": "pcert-", + "documentDBDatabaseAccounts": "cosmos-", + "eventGridDomains": "evgd-", + "eventGridDomainsTopics": "evgt-", + "eventGridEventSubscriptions": "evgs-", + "eventHubNamespaces": "evhns-", + "eventHubNamespacesEventHubs": "evh-", + "hdInsightClustersHadoop": "hadoop-", + "hdInsightClustersHbase": "hbase-", + "hdInsightClustersKafka": "kafka-", + "hdInsightClustersMl": "mls-", + "hdInsightClustersSpark": "spark-", + "hdInsightClustersStorm": "storm-", + "hybridComputeMachines": "arcs-", + "insightsActionGroups": "ag-", + "insightsComponents": "appi-", + "keyVaultVaults": "kv-", + "kubernetesConnectedClusters": "arck", + "kustoClusters": "dec", + "kustoClustersDatabases": "dedb", + "loadTesting": "lt-", + "logicIntegrationAccounts": "ia-", + "logicWorkflows": "logic-", + "machineLearningServicesWorkspaces": "mlw-", + "managedIdentityUserAssignedIdentities": "id-", + "managementManagementGroups": "mg-", + "migrateAssessmentProjects": "migr-", + "networkApplicationGateways": "agw-", + "networkApplicationSecurityGroups": "asg-", + "networkAzureFirewalls": "afw-", + "networkBastionHosts": "bas-", + "networkConnections": "con-", + "networkDnsZones": "dnsz-", + "networkExpressRouteCircuits": "erc-", + "networkFirewallPolicies": "afwp-", + "networkFirewallPoliciesWebApplication": "waf", + "networkFirewallPoliciesRuleGroups": "wafrg", + "networkFrontDoors": "fd-", + "networkFrontdoorWebApplicationFirewallPolicies": "fdfp-", + "networkLoadBalancersExternal": "lbe-", + "networkLoadBalancersInternal": "lbi-", + "networkLoadBalancersInboundNatRules": "rule-", + "networkLocalNetworkGateways": "lgw-", + "networkNatGateways": "ng-", + "networkNetworkInterfaces": "nic-", + "networkNetworkSecurityGroups": "nsg-", + "networkNetworkSecurityGroupsSecurityRules": "nsgsr-", + "networkNetworkWatchers": "nw-", + "networkPrivateDnsZones": "pdnsz-", + "networkPrivateLinkServices": "pl-", + "networkPublicIPAddresses": "pip-", + "networkPublicIPPrefixes": "ippre-", + "networkRouteFilters": "rf-", + "networkRouteTables": "rt-", + "networkRouteTablesRoutes": "udr-", + "networkTrafficManagerProfiles": "traf-", + "networkVirtualNetworkGateways": "vgw-", + "networkVirtualNetworks": "vnet-", + "networkVirtualNetworksSubnets": "snet-", + "networkVirtualNetworksVirtualNetworkPeerings": "peer-", + "networkVirtualWans": "vwan-", + "networkVpnGateways": "vpng-", + "networkVpnGatewaysVpnConnections": "vcn-", + "networkVpnGatewaysVpnSites": "vst-", + "notificationHubsNamespaces": "ntfns-", + "notificationHubsNamespacesNotificationHubs": "ntf-", + "operationalInsightsWorkspaces": "log-", + "portalDashboards": "dash-", + "powerBIDedicatedCapacities": "pbi-", + "purviewAccounts": "pview-", + "recoveryServicesVaults": "rsv-", + "resourcesResourceGroups": "rg-", + "searchSearchServices": "srch-", + "serviceBusNamespaces": "sb-", + "serviceBusNamespacesQueues": "sbq-", + "serviceBusNamespacesTopics": "sbt-", + "serviceEndPointPolicies": "se-", + "serviceFabricClusters": "sf-", + "signalRServiceSignalR": "sigr", + "sqlManagedInstances": "sqlmi-", + "sqlServers": "sql-", + "sqlServersDataWarehouse": "sqldw-", + "sqlServersDatabases": "sqldb-", + "sqlServersDatabasesStretch": "sqlstrdb-", + "storageStorageAccounts": "st", + "storageStorageAccountsVm": "stvm", + "storSimpleManagers": "ssimp", + "streamAnalyticsCluster": "asa-", + "synapseWorkspaces": "syn", + "synapseWorkspacesAnalyticsWorkspaces": "synw", + "synapseWorkspacesSqlPoolsDedicated": "syndp", + "synapseWorkspacesSqlPoolsSpark": "synsp", + "timeSeriesInsightsEnvironments": "tsi-", + "webServerFarms": "plan-", + "webSitesAppService": "app-", + "webSitesAppServiceEnvironment": "ase-", + "webSitesFunctions": "func-", + "webStaticSites": "stapp-" +} \ No newline at end of file diff --git a/infra/core/gateway/apicenter.bicep b/infra/core/gateway/apicenter.bicep new file mode 100644 index 0000000..5363d73 --- /dev/null +++ b/infra/core/gateway/apicenter.bicep @@ -0,0 +1,24 @@ +metadata description = 'Creates an Azure API Center instance.' +param name string +param location string +param tags object + +param skuName string = 'Free' + +// Create an API center service +resource apiCenter 'Microsoft.ApiCenter/services@2024-03-15-preview' = { + name: name + location: location + tags: tags + sku: { + name: skuName + } + identity: { + type: 'SystemAssigned' + } +} + +output name string = apiCenter.name +output id string = apiCenter.id +output location string = apiCenter.location +output identityPrincipalId string = apiCenter.identity.principalId diff --git a/infra/core/host/staticwebapp.bicep b/infra/core/host/staticwebapp.bicep new file mode 100644 index 0000000..6506810 --- /dev/null +++ b/infra/core/host/staticwebapp.bicep @@ -0,0 +1,23 @@ +metadata description = 'Creates an Azure Static Web Apps instance.' +param name string +param location string = resourceGroup().location +param tags object = {} + +param sku object = { + name: 'Free' + tier: 'Free' +} + +resource web 'Microsoft.Web/staticSites@2022-03-01' = { + name: name + location: location + tags: tags + sku: sku + properties: { + provider: 'Custom' + } +} + +output name string = web.name +output uri string = 'https://${web.properties.defaultHostname}' +output location string = toLower(replace(web.location, ' ', '')) diff --git a/infra/core/monitor/applicationinsights-dashboard.bicep b/infra/core/monitor/applicationinsights-dashboard.bicep new file mode 100644 index 0000000..d082e66 --- /dev/null +++ b/infra/core/monitor/applicationinsights-dashboard.bicep @@ -0,0 +1,1236 @@ +metadata description = 'Creates a dashboard for an Application Insights instance.' +param name string +param applicationInsightsName string +param location string = resourceGroup().location +param tags object = {} + +// 2020-09-01-preview because that is the latest valid version +resource applicationInsightsDashboard 'Microsoft.Portal/dashboards@2020-09-01-preview' = { + name: name + location: location + tags: tags + properties: { + lenses: [ + { + order: 0 + parts: [ + { + position: { + x: 0 + y: 0 + colSpan: 2 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'id' + value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + { + name: 'Version' + value: '1.0' + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/AspNetOverviewPinnedPart' + asset: { + idInputName: 'id' + type: 'ApplicationInsights' + } + defaultMenuItemId: 'overview' + } + } + { + position: { + x: 2 + y: 0 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ComponentId' + value: { + Name: applicationInsights.name + SubscriptionId: subscription().subscriptionId + ResourceGroup: resourceGroup().name + } + } + { + name: 'Version' + value: '1.0' + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/ProactiveDetectionAsyncPart' + asset: { + idInputName: 'ComponentId' + type: 'ApplicationInsights' + } + defaultMenuItemId: 'ProactiveDetection' + } + } + { + position: { + x: 3 + y: 0 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ComponentId' + value: { + Name: applicationInsights.name + SubscriptionId: subscription().subscriptionId + ResourceGroup: resourceGroup().name + } + } + { + name: 'ResourceId' + value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/QuickPulseButtonSmallPart' + asset: { + idInputName: 'ComponentId' + type: 'ApplicationInsights' + } + } + } + { + position: { + x: 4 + y: 0 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ComponentId' + value: { + Name: applicationInsights.name + SubscriptionId: subscription().subscriptionId + ResourceGroup: resourceGroup().name + } + } + { + name: 'TimeContext' + value: { + durationMs: 86400000 + endTime: null + createdTime: '2018-05-04T01:20:33.345Z' + isInitialTime: true + grain: 1 + useDashboardTimeRange: false + } + } + { + name: 'Version' + value: '1.0' + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/AvailabilityNavButtonPart' + asset: { + idInputName: 'ComponentId' + type: 'ApplicationInsights' + } + } + } + { + position: { + x: 5 + y: 0 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ComponentId' + value: { + Name: applicationInsights.name + SubscriptionId: subscription().subscriptionId + ResourceGroup: resourceGroup().name + } + } + { + name: 'TimeContext' + value: { + durationMs: 86400000 + endTime: null + createdTime: '2018-05-08T18:47:35.237Z' + isInitialTime: true + grain: 1 + useDashboardTimeRange: false + } + } + { + name: 'ConfigurationId' + value: '78ce933e-e864-4b05-a27b-71fd55a6afad' + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/AppMapButtonPart' + asset: { + idInputName: 'ComponentId' + type: 'ApplicationInsights' + } + } + } + { + position: { + x: 0 + y: 1 + colSpan: 3 + rowSpan: 1 + } + metadata: { + inputs: [] + type: 'Extension/HubsExtension/PartType/MarkdownPart' + settings: { + content: { + settings: { + content: '# Usage' + title: '' + subtitle: '' + } + } + } + } + } + { + position: { + x: 3 + y: 1 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ComponentId' + value: { + Name: applicationInsights.name + SubscriptionId: subscription().subscriptionId + ResourceGroup: resourceGroup().name + } + } + { + name: 'TimeContext' + value: { + durationMs: 86400000 + endTime: null + createdTime: '2018-05-04T01:22:35.782Z' + isInitialTime: true + grain: 1 + useDashboardTimeRange: false + } + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/UsageUsersOverviewPart' + asset: { + idInputName: 'ComponentId' + type: 'ApplicationInsights' + } + } + } + { + position: { + x: 4 + y: 1 + colSpan: 3 + rowSpan: 1 + } + metadata: { + inputs: [] + type: 'Extension/HubsExtension/PartType/MarkdownPart' + settings: { + content: { + settings: { + content: '# Reliability' + title: '' + subtitle: '' + } + } + } + } + } + { + position: { + x: 7 + y: 1 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ResourceId' + value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + { + name: 'DataModel' + value: { + version: '1.0.0' + timeContext: { + durationMs: 86400000 + createdTime: '2018-05-04T23:42:40.072Z' + isInitialTime: false + grain: 1 + useDashboardTimeRange: false + } + } + isOptional: true + } + { + name: 'ConfigurationId' + value: '8a02f7bf-ac0f-40e1-afe9-f0e72cfee77f' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/CuratedBladeFailuresPinnedPart' + isAdapter: true + asset: { + idInputName: 'ResourceId' + type: 'ApplicationInsights' + } + defaultMenuItemId: 'failures' + } + } + { + position: { + x: 8 + y: 1 + colSpan: 3 + rowSpan: 1 + } + metadata: { + inputs: [] + type: 'Extension/HubsExtension/PartType/MarkdownPart' + settings: { + content: { + settings: { + content: '# Responsiveness\r\n' + title: '' + subtitle: '' + } + } + } + } + } + { + position: { + x: 11 + y: 1 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ResourceId' + value: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + { + name: 'DataModel' + value: { + version: '1.0.0' + timeContext: { + durationMs: 86400000 + createdTime: '2018-05-04T23:43:37.804Z' + isInitialTime: false + grain: 1 + useDashboardTimeRange: false + } + } + isOptional: true + } + { + name: 'ConfigurationId' + value: '2a8ede4f-2bee-4b9c-aed9-2db0e8a01865' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/CuratedBladePerformancePinnedPart' + isAdapter: true + asset: { + idInputName: 'ResourceId' + type: 'ApplicationInsights' + } + defaultMenuItemId: 'performance' + } + } + { + position: { + x: 12 + y: 1 + colSpan: 3 + rowSpan: 1 + } + metadata: { + inputs: [] + type: 'Extension/HubsExtension/PartType/MarkdownPart' + settings: { + content: { + settings: { + content: '# Browser' + title: '' + subtitle: '' + } + } + } + } + } + { + position: { + x: 15 + y: 1 + colSpan: 1 + rowSpan: 1 + } + metadata: { + inputs: [ + { + name: 'ComponentId' + value: { + Name: applicationInsights.name + SubscriptionId: subscription().subscriptionId + ResourceGroup: resourceGroup().name + } + } + { + name: 'MetricsExplorerJsonDefinitionId' + value: 'BrowserPerformanceTimelineMetrics' + } + { + name: 'TimeContext' + value: { + durationMs: 86400000 + createdTime: '2018-05-08T12:16:27.534Z' + isInitialTime: false + grain: 1 + useDashboardTimeRange: false + } + } + { + name: 'CurrentFilter' + value: { + eventTypes: [ + 4 + 1 + 3 + 5 + 2 + 6 + 13 + ] + typeFacets: {} + isPermissive: false + } + } + { + name: 'id' + value: { + Name: applicationInsights.name + SubscriptionId: subscription().subscriptionId + ResourceGroup: resourceGroup().name + } + } + { + name: 'Version' + value: '1.0' + } + ] + #disable-next-line BCP036 + type: 'Extension/AppInsightsExtension/PartType/MetricsExplorerBladePinnedPart' + asset: { + idInputName: 'ComponentId' + type: 'ApplicationInsights' + } + defaultMenuItemId: 'browser' + } + } + { + position: { + x: 0 + y: 2 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'sessions/count' + aggregationType: 5 + namespace: 'microsoft.insights/components/kusto' + metricVisualization: { + displayName: 'Sessions' + color: '#47BDF5' + } + } + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'users/count' + aggregationType: 5 + namespace: 'microsoft.insights/components/kusto' + metricVisualization: { + displayName: 'Users' + color: '#7E58FF' + } + } + ] + title: 'Unique sessions and users' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + openBladeOnClick: { + openBlade: true + destinationBlade: { + extensionName: 'HubsExtension' + bladeName: 'ResourceMenuBlade' + parameters: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + menuid: 'segmentationUsers' + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 4 + y: 2 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'requests/failed' + aggregationType: 7 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Failed requests' + color: '#EC008C' + } + } + ] + title: 'Failed requests' + visualization: { + chartType: 3 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + openBladeOnClick: { + openBlade: true + destinationBlade: { + extensionName: 'HubsExtension' + bladeName: 'ResourceMenuBlade' + parameters: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + menuid: 'failures' + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 8 + y: 2 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'requests/duration' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Server response time' + color: '#00BCF2' + } + } + ] + title: 'Server response time' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + openBladeOnClick: { + openBlade: true + destinationBlade: { + extensionName: 'HubsExtension' + bladeName: 'ResourceMenuBlade' + parameters: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + menuid: 'performance' + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 12 + y: 2 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'browserTimings/networkDuration' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Page load network connect time' + color: '#7E58FF' + } + } + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'browserTimings/processingDuration' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Client processing time' + color: '#44F1C8' + } + } + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'browserTimings/sendDuration' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Send request time' + color: '#EB9371' + } + } + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'browserTimings/receiveDuration' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Receiving response time' + color: '#0672F1' + } + } + ] + title: 'Average page load time breakdown' + visualization: { + chartType: 3 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 0 + y: 5 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'availabilityResults/availabilityPercentage' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Availability' + color: '#47BDF5' + } + } + ] + title: 'Average availability' + visualization: { + chartType: 3 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + openBladeOnClick: { + openBlade: true + destinationBlade: { + extensionName: 'HubsExtension' + bladeName: 'ResourceMenuBlade' + parameters: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + menuid: 'availability' + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 4 + y: 5 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'exceptions/server' + aggregationType: 7 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Server exceptions' + color: '#47BDF5' + } + } + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'dependencies/failed' + aggregationType: 7 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Dependency failures' + color: '#7E58FF' + } + } + ] + title: 'Server exceptions and Dependency failures' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 8 + y: 5 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'performanceCounters/processorCpuPercentage' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Processor time' + color: '#47BDF5' + } + } + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'performanceCounters/processCpuPercentage' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Process CPU' + color: '#7E58FF' + } + } + ] + title: 'Average processor and process CPU utilization' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 12 + y: 5 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'exceptions/browser' + aggregationType: 7 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Browser exceptions' + color: '#47BDF5' + } + } + ] + title: 'Browser exceptions' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 0 + y: 8 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'availabilityResults/count' + aggregationType: 7 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Availability test results count' + color: '#47BDF5' + } + } + ] + title: 'Availability test results count' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 4 + y: 8 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'performanceCounters/processIOBytesPerSecond' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Process IO rate' + color: '#47BDF5' + } + } + ] + title: 'Average process I/O rate' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + { + position: { + x: 8 + y: 8 + colSpan: 4 + rowSpan: 3 + } + metadata: { + inputs: [ + { + name: 'options' + value: { + chart: { + metrics: [ + { + resourceMetadata: { + id: '/subscriptions/${subscription().subscriptionId}/resourceGroups/${resourceGroup().name}/providers/Microsoft.Insights/components/${applicationInsights.name}' + } + name: 'performanceCounters/memoryAvailableBytes' + aggregationType: 4 + namespace: 'microsoft.insights/components' + metricVisualization: { + displayName: 'Available memory' + color: '#47BDF5' + } + } + ] + title: 'Average available memory' + visualization: { + chartType: 2 + legendVisualization: { + isVisible: true + position: 2 + hideSubtitle: false + } + axisVisualization: { + x: { + isVisible: true + axisType: 2 + } + y: { + isVisible: true + axisType: 1 + } + } + } + } + } + } + { + name: 'sharedTimeRange' + isOptional: true + } + ] + #disable-next-line BCP036 + type: 'Extension/HubsExtension/PartType/MonitorChartPart' + settings: {} + } + } + ] + } + ] + } +} + +resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = { + name: applicationInsightsName +} diff --git a/infra/core/monitor/applicationinsights.bicep b/infra/core/monitor/applicationinsights.bicep new file mode 100644 index 0000000..850e9fe --- /dev/null +++ b/infra/core/monitor/applicationinsights.bicep @@ -0,0 +1,31 @@ +metadata description = 'Creates an Application Insights instance based on an existing Log Analytics workspace.' +param name string +param dashboardName string = '' +param location string = resourceGroup().location +param tags object = {} +param logAnalyticsWorkspaceId string + +resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { + name: name + location: location + tags: tags + kind: 'web' + properties: { + Application_Type: 'web' + WorkspaceResourceId: logAnalyticsWorkspaceId + } +} + +module applicationInsightsDashboard 'applicationinsights-dashboard.bicep' = if (!empty(dashboardName)) { + name: 'application-insights-dashboard' + params: { + name: dashboardName + location: location + applicationInsightsName: applicationInsights.name + } +} + +output connectionString string = applicationInsights.properties.ConnectionString +output id string = applicationInsights.id +output instrumentationKey string = applicationInsights.properties.InstrumentationKey +output name string = applicationInsights.name diff --git a/infra/core/monitor/loganalytics.bicep b/infra/core/monitor/loganalytics.bicep new file mode 100644 index 0000000..33f9dc2 --- /dev/null +++ b/infra/core/monitor/loganalytics.bicep @@ -0,0 +1,22 @@ +metadata description = 'Creates a Log Analytics workspace.' +param name string +param location string = resourceGroup().location +param tags object = {} + +resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = { + name: name + location: location + tags: tags + properties: any({ + retentionInDays: 30 + features: { + searchVersion: 1 + } + sku: { + name: 'PerGB2018' + } + }) +} + +output id string = logAnalytics.id +output name string = logAnalytics.name diff --git a/infra/core/monitor/monitoring.bicep b/infra/core/monitor/monitoring.bicep new file mode 100644 index 0000000..7476125 --- /dev/null +++ b/infra/core/monitor/monitoring.bicep @@ -0,0 +1,33 @@ +metadata description = 'Creates an Application Insights instance and a Log Analytics workspace.' +param logAnalyticsName string +param applicationInsightsName string +param applicationInsightsDashboardName string = '' +param location string = resourceGroup().location +param tags object = {} + +module logAnalytics 'loganalytics.bicep' = { + name: 'loganalytics' + params: { + name: logAnalyticsName + location: location + tags: tags + } +} + +module applicationInsights 'applicationinsights.bicep' = { + name: 'applicationinsights' + params: { + name: applicationInsightsName + location: location + tags: tags + dashboardName: applicationInsightsDashboardName + logAnalyticsWorkspaceId: logAnalytics.outputs.id + } +} + +output applicationInsightsConnectionString string = applicationInsights.outputs.connectionString +output applicationInsightsId string = applicationInsights.outputs.id +output applicationInsightsInstrumentationKey string = applicationInsights.outputs.instrumentationKey +output applicationInsightsName string = applicationInsights.outputs.name +output logAnalyticsWorkspaceId string = logAnalytics.outputs.id +output logAnalyticsWorkspaceName string = logAnalytics.outputs.name diff --git a/infra/core/security/keyvault-access.bicep b/infra/core/security/keyvault-access.bicep new file mode 100644 index 0000000..316775f --- /dev/null +++ b/infra/core/security/keyvault-access.bicep @@ -0,0 +1,22 @@ +metadata description = 'Assigns an Azure Key Vault access policy.' +param name string = 'add' + +param keyVaultName string +param permissions object = { secrets: [ 'get', 'list' ] } +param principalId string + +resource keyVaultAccessPolicies 'Microsoft.KeyVault/vaults/accessPolicies@2022-07-01' = { + parent: keyVault + name: name + properties: { + accessPolicies: [ { + objectId: principalId + tenantId: subscription().tenantId + permissions: permissions + } ] + } +} + +resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { + name: keyVaultName +} diff --git a/infra/core/security/keyvault-secret.bicep b/infra/core/security/keyvault-secret.bicep new file mode 100644 index 0000000..7441b29 --- /dev/null +++ b/infra/core/security/keyvault-secret.bicep @@ -0,0 +1,31 @@ +metadata description = 'Creates or updates a secret in an Azure Key Vault.' +param name string +param tags object = {} +param keyVaultName string +param contentType string = 'string' +@description('The value of the secret. Provide only derived values like blob storage access, but do not hard code any secrets in your templates') +@secure() +param secretValue string + +param enabled bool = true +param exp int = 0 +param nbf int = 0 + +resource keyVaultSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { + name: name + tags: tags + parent: keyVault + properties: { + attributes: { + enabled: enabled + exp: exp + nbf: nbf + } + contentType: contentType + value: secretValue + } +} + +resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { + name: keyVaultName +} diff --git a/infra/core/security/keyvault.bicep b/infra/core/security/keyvault.bicep new file mode 100644 index 0000000..663ec00 --- /dev/null +++ b/infra/core/security/keyvault.bicep @@ -0,0 +1,27 @@ +metadata description = 'Creates an Azure Key Vault.' +param name string +param location string = resourceGroup().location +param tags object = {} + +param principalId string = '' + +resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' = { + name: name + location: location + tags: tags + properties: { + tenantId: subscription().tenantId + sku: { family: 'A', name: 'standard' } + accessPolicies: !empty(principalId) ? [ + { + objectId: principalId + permissions: { secrets: [ 'get', 'list' ] } + tenantId: subscription().tenantId + } + ] : [] + } +} + +output endpoint string = keyVault.properties.vaultUri +output id string = keyVault.id +output name string = keyVault.name diff --git a/infra/core/security/role.bicep b/infra/core/security/role.bicep new file mode 100644 index 0000000..0b30cfd --- /dev/null +++ b/infra/core/security/role.bicep @@ -0,0 +1,21 @@ +metadata description = 'Creates a role assignment for a service principal.' +param principalId string + +@allowed([ + 'Device' + 'ForeignGroup' + 'Group' + 'ServicePrincipal' + 'User' +]) +param principalType string = 'ServicePrincipal' +param roleDefinitionId string + +resource role 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid(subscription().id, resourceGroup().id, principalId, roleDefinitionId) + properties: { + principalId: principalId + principalType: principalType + roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionId) + } +} diff --git a/infra/hooks/load_azd_env.ps1 b/infra/hooks/load_azd_env.ps1 new file mode 100644 index 0000000..16bd9e9 --- /dev/null +++ b/infra/hooks/load_azd_env.ps1 @@ -0,0 +1,21 @@ +# Loads the azd .env file into the current environment +# It does the following: +# 1. Loads the azd .env file from the current environment + +Param( + [switch] + [Parameter(Mandatory=$false)] + $ShowMessage +) + +if ($ShowMessage) { + Write-Host "Loading azd .env file from current environment" -ForegroundColor Cyan +} + +foreach ($line in (& azd env get-values)) { + if ($line -match "([^=]+)=(.*)") { + $key = $matches[1] + $value = $matches[2] -replace '^"|"$' + [Environment]::SetEnvironmentVariable($key, $value) + } +} diff --git a/infra/hooks/load_azd_env.sh b/infra/hooks/load_azd_env.sh new file mode 100755 index 0000000..75fb3ec --- /dev/null +++ b/infra/hooks/load_azd_env.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +set -e + +SHOW_MESSAGE=false + +if [[ $# -eq 0 ]]; then + SHOW_MESSAGE=false +fi + +while [[ "$1" != "" ]]; do + case $1 in + -m | --show-message) + SHOW_MESSAGE=true + ;; + + *) + usage + exit 1 + ;; + esac + + shift +done + +if [[ $SHOW_MESSAGE == true ]]; then + echo -e "\e[36mLoading azd .env file from current environment...\e[0m" +fi + +while IFS='=' read -r key value; do + value=$(echo "$value" | sed 's/^"//' | sed 's/"$//') + export "$key=$value" +done <$null + +if ([string]::IsNullOrEmpty($EXPIRED_TOKEN)) { + az login --scope https://graph.microsoft.com/.default -o none +} + +if ([string]::IsNullOrEmpty($env:AZURE_SUBSCRIPTION_ID)) { + $ACCOUNT = az account show --query '[id,name]' + Write-Host "You can set the `AZURE_SUBSCRIPTION_ID` environment variable with `azd env set AZURE_SUBSCRIPTION_ID`." + Write-Host $ACCOUNT + + $response = Read-Host "Do you want to use the above subscription? (Y/n) " + $response = if ([string]::IsNullOrEmpty($response)) { "Y" } else { $response } + switch ($response) { + { $_ -match "^[yY](es)?$" } { + # Do nothing + break + } + default { + Write-Host "Listing available subscriptions..." + $SUBSCRIPTIONS = az account list --query 'sort_by([], &name)' --output json + Write-Host "Available subscriptions:" + Write-Host ($SUBSCRIPTIONS | ConvertFrom-Json | ForEach-Object { "{0} {1}" -f $_.name, $_.id } | Format-Table) + $subscription_input = Read-Host "Enter the name or ID of the subscription you want to use: " + $AZURE_SUBSCRIPTION_ID = ($SUBSCRIPTIONS | ConvertFrom-Json | Where-Object { $_.name -eq $subscription_input -or $_.id -eq $subscription_input } | Select-Object -exp id) + if (-not [string]::IsNullOrEmpty($AZURE_SUBSCRIPTION_ID)) { + Write-Host "Setting active subscription to: $AZURE_SUBSCRIPTION_ID" + az account set -s $AZURE_SUBSCRIPTION_ID + } + else { + Write-Host "Subscription not found. Please enter a valid subscription name or ID." + exit 1 + } + break + } + } +} +else { + az account set -s $env:AZURE_SUBSCRIPTION_ID +} diff --git a/infra/hooks/login.sh b/infra/hooks/login.sh new file mode 100755 index 0000000..2242233 --- /dev/null +++ b/infra/hooks/login.sh @@ -0,0 +1,71 @@ +#!/bin/bash + +# Logs in to Azure through AZD and AZ CLI +# It does the following: +# 1. Checks if the user is logged in to Azure +# 2. Logs in to Azure Developer CLI if the user is not logged in +# 3. Logs in to Azure CLI if the user is not logged in +# 4. Sets the active subscription if the user is logged in +# 5. Prompts the user to select a subscription if the subscription is not set +# 6. Sets the active subscription to the selected subscription +# 7. Exits if the subscription is not found + +set -e + +# REPOSITORY_ROOT=$(git rev-parse --show-toplevel) +REPOSITORY_ROOT="$(dirname "$(realpath "$0")")/../.." + +# Load the azd environment variables +"$REPOSITORY_ROOT/infra/hooks/load_azd_env.sh" + +# AZD LOGIN +# Check if the user is logged in to Azure +login_status=$(azd auth login --check-status) + +# Check if the user is not logged in +if [[ "$login_status" == *"Not logged in"* ]]; then + echo "Not logged in, initiating login process..." + # Command to log in to Azure + azd auth login +fi + +# AZ LOGIN +EXPIRED_TOKEN=$(az ad signed-in-user show --query 'id' -o tsv 2>/dev/null || true) + +if [[ -z "$EXPIRED_TOKEN" ]]; then + az login --scope https://graph.microsoft.com/.default -o none +fi + +if [[ -z "${AZURE_SUBSCRIPTION_ID:-}" ]]; then + ACCOUNT=$(az account show --query '[id,name]') + echo "You can set the \`AZURE_SUBSCRIPTION_ID\` environment variable with \`azd env set AZURE_SUBSCRIPTION_ID\`." + echo $ACCOUNT + + read -r -p "Do you want to use the above subscription? (Y/n) " response + response=${response:-Y} + case "$response" in + [yY][eE][sS]|[yY]) + ;; + *) + echo "Listing available subscriptions..." + SUBSCRIPTIONS=$(az account list --query 'sort_by([], &name)' --output json) + echo "Available subscriptions:" + echo "$SUBSCRIPTIONS" | jq -r '.[] | [.name, .id] | @tsv' | column -t -s $'\t' + read -r -p "Enter the name or ID of the subscription you want to use: " subscription_input + AZURE_SUBSCRIPTION_ID=$(echo "$SUBSCRIPTIONS" | jq -r --arg input "$subscription_input" '.[] | select(.name==$input or .id==$input) | .id') + if [[ -n "$AZURE_SUBSCRIPTION_ID" ]]; then + echo "Setting active subscription to: $AZURE_SUBSCRIPTION_ID" + az account set -s $AZURE_SUBSCRIPTION_ID + else + echo "Subscription not found. Please enter a valid subscription name or ID." + exit 1 + fi + ;; + *) + echo "Use the \`az account set\` command to set the subscription you'd like to use and re-run this script." + exit 0 + ;; + esac +else + az account set -s $AZURE_SUBSCRIPTION_ID +fi diff --git a/infra/hooks/postprovision.ps1 b/infra/hooks/postprovision.ps1 new file mode 100644 index 0000000..fe672a8 --- /dev/null +++ b/infra/hooks/postprovision.ps1 @@ -0,0 +1,62 @@ +# Runs the post-provision script after the environment is provisioned +# It does the following: +# 1. Creates a service principal and assigns the required permissions +# 2. Adds redirect URLs and required permissions to the app +# 3. Assigns the required role to the current user and service principal +# 4. Sets the environment variables + +Write-Host "Running post-provision script..." + +# $REPOSITORY_ROOT = git rev-parse --show-toplevel +$REPOSITORY_ROOT = "$(Split-Path $MyInvocation.MyCommand.Path)/../.." + +# Run only if GITHUB_WORKSPACE is NOT set - this is NOT running in a GitHub Action workflow +if ([string]::IsNullOrEmpty($env:GITHUB_WORKSPACE)) { + Write-Host "Registering the application in Azure..." + + # Load the azd environment variables + & "$REPOSITORY_ROOT/infra/hooks/load_azd_env.ps1" + + $USE_EXISTING_API_CENTER = $env:USE_EXISTING_API_CENTER + $AZURE_ENV_NAME = $env:AZURE_ENV_NAME + $RESOURCE_GROUP = $USE_EXISTING_API_CENTER ? $env:AZURE_API_CENTER_RESOURCE_GROUP : "rg-$AZURE_ENV_NAME" + + # Create a service principal and assign the required permissions + $appId = $env:AZURE_CLIENT_ID + if ([string]::IsNullOrEmpty($appId)) { + $appId = az ad app list --display-name "spn-$AZURE_ENV_NAME" --query "[].appId" -o tsv + if ([string]::IsNullOrEmpty($appId)) { + $appId = az ad app create --display-name spn-$AZURE_ENV_NAME --query "appId" -o tsv + $spnId = az ad sp create --id $appId --query "id" -o tsv + } + } + $spnId = az ad sp list --display-name "spn-$AZURE_ENV_NAME" --query "[].id" -o tsv + if ([string]::IsNullOrEmpty($spnId)) { + $spnId = az ad sp create --id $appId --query "id" -o tsv + } + $objectId = az ad app show --id $appId --query "id" -o tsv + + # Add redirect URLs and required permissions to the app + $spa = @{ redirectUris = @( "http://localhost:5173", "https://localhost:5173", "$env:AZURE_STATIC_APP_URL" ) } + $requiredResourceAccess = @( @{ resourceAppId = "c3ca1a77-7a87-4dba-b8f8-eea115ae4573"; resourceAccess = @( @{ type = "Scope"; id = "44327351-3395-414e-882e-7aa4a9c3b25d" } ) } ) + + $payload = @{ requiredResourceAccess = $requiredResourceAccess; spa = $spa } | ConvertTo-Json -Depth 100 -Compress | ConvertTo-Json + + az rest -m PATCH ` + --uri "https://graph.microsoft.com/v1.0/applications/$objectId" ` + --headers Content-Type=application/json ` + --body $payload + + # Assign the required role to the current user and service principal + $userId = az ad signed-in-user show --query "id" -o tsv + $roleDefinitionId = "c7244dfb-f447-457d-b2ba-3999044d1706" + $resourceId = az resource list --namespace "Microsoft.ApiCenter" --resource-type "services" -g $RESOURCE_GROUP --query "[].id" -o tsv + + $userAssigned = az role assignment create --role $roleDefinitionId --scope $resourceId --assignee $userId + $spnAssigned = az role assignment create --role $roleDefinitionId --scope $resourceId --assignee $spnId + + # Set the environment variables + azd env set AZURE_CLIENT_ID $appId +} else { + Write-Host "Skipping to register the application in Azure..." +} diff --git a/infra/hooks/postprovision.sh b/infra/hooks/postprovision.sh new file mode 100755 index 0000000..fc42018 --- /dev/null +++ b/infra/hooks/postprovision.sh @@ -0,0 +1,76 @@ +#!/bin/bash + +# Runs the post-provision script after the environment is provisioned +# It does the following: +# 1. Creates a service principal and assigns the required permissions +# 2. Adds redirect URLs and required permissions to the app +# 3. Assigns the required role to the current user and service principal +# 4. Sets the environment variables + +set -e + +echo "Running post-provision script..." + +# REPOSITORY_ROOT=$(git rev-parse --show-toplevel) +REPOSITORY_ROOT="$(dirname "$(realpath "$0")")/../.." + +# Run only if GITHUB_WORKSPACE is NOT set - this is NOT running in a GitHub Action workflow +if [ -z "$GITHUB_WORKSPACE" ]; +then + echo "Registering the application in Azure..." + + # Load the azd environment variables + "$REPOSITORY_ROOT/infra/hooks/load_azd_env.sh" + + if [ "$USE_EXISTING_API_CENTER" = true ]; then + RESOURCE_GROUP="$AZURE_API_CENTER_RESOURCE_GROUP" + else + RESOURCE_GROUP="rg-$AZURE_ENV_NAME" + fi + + # Create a service principal and assign the required permissions + appId=$AZURE_CLIENT_ID + + if [ -z "$appId" ] + then + appId=$(az ad app list --display-name "spn-$AZURE_ENV_NAME" --query "[].appId" -o tsv) + if [ -z "$appId" ] + then + appId=$(az ad app create --display-name "spn-$AZURE_ENV_NAME" --query "appId" -o tsv) + spnId=$(az ad sp create --id $appId --query "id" -o tsv) + fi + fi + spnId=$(az ad sp list --display-name "spn-$AZURE_ENV_NAME" --query "[].id" -o tsv) + if [ -z "$spnId" ] + then + spnId=$(az ad sp create --id $appId --query "id" -o tsv) + fi + objectId=$(az ad app show --id $appId --query "id" -o tsv) + + # Add redirect URLs and required permissions to the app + spa="{\"redirectUris\": [\"http://localhost:5173\", \"https://localhost:5173\", \"$AZURE_STATIC_APP_URL\"]}" + requiredResourceAccess="[{\"resourceAppId\": \"c3ca1a77-7a87-4dba-b8f8-eea115ae4573\", \"resourceAccess\": [{\"type\": \"Scope\", \"id\": \"44327351-3395-414e-882e-7aa4a9c3b25d\"}]}]" + + payload=$(jq -n \ + --argjson spa "$spa" \ + --argjson requiredResourceAccess "$requiredResourceAccess" \ + "{\"spa\": $spa, \"requiredResourceAccess\": $requiredResourceAccess}") + + az rest -m PATCH \ + --uri "https://graph.microsoft.com/v1.0/applications/$objectId" \ + --headers Content-Type=application/json \ + --body "$payload" + + # Assign the required role to the current user and service principal + userId=$(az ad signed-in-user show --query "id" -o tsv) + roleDefinitionId="c7244dfb-f447-457d-b2ba-3999044d1706" + resourceId=$(az resource list --namespace "Microsoft.ApiCenter" --resource-type "services" -g $RESOURCE_GROUP --query "[].id" -o tsv) + + userAssigned=$(az role assignment create --role $roleDefinitionId --scope $resourceId --assignee $userId) + spnAssigned=$(az role assignment create --role $roleDefinitionId --scope $resourceId --assignee $spnId) + + # Set the environment variables + azd env set AZURE_CLIENT_ID $appId +else + echo "Skipping to register the application in Azure..." +fi diff --git a/infra/hooks/predeploy.ps1 b/infra/hooks/predeploy.ps1 new file mode 100644 index 0000000..21a0969 --- /dev/null +++ b/infra/hooks/predeploy.ps1 @@ -0,0 +1,27 @@ +# Runs the pre-deploy script after the environment is provisioned +# It does the following: +# 1. Copies the config.example file to config.json +# 2. Updates the dataApiHostName, clientId, and tenantId in the config.json file +# 3. Saves the updated config.json file + +Write-Host "Running pre-deploy script..." + +# $REPOSITORY_ROOT = git rev-parse --show-toplevel +$REPOSITORY_ROOT = "$(Split-Path $MyInvocation.MyCommand.Path)/../.." + +# Load the azd environment variables +& "$REPOSITORY_ROOT/infra/hooks/load_azd_env.ps1" + +# Update the config.json file +Copy-Item $REPOSITORY_ROOT/public/config.example -Destination $REPOSITORY_ROOT/public/config.json -Force + +$config = Get-Content $REPOSITORY_ROOT/public/config.json | ConvertFrom-Json +$config.dataApiHostName = "$($env:AZURE_API_CENTER).data.$($env:AZURE_API_CENTER_LOCATION).azure-apicenter.ms/workspaces/default" +$config.authentication.clientId = $env:AZURE_CLIENT_ID +$config.authentication.tenantId = $env:AZURE_TENANT_ID + +$config | ConvertTo-Json -Depth 100 | Out-File $REPOSITORY_ROOT/public/config.json -Force + +# Copy the config.json file to the dist folder +Copy-Item $REPOSITORY_ROOT/public/config.json -Destination $REPOSITORY_ROOT/dist/config.json -Force +Remove-Item $REPOSITORY_ROOT/dist/config.example -Force diff --git a/infra/hooks/predeploy.sh b/infra/hooks/predeploy.sh new file mode 100755 index 0000000..d2ccd27 --- /dev/null +++ b/infra/hooks/predeploy.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +# Runs the pre-deploy script after the environment is provisioned +# It does the following: +# 1. Copies the config.example file to config.json +# 2. Updates the dataApiHostName, clientId, and tenantId in the config.json file +# 3. Saves the updated config.json file + +set -e + +echo "Running pre-deploy script..." + +# REPOSITORY_ROOT=$(git rev-parse --show-toplevel) +REPOSITORY_ROOT="$(dirname "$(realpath "$0")")/../.." + +# Load the azd environment variables +"$REPOSITORY_ROOT/infra/hooks/load_azd_env.sh" + +# Update the config.json file +cp $REPOSITORY_ROOT/public/config.example $REPOSITORY_ROOT/public/config.json + +# Read the JSON file, modify it, and write it back +jq --arg dataApiHostName "${AZURE_API_CENTER}.data.${AZURE_API_CENTER_LOCATION}.azure-apicenter.ms/workspaces/default" \ + --arg clientId "$AZURE_CLIENT_ID" \ + --arg tenantId "$AZURE_TENANT_ID" \ + '.dataApiHostName = $dataApiHostName | .authentication.clientId = $clientId | .authentication.tenantId = $tenantId' \ + $REPOSITORY_ROOT/public/config.json > tmp.$$.json && mv tmp.$$.json $REPOSITORY_ROOT/public/config.json + +# Copy the config.json file to the dist folder +cp $REPOSITORY_ROOT/public/config.json $REPOSITORY_ROOT/dist/config.json +rm $REPOSITORY_ROOT/dist/config.example diff --git a/infra/hooks/predown.ps1 b/infra/hooks/predown.ps1 new file mode 100644 index 0000000..bf0a789 --- /dev/null +++ b/infra/hooks/predown.ps1 @@ -0,0 +1,18 @@ +# Runs the pre-down script before the environment is provisioned +# It does the following: +# 1. Loads the azd environment variables +# 2. Logs in to the Azure CLI if not running in a GitHub Action + +Write-Host "Running pre-down script..." + +# $REPOSITORY_ROOT = git rev-parse --show-toplevel +$REPOSITORY_ROOT = "$(Split-Path $MyInvocation.MyCommand.Path)/../.." + +# Load the azd environment variables +& "$REPOSITORY_ROOT/infra/hooks/load_azd_env.ps1" + + +if ([string]::IsNullOrEmpty($env:GITHUB_WORKSPACE)) { + # The GITHUB_WORKSPACE is not set, meaning this is not running in a GitHub Action + & "$REPOSITORY_ROOT/infra/hooks/login.ps1" +} diff --git a/infra/hooks/predown.sh b/infra/hooks/predown.sh new file mode 100755 index 0000000..4eb0b03 --- /dev/null +++ b/infra/hooks/predown.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# Runs the pre-down script before the environment is provisioned +# It does the following: +# 1. Loads the azd environment variables +# 2. Logs in to the Azure CLI if not running in a GitHub Action + +set -e + +echo "Running pre-down script..." + +# REPOSITORY_ROOT=$(git rev-parse --show-toplevel) +REPOSITORY_ROOT="$(dirname "$(realpath "$0")")/../.." + +# Load the azd environment variables +"$REPOSITORY_ROOT/infra/hooks/load_azd_env.sh" + +if [ -z "$GITHUB_WORKSPACE" ]; then + # The GITHUB_WORKSPACE is not set, meaning this is not running in a GitHub Action + "$REPOSITORY_ROOT/infra/hooks/login.sh" +fi diff --git a/infra/hooks/preprovision.ps1 b/infra/hooks/preprovision.ps1 new file mode 100644 index 0000000..7135780 --- /dev/null +++ b/infra/hooks/preprovision.ps1 @@ -0,0 +1,17 @@ +# Runs the pre-provision script before the environment is provisioned +# It does the following: +# 1. Loads the azd environment variables +# 2. Logs in to the Azure CLI if not running in a GitHub Action + +Write-Host "Running pre-provision script..." + +# $REPOSITORY_ROOT = git rev-parse --show-toplevel +$REPOSITORY_ROOT = "$(Split-Path $MyInvocation.MyCommand.Path)/../.." + +# Load the azd environment variables +& "$REPOSITORY_ROOT/infra/hooks/load_azd_env.ps1" -ShowMessage + +if ([string]::IsNullOrEmpty($env:GITHUB_WORKSPACE)) { + # The GITHUB_WORKSPACE is not set, meaning this is not running in a GitHub Action + & "$REPOSITORY_ROOT/infra/hooks/login.ps1" +} diff --git a/infra/hooks/preprovision.sh b/infra/hooks/preprovision.sh new file mode 100755 index 0000000..ed1ea2c --- /dev/null +++ b/infra/hooks/preprovision.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# Runs the pre-provision script before the environment is provisioned +# It does the following: +# 1. Loads the azd environment variables +# 2. Logs in to the Azure CLI if not running in a GitHub Action + +set -e + +echo "Running pre-provision script..." + +# REPOSITORY_ROOT=$(git rev-parse --show-toplevel) +REPOSITORY_ROOT="$(dirname "$(realpath "$0")")/../.." + +# Load the azd environment variables +"$REPOSITORY_ROOT/infra/hooks/load_azd_env.sh" --show-message + +if [ -z "$GITHUB_WORKSPACE" ]; then + # The GITHUB_WORKSPACE is not set, meaning this is not running in a GitHub Action + "$REPOSITORY_ROOT/infra/hooks/login.sh" +fi diff --git a/infra/hooks/preup.ps1 b/infra/hooks/preup.ps1 new file mode 100644 index 0000000..14b7773 --- /dev/null +++ b/infra/hooks/preup.ps1 @@ -0,0 +1,17 @@ +# Runs the pre-up script before the environment is provisioned +# It does the following: +# 1. Loads the azd environment variables +# 2. Logs in to the Azure CLI if not running in a GitHub Action + +Write-Host "Running pre-up script..." + +# $REPOSITORY_ROOT = git rev-parse --show-toplevel +$REPOSITORY_ROOT = "$(Split-Path $MyInvocation.MyCommand.Path)/../.." + +# Load the azd environment variables +& "$REPOSITORY_ROOT/infra/hooks/load_azd_env.ps1" -ShowMessage + +if ([string]::IsNullOrEmpty($env:GITHUB_WORKSPACE)) { + # The GITHUB_WORKSPACE is not set, meaning this is not running in a GitHub Action + & "$REPOSITORY_ROOT/infra/hooks/login.ps1" +} diff --git a/infra/hooks/preup.sh b/infra/hooks/preup.sh new file mode 100755 index 0000000..9573e05 --- /dev/null +++ b/infra/hooks/preup.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# Runs the pre-up script before the environment is provisioned +# It does the following: +# 1. Loads the azd environment variables +# 2. Logs in to the Azure CLI if not running in a GitHub Action + +set -e + +echo "Running pre-up script..." + +# REPOSITORY_ROOT=$(git rev-parse --show-toplevel) +REPOSITORY_ROOT="$(dirname "$(realpath "$0")")/../.." + +# Load the azd environment variables +"$REPOSITORY_ROOT/infra/hooks/load_azd_env.sh" --show-message + +if [ -z "$GITHUB_WORKSPACE" ]; then + # The GITHUB_WORKSPACE is not set, meaning this is not running in a GitHub Action + "$REPOSITORY_ROOT/infra/hooks/login.sh" +fi diff --git a/infra/main.bicep b/infra/main.bicep new file mode 100644 index 0000000..4ffbce0 --- /dev/null +++ b/infra/main.bicep @@ -0,0 +1,152 @@ +targetScope = 'subscription' + +// The main bicep module to provision Azure resources. +// For a more complete walkthrough to understand how this file works with azd, +// see https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/make-azd-compatible?pivots=azd-create + +@minLength(1) +@maxLength(64) +@description('Name of the the environment which is used to generate a short unique hash used in all resources.') +param environmentName string + +// Limited to the following locations due to the availability of API Center +@minLength(1) +@description('Primary location for all resources') +@allowed([ + 'australiaeast' + 'centralindia' + 'eastus' + 'uksouth' + 'westeurope' +]) +@metadata({ + azd: { + type: 'location' + } +}) +param location string + +param resourceGroupName string = '' + +@description('Value indicating whether to use existing API Center instance or not.') +param apiCenterExisted bool +@description('Name of the API Center. You can omit this value if `apiCenterExisted` value is set to `False`.') +param apiCenterName string +// Set API Center location the same location as the main location +var apiCenterRegion = location +@description('Name of the API Center resource group. You can omit this value if `apiCenterExisted` value is set to `False`.') +param apiCenterResourceGroupName string + +@description('Use monitoring and performance tracing') +param useMonitoring bool // Set in main.parameters.json + +param logAnalyticsName string = '' +param applicationInsightsName string = '' +param applicationInsightsDashboardName string = '' + +// Limited to the following locations due to the availability of Static Web Apps +@minLength(1) +@description('Location for Static Web Apps') +@allowed([ + 'centralus' + 'eastasia' + 'eastus2' + 'westeurope' + 'westus2' +]) +@metadata({ + azd: { + type: 'location' + } +}) +param staticAppLocation string +param staticAppSkuName string = 'Free' +param staticAppName string = '' + +var abbrs = loadJsonContent('./abbreviations.json') + +// tags that should be applied to all resources. +var tags = { + // Tag all resources with the environment name. + 'azd-env-name': environmentName +} + +// Generate a unique token to be used in naming resources. +// Remove linter suppression after using. +#disable-next-line no-unused-vars +var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) + +// Name of the service defined in azure.yaml +// A tag named azd-service-name with this value should be applied to the service host resource, such as: +// Microsoft.Web/sites for appservice, function +// Example usage: +// tags: union(tags, { 'azd-service-name': apiServiceName }) +#disable-next-line no-unused-vars +var azdServiceName = 'staticapp-portal' + +// Organize resources in a resource group +resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { + name: !empty(resourceGroupName) ? resourceGroupName : '${abbrs.resourcesResourceGroups}${environmentName}' + location: location + tags: tags +} + +resource rgApiCenter 'Microsoft.Resources/resourceGroups@2021-04-01' existing = if (apiCenterExisted == true) { + name: apiCenterResourceGroupName +} + +// Provision API Center +module apiCenter './core/gateway/apicenter.bicep' = if (apiCenterExisted != true) { + name: 'apicenter' + scope: rg + params: { + name: !empty(apiCenterName) ? apiCenterName : 'apic-${resourceToken}' + location: apiCenterRegion + tags: tags + } +} + +resource apiCenterExisting 'Microsoft.ApiCenter/services@2024-03-15-preview' existing = if (apiCenterExisted == true) { + name: apiCenterName + scope: rgApiCenter +} + +// Provision monitoring resource with Azure Monitor +module monitoring './core/monitor/monitoring.bicep' = if (useMonitoring == true) { + name: 'monitoring' + scope: rg + params: { + location: location + tags: tags + logAnalyticsName: !empty(logAnalyticsName) ? logAnalyticsName : '${abbrs.operationalInsightsWorkspaces}${resourceToken}' + applicationInsightsName: !empty(applicationInsightsName) ? applicationInsightsName : '${abbrs.insightsComponents}${resourceToken}' + applicationInsightsDashboardName: !empty(applicationInsightsDashboardName) ? applicationInsightsDashboardName : '${abbrs.portalDashboards}${resourceToken}' + } +} + +// Provision Static Web Apps for each application +module staticApp './core/host/staticwebapp.bicep' = { + name: 'staticapp' + scope: rg + params: { + name: !empty(staticAppName) ? staticAppName : '${abbrs.webStaticSites}${resourceToken}-portal' + location: staticAppLocation + tags: union(tags, { 'azd-service-name': azdServiceName }) + sku: { + name: staticAppSkuName + tier: staticAppSkuName + } + } +} + +output AZURE_LOCATION string = location +output AZURE_TENANT_ID string = tenant().tenantId + +output USE_EXISTING_API_CENTER bool = apiCenterExisted +output AZURE_API_CENTER string = apiCenterExisted ? apiCenterExisting.name : apiCenter.outputs.name +output AZURE_API_CENTER_LOCATION string = apiCenterExisted ? apiCenterExisting.location : apiCenter.outputs.location +output AZURE_API_CENTER_RESOURCE_GROUP string = apiCenterExisted ? rgApiCenter.name : rg.name + +output AZURE_STATIC_APP string = staticApp.outputs.name +output AZURE_STATIC_APP_URL string = staticApp.outputs.uri +output AZURE_STATIC_APP_LOCATION string = staticApp.outputs.location diff --git a/infra/main.parameters.json b/infra/main.parameters.json new file mode 100644 index 0000000..1306189 --- /dev/null +++ b/infra/main.parameters.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "environmentName": { + "value": "${AZURE_ENV_NAME}" + }, + "location": { + "value": "${AZURE_LOCATION}" + }, + "apiCenterExisted": { + "value": "${USE_EXISTING_API_CENTER}" + }, + "apiCenterName": { + "value": "${AZURE_API_CENTER}" + }, + "apiCenterRegion": { + "value": "${AZURE_API_CENTER_LOCATION}" + }, + "apiCenterResourceGroupName": { + "value": "${AZURE_API_CENTER_RESOURCE_GROUP}" + }, + "useMonitoring": { + "value": "${USE_MONITORING=false}" + }, + "staticAppLocation": { + "value": "${AZURE_STATIC_APP_LOCATION}" + } + } +} \ No newline at end of file diff --git a/infra/scripts/Get-AzdVariable.ps1 b/infra/scripts/Get-AzdVariable.ps1 new file mode 100644 index 0000000..824765f --- /dev/null +++ b/infra/scripts/Get-AzdVariable.ps1 @@ -0,0 +1,60 @@ +# This gets environment variables from the current azd context. +Param( + [string] + [Parameter(Mandatory=$false)] + $Key = "", + + [switch] + [Parameter(Mandatory=$false)] + $Help +) + +function Show-Usage { + Write-Output " This gets environment variables from the current azd context + + Usage: $(Split-Path $MyInvocation.ScriptName -Leaf) `` + [-Key ] `` + + [-Help] + + Options: + -Key: Environment variable key. Example: ``AZURE_ENV_NAME`` + + -Help: Show this message. +" + + Exit 0 +} + +# Show usage +$needHelp = $Help -eq $true +if ($needHelp -eq $true) { + Show-Usage + Exit 0 +} + +if ($Key -eq "") { + Write-Host " Key is required." -ForegroundColor Red + Write-Host "" + + Show-Usage + Exit 0 +} + +$REPOSITORY_ROOT = git rev-parse --show-toplevel + +pwsh -Command { + Param( + $REPOSITORY_ROOT, + $Key + ) + + # Load the azd environment variables + & "$REPOSITORY_ROOT/infra/hooks/load_azd_env.ps1" + + $envs = Get-ChildItem -Path env: + + $value = $($envs | Where-Object { $_.Name -eq $Key }).Value + + Write-Output $value +} -args $REPOSITORY_ROOT, $Key diff --git a/infra/scripts/Set-GitHubVariables.ps1 b/infra/scripts/Set-GitHubVariables.ps1 new file mode 100644 index 0000000..f16b04c --- /dev/null +++ b/infra/scripts/Set-GitHubVariables.ps1 @@ -0,0 +1,53 @@ +# This sets environment variables to GitHub repository. +Param( + [string] + [Parameter(Mandatory=$false)] + $Repo = "", + + [switch] + [Parameter(Mandatory=$false)] + $Help +) + +function Show-Usage { + Write-Output " This sets environment variables to GitHub repository + + Usage: $(Split-Path $MyInvocation.ScriptName -Leaf) `` + [-Repo ] `` + + [-Help] + + Options: + -Repo: GitHub repository formatted as ``owner/repo``. Example: ``Azure/APICenter-Portal-Starter`` + + -Help: Show this message. +" + + Exit 0 +} + +# Show usage +$needHelp = $Help -eq $true +if ($needHelp -eq $true) { + Show-Usage + Exit 0 +} + +if ($Repo -eq "") { + $segments = $(git config --get remote.origin.url).Split("/", [System.StringSplitOptions]::RemoveEmptyEntries) + $Repo = $([string]::Join("/", $segments[2], $segments[3])).Replace(".git", "") +} + +$REPOSITORY_ROOT = git rev-parse --show-toplevel + +pwsh -Command { + Param( + $REPOSITORY_ROOT, + $Repo + ) + + # Load the azd environment variables + & "$REPOSITORY_ROOT/infra/hooks/load_azd_env.ps1" -ShowMessage + + gh variable set -f $REPOSITORY_ROOT/.azure/$env:AZURE_ENV_NAME/.env -R $Repo +} -args $REPOSITORY_ROOT, $Repo diff --git a/infra/scripts/get-azdvariable.sh b/infra/scripts/get-azdvariable.sh new file mode 100755 index 0000000..903be78 --- /dev/null +++ b/infra/scripts/get-azdvariable.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +# This gets environment variables from the current azd context. + +set -e + +function usage() { + cat <