diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 903d67a..f01183b 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -17,6 +17,9 @@ sudo mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg; \ sudo sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/microsoft-ubuntu-$(lsb_release -cs)-prod $(lsb_release -cs) main" > /etc/apt/sources.list.d/dotnetdev.list';\ sudo apt-get update && sudo apt-get -y install azure-functions-core-tools-4 +# Install Azure Dev CLI +RUN curl -fsSL https://aka.ms/install-azd.sh | bash + # apt-get update && export DEBIAN_FRONTEND=noninteractive \ # && apt-get -y install --no-install-recommends diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b3f8ff2..28c3940 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -27,7 +27,12 @@ "ms-dotnettools.csharp", "ms-mssql.mssql", "ms-azuretools.vscode-azurefunctions", - "Prisma.prisma" + "Prisma.prisma", + + // for Azure Developer CLI + "ms-azuretools.azure-dev", + "ms-azuretools.vscode-bicep", + "ms-azuretools.vscode-docker" ], // Use 'forwardPorts' to make a list of ports inside the container available locally. @@ -56,7 +61,14 @@ "postCreateCommand": "bash .devcontainer/mssql/postCreateCommand.sh 'P@ssw0rd' './bin/Debug/' './.devcontainer/mssql/'", "features": { "github-cli": "latest", - "azure-cli": "latest" + "azure-cli": "latest", + + // for Azure Developer CLI + "docker-from-docker": "20.10", + "node": { + "version": "16", + "nodeGypDependencies": false + } } } diff --git a/azure.yaml b/.repo/bicep/azure.yaml similarity index 51% rename from azure.yaml rename to .repo/bicep/azure.yaml index 8aae34c..cfea1fe 100644 --- a/azure.yaml +++ b/.repo/bicep/azure.yaml @@ -1,14 +1,9 @@ # yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json name: azure-sql-prisma-vue - -infra: - provider: bicep - path: main - services: - web: - project: ./client - dist: dist - language: js - host: staticwebapp + web: + project: ../../client + dist: dist + language: js + host: staticwebapp \ No newline at end of file diff --git a/.repo/bicep/infra/abbreviations.json b/.repo/bicep/infra/abbreviations.json new file mode 100644 index 0000000..a4fc9df --- /dev/null +++ b/.repo/bicep/infra/abbreviations.json @@ -0,0 +1,135 @@ +{ + "analysisServicesServers": "as", + "apiManagementService": "apim-", + "appConfigurationConfigurationStores": "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", + "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/.repo/bicep/infra/app/sqlserver.bicep b/.repo/bicep/infra/app/sqlserver.bicep new file mode 100644 index 0000000..d3e9593 --- /dev/null +++ b/.repo/bicep/infra/app/sqlserver.bicep @@ -0,0 +1,42 @@ +param environmentName string +param location string = resourceGroup().location + +param databaseName string = 'ToDo' +param keyVaultName string + +@secure() +param sqlAdminPassword string +@secure() +param appUserPassword string + +module sqlServer '../core/database/sqlserver/sqlserver.bicep' = { + name: 'sqlserver' + params: { + environmentName: environmentName + location: location + dbName: databaseName + keyVaultName: keyVaultName + sqlAdminPassword: sqlAdminPassword + appUserPassword: appUserPassword + } +} + +module sqlServerShadow '../core/database/sqlserver/sqlserver.bicep' = { + name: 'sqlserverShadow' + params: { + environmentName: environmentName + location: location + dbName: '${databaseName}-shadow' + keyVaultName: keyVaultName + sqlAdminPassword: sqlAdminPassword + appUserPassword: appUserPassword + } +} + +output sqlConnectionStringKey string = sqlServer.outputs.sqlConnectionStringKey +output sqlDatabaseName string = databaseName +output sqlDatabaseEndpoint string = sqlServer.outputs.sqlDatabaseEndpoint + +output sqlConnectionStringKeyShadow string = sqlServerShadow.outputs.sqlConnectionStringKey +output sqlDatabaseNameShadow string = databaseName +output sqlDatabaseEndpointShadow string = sqlServerShadow.outputs.sqlDatabaseEndpoint diff --git a/.repo/bicep/infra/app/web-staticwebapp.bicep b/.repo/bicep/infra/app/web-staticwebapp.bicep new file mode 100644 index 0000000..be6f561 --- /dev/null +++ b/.repo/bicep/infra/app/web-staticwebapp.bicep @@ -0,0 +1,24 @@ +param environmentName string +param location string = resourceGroup().location + +param serviceName string = 'web' +param applicationInsightsName string = '' +param appSettings object = {} +param keyVaultName string + +module web '../core/host/staticwebapp.bicep' = { + name: '${serviceName}-staticwebapp-module' + params: { + environmentName: environmentName + location: location + serviceName: serviceName + applicationInsightsName: applicationInsightsName + appSettings: appSettings + keyVaultName: keyVaultName + scmDoBuildDuringDeployment: true + } +} + +output WEB_NAME string = web.outputs.name +output WEB_URI string = web.outputs.uri +output WEB_IDENTITY_PRINCIPAL_ID string = web.outputs.principalId diff --git a/.repo/bicep/infra/core/database/sqlserver/sqlserver.bicep b/.repo/bicep/infra/core/database/sqlserver/sqlserver.bicep new file mode 100644 index 0000000..209ab2f --- /dev/null +++ b/.repo/bicep/infra/core/database/sqlserver/sqlserver.bicep @@ -0,0 +1,132 @@ +param environmentName string +param location string = resourceGroup().location + +param appUser string = 'appUser' +param dbName string +param keyVaultName string +param sqlAdmin string = 'sqlAdmin' +param sqlConnectionStringKey string = 'AZURE-SQL-CONNECTION-STRING' + +@secure() +param sqlAdminPassword string +@secure() +param appUserPassword string + +var abbrs = loadJsonContent('../../../abbreviations.json') +var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) +var tags = { 'azd-env-name': environmentName } + +resource sqlServer 'Microsoft.Sql/servers@2022-02-01-preview' = { + name: '${abbrs.sqlServers}${resourceToken}' + location: location + tags: tags + properties: { + version: '12.0' + minimalTlsVersion: '1.2' + publicNetworkAccess: 'Enabled' + administratorLogin: sqlAdmin + administratorLoginPassword: sqlAdminPassword + } + + resource database 'databases' = { + name: dbName + location: location + } + + resource firewall 'firewallRules' = { + name: 'Azure Services' + properties: { + // Allow all clients + // Note: range [0.0.0.0-0.0.0.0] means "allow all Azure-hosted clients only". + // This is not sufficient, because we also want to allow direct access from developer machine, for debugging purposes. + startIpAddress: '0.0.0.1' + endIpAddress: '255.255.255.254' + } + } +} + +resource sqlDeploymentScript 'Microsoft.Resources/deploymentScripts@2020-10-01' = { + name: 'script-${resourceToken}' + location: location + kind: 'AzureCLI' + properties: { + azCliVersion: '2.37.0' + retentionInterval: 'PT1H' // Retain the script resource for 1 hour after it ends running + timeout: 'PT5M' // Five minutes + cleanupPreference: 'OnSuccess' + environmentVariables: [ + { + name: 'APPUSERNAME' + value: appUser + } + { + name: 'APPUSERPASSWORD' + secureValue: appUserPassword + } + { + name: 'DBNAME' + value: dbName + } + { + name: 'DBSERVER' + value: sqlServer.properties.fullyQualifiedDomainName + } + { + name: 'SQLCMDPASSWORD' + secureValue: sqlAdminPassword + } + { + name: 'SQLADMIN' + value: sqlAdmin + } + ] + + scriptContent: ''' +wget https://github.com/microsoft/go-sqlcmd/releases/download/v0.8.1/sqlcmd-v0.8.1-linux-x64.tar.bz2 +tar x -f sqlcmd-v0.8.1-linux-x64.tar.bz2 -C . + +cat < ./initDb.sql +drop user ${APPUSERNAME} +go +create user ${APPUSERNAME} with password = '${APPUSERPASSWORD}' +go +alter role db_owner add member ${APPUSERNAME} +go +SCRIPT_END + +./sqlcmd -S ${DBSERVER} -d ${DBNAME} -U ${SQLADMIN} -i ./initDb.sql + ''' + } +} + +resource sqlAdminPasswordSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { + parent: keyVault + name: 'sqlAdminPassword' + properties: { + value: sqlAdminPassword + } +} + +resource appUserPasswordSecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { + parent: keyVault + name: 'appUserPassword' + properties: { + value: appUserPassword + } +} + +resource sqlAzureConnectionStringSercret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = { + parent: keyVault + name: sqlConnectionStringKey + properties: { + value: '${azureSqlConnectionString}; Password=${appUserPassword}' + } +} + +resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { + name: keyVaultName +} + +var azureSqlConnectionString = 'Server=${sqlServer.properties.fullyQualifiedDomainName}; Database=${sqlServer::database.name}; User=${appUser}' +output sqlConnectionStringKey string = sqlConnectionStringKey +output sqlDatabaseEndpoint string = sqlServer.properties.fullyQualifiedDomainName diff --git a/.repo/bicep/infra/core/host/staticwebapp.bicep b/.repo/bicep/infra/core/host/staticwebapp.bicep new file mode 100644 index 0000000..7a57431 --- /dev/null +++ b/.repo/bicep/infra/core/host/staticwebapp.bicep @@ -0,0 +1,58 @@ +param environmentName string +param location string = resourceGroup().location + +param scmDoBuildDuringDeployment bool = false +param applicationInsightsName string = '' +param keyVaultName string = '' +param appSettings object = {} +param serviceName string +param sku object = { + name: 'Free' + tier: 'Free' +} + +var abbrs = loadJsonContent('../..//abbreviations.json') +var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) +var tags = { 'azd-env-name': environmentName } + +resource web 'Microsoft.Web/staticSites@2022-03-01' = { + name: '${abbrs.webStaticSites}${serviceName}-${resourceToken}' + location: location + tags: union(tags, { 'azd-service-name': serviceName }) + sku: sku + properties: { + provider: 'SWA-CLI' + } + + resource configAppSettings 'config' = { + name: 'appsettings' + properties: union(appSettings, + { + SCM_DO_BUILD_DURING_DEPLOYMENT: string(scmDoBuildDuringDeployment) + }, + !(empty(applicationInsightsName)) ? { APPLICATIONINSIGHTS_CONNECTION_STRING: applicationInsights.properties.ConnectionString } : {}, + !(empty(keyVaultName)) ? { AZURE_KEY_VAULT_ENDPOINT: keyVault.properties.vaultUri } : {}) + } +} + +resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (!(empty(applicationInsightsName))) { + name: applicationInsightsName +} + +module keyVaultAccess '../security/keyvault-access.bicep' = if (!(empty(keyVaultName))) { + name: '${serviceName}-appservice-keyvault-access' + params: { + principalId: web.identity.principalId + environmentName: environmentName + location: location + } +} + +resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = if (!(empty(keyVaultName))) { + name: keyVaultName +} + + +output name string = web.name +output uri string = 'https://${web.properties.defaultHostname}' +output principalId string = web.identity.principalId diff --git a/.repo/bicep/infra/core/monitor/applicationinsights-dashboard.bicep b/.repo/bicep/infra/core/monitor/applicationinsights-dashboard.bicep new file mode 100644 index 0000000..11a6512 --- /dev/null +++ b/.repo/bicep/infra/core/monitor/applicationinsights-dashboard.bicep @@ -0,0 +1,1238 @@ +param environmentName string +param location string = resourceGroup().location +param applicationInsightsName string + +var abbrs = loadJsonContent('../../abbreviations.json') +var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) +var tags = { 'azd-env-name': environmentName } + +// 2020-09-01-preview because that is the latest valid version +resource applicationInsightsDashboard 'Microsoft.Portal/dashboards@2020-09-01-preview' = { + name: '${abbrs.portalDashboards}${resourceToken}' + 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/.repo/bicep/infra/core/monitor/applicationinsights.bicep b/.repo/bicep/infra/core/monitor/applicationinsights.bicep new file mode 100644 index 0000000..b9ce1fd --- /dev/null +++ b/.repo/bicep/infra/core/monitor/applicationinsights.bicep @@ -0,0 +1,30 @@ +param environmentName string +param location string = resourceGroup().location +param logAnalyticsWorkspaceId string + +var abbrs = loadJsonContent('../../abbreviations.json') +var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) +var tags = { 'azd-env-name': environmentName } + +resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { + name: '${abbrs.insightsComponents}${resourceToken}' + location: location + tags: tags + kind: 'web' + properties: { + Application_Type: 'web' + WorkspaceResourceId: logAnalyticsWorkspaceId + } +} + +module applicationInsightsDashboard 'applicationinsights-dashboard.bicep' = { + name: 'application-insights-dashboard' + params: { + environmentName: environmentName + location: location + applicationInsightsName: applicationInsights.name + } +} + +output applicationInsightsConnectionString string = applicationInsights.properties.ConnectionString +output applicationInsightsName string = applicationInsights.name diff --git a/.repo/bicep/infra/core/monitor/loganalytics.bicep b/.repo/bicep/infra/core/monitor/loganalytics.bicep new file mode 100644 index 0000000..a36912c --- /dev/null +++ b/.repo/bicep/infra/core/monitor/loganalytics.bicep @@ -0,0 +1,24 @@ +param environmentName string +param location string = resourceGroup().location + +var abbrs = loadJsonContent('../../abbreviations.json') +var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) +var tags = { 'azd-env-name': environmentName } + +resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = { + name: '${abbrs.operationalInsightsWorkspaces}${resourceToken}' + location: location + tags: tags + properties: any({ + retentionInDays: 30 + features: { + searchVersion: 1 + } + sku: { + name: 'PerGB2018' + } + }) +} + +output logAnalyticsWorkspaceId string = logAnalytics.id +output logAnalyticsWorkspaceName string = logAnalytics.name diff --git a/.repo/bicep/infra/core/monitor/monitoring.bicep b/.repo/bicep/infra/core/monitor/monitoring.bicep new file mode 100644 index 0000000..1c5ae3e --- /dev/null +++ b/.repo/bicep/infra/core/monitor/monitoring.bicep @@ -0,0 +1,24 @@ +param environmentName string +param location string = resourceGroup().location + +module logAnalytics 'loganalytics.bicep' = { + name: 'loganalytics' + params: { + environmentName: environmentName + location: location + } +} + +module applicationInsights 'applicationinsights.bicep' = { + name: 'applicationinsights' + params: { + environmentName: environmentName + location: location + logAnalyticsWorkspaceId: logAnalytics.outputs.logAnalyticsWorkspaceId + } +} + +output applicationInsightsConnectionString string = applicationInsights.outputs.applicationInsightsConnectionString +output applicationInsightsName string = applicationInsights.outputs.applicationInsightsName +output logAnalyticsWorkspaceId string = logAnalytics.outputs.logAnalyticsWorkspaceId +output logAnalyticsWorkspaceName string = logAnalytics.outputs.logAnalyticsWorkspaceName diff --git a/.repo/bicep/infra/core/security/keyvault-access.bicep b/.repo/bicep/infra/core/security/keyvault-access.bicep new file mode 100644 index 0000000..30c00f4 --- /dev/null +++ b/.repo/bicep/infra/core/security/keyvault-access.bicep @@ -0,0 +1,25 @@ +param environmentName string +param location string = resourceGroup().location + +param keyVaultName string = '' +param permissions object = { secrets: [ 'get', 'list' ] } +param principalId string + +var abbrs = loadJsonContent('../../abbreviations.json') +var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) + +resource keyVaultAccessPolicies 'Microsoft.KeyVault/vaults/accessPolicies@2022-07-01' = { + parent: keyVault + name: 'add' + properties: { + accessPolicies: [ { + objectId: principalId + tenantId: subscription().tenantId + permissions: permissions + } ] + } +} + +resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = { + name: !empty(keyVaultName) ? keyVaultName : '${abbrs.keyVaultVaults}${resourceToken}' +} diff --git a/.repo/bicep/infra/core/security/keyvault.bicep b/.repo/bicep/infra/core/security/keyvault.bicep new file mode 100644 index 0000000..96b8e84 --- /dev/null +++ b/.repo/bicep/infra/core/security/keyvault.bicep @@ -0,0 +1,29 @@ +param environmentName string +param location string = resourceGroup().location + +param keyVaultName string = '' +param principalId string = '' + +var abbrs = loadJsonContent('../../abbreviations.json') +var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) +var tags = { 'azd-env-name': environmentName } + +resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' = { + name: !empty(keyVaultName) ? keyVaultName : '${abbrs.keyVaultVaults}${resourceToken}' + 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 keyVaultEndpoint string = keyVault.properties.vaultUri +output keyVaultName string = keyVault.name diff --git a/.repo/bicep/infra/core/storage/storage-account.bicep b/.repo/bicep/infra/core/storage/storage-account.bicep new file mode 100644 index 0000000..7d2eb91 --- /dev/null +++ b/.repo/bicep/infra/core/storage/storage-account.bicep @@ -0,0 +1,29 @@ +param environmentName string +param location string = resourceGroup().location + +param allowBlobPublicAccess bool = false +param kind string = 'StorageV2' +param minimumTlsVersion string = 'TLS1_2' +param sku object = { name: 'Standard_LRS' } + +var abbrs = loadJsonContent('../../abbreviations.json') +var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) +var tags = { 'azd-env-name': environmentName } + +resource storage 'Microsoft.Storage/storageAccounts@2021-09-01' = { + name: '${abbrs.storageStorageAccounts}${resourceToken}' + location: location + tags: tags + kind: kind + sku: sku + properties: { + minimumTlsVersion: minimumTlsVersion + allowBlobPublicAccess: allowBlobPublicAccess + networkAcls: { + bypass: 'AzureServices' + defaultAction: 'Allow' + } + } +} + +output name string = storage.name diff --git a/.repo/bicep/infra/main.bicep b/.repo/bicep/infra/main.bicep new file mode 100644 index 0000000..9bc27b9 --- /dev/null +++ b/.repo/bicep/infra/main.bicep @@ -0,0 +1,42 @@ +targetScope = 'subscription' + +@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 + +@minLength(1) +@description('Primary location for all resources') +param location string + +@description('Id of the user or app to assign application roles') +param principalId string = '' + +var abbrs = loadJsonContent('abbreviations.json') +var tags = { 'azd-env-name': environmentName } + +resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = { + name: '${abbrs.resourcesResourceGroups}${environmentName}' + location: location + tags: tags +} + +module resources 'resources.bicep' = { + name: 'resources' + scope: rg + params: { + environmentName: environmentName + location: location + principalId: principalId + } +} + +output APPLICATIONINSIGHTS_CONNECTION_STRING string = resources.outputs.APPLICATIONINSIGHTS_CONNECTION_STRING +output AZURE_COSMOS_CONNECTION_STRING_KEY string = resources.outputs.AZURE_COSMOS_CONNECTION_STRING_KEY +output AZURE_COSMOS_DATABASE_NAME string = resources.outputs.AZURE_COSMOS_DATABASE_NAME +output AZURE_KEY_VAULT_ENDPOINT string = resources.outputs.AZURE_KEY_VAULT_ENDPOINT +output AZURE_LOCATION string = location +output AZURE_TENANT_ID string = tenant().tenantId +output REACT_APP_API_BASE_URL string = resources.outputs.API_URI +output REACT_APP_APPLICATIONINSIGHTS_CONNECTION_STRING string = resources.outputs.APPLICATIONINSIGHTS_CONNECTION_STRING +output REACT_APP_WEB_BASE_URL string = resources.outputs.WEB_URI diff --git a/infra/main.parameters.json b/.repo/bicep/infra/main.parameters.json similarity index 56% rename from infra/main.parameters.json rename to .repo/bicep/infra/main.parameters.json index 82d1050..9e91adf 100644 --- a/infra/main.parameters.json +++ b/.repo/bicep/infra/main.parameters.json @@ -2,7 +2,7 @@ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", "contentVersion": "1.0.0.0", "parameters": { - "name": { + "environmentName": { "value": "${AZURE_ENV_NAME}" }, "location": { @@ -10,6 +10,12 @@ }, "principalId": { "value": "${AZURE_PRINCIPAL_ID}" + }, + "sqlAdminPassword": { + "value": "$(secretOrRandomPassword ${AZURE_KEY_VAULT_NAME} sqlAdminPassword)" + }, + "appUserPassword": { + "value": "$(secretOrRandomPassword ${AZURE_KEY_VAULT_NAME} appUserPassword)" } } } \ No newline at end of file diff --git a/.repo/bicep/infra/resources.bicep b/.repo/bicep/infra/resources.bicep new file mode 100644 index 0000000..2192eb8 --- /dev/null +++ b/.repo/bicep/infra/resources.bicep @@ -0,0 +1,78 @@ +param environmentName string +param location string = resourceGroup().location +param principalId string = '' + +@secure() +param sqlAdminPassword string = '' + +@secure() +param appUserPassword string = '' + +// The application frontend +module web 'app/web-staticwebapp.bicep' = { + name: 'web' + params: { + environmentName: environmentName + location: location + applicationInsightsName: monitoring.outputs.applicationInsightsName + keyVaultName: keyVault.outputs.keyVaultName + appSettings: { + APPINSIGHTS_INSTRUMENTATIONKEY: monitoring.outputs.applicationInsightsConnectionString + DATABASE_URL: sqlserver.outputs.sqlDatabaseEndpoint + SHADOW_DATABASE_URL: sqlserver.outputs.sqlDatabaseEndpointShadow + FUNCTIONS_EXTENSION_VERSION: '~4' + FUNCTIONS_WORKER_RUNTIME: 'node' + SCM_DO_BUILD_DURING_DEPLOYMENT: 'true' + } + } +} + +// Give the API access to KeyVault +module apiKeyVaultAccess 'core/security/keyvault-access.bicep' = { + name: 'api-keyvault-access' + params: { + environmentName: environmentName + location: location + keyVaultName: keyVault.outputs.keyVaultName + principalId: web.outputs.WEB_IDENTITY_PRINCIPAL_ID + } +} + +// The application database +module sqlserver 'app/sqlserver.bicep' = { + name: 'sqlserver' + params: { + environmentName: environmentName + location: location + keyVaultName: keyVault.outputs.keyVaultName + sqlAdminPassword: sqlAdminPassword + appUserPassword: appUserPassword + } +} + +// Store secrets in a keyvault +module keyVault 'core/security/keyvault.bicep' = { + name: 'keyvault' + params: { + environmentName: environmentName + location: location + principalId: principalId + } +} + +// Monitor application with Azure Monitor +module monitoring 'core/monitor/monitoring.bicep' = { + name: 'monitoring' + params: { + environmentName: environmentName + location: location + } +} + +output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString +output AZURE_COSMOS_CONNECTION_STRING_KEY string = sqlserver.outputs.sqlConnectionStringKey +output AZURE_COSMOS_DATABASE_NAME string = sqlserver.outputs.sqlDatabaseName +output AZURE_COSMOS_ENDPOINT string = sqlserver.outputs.sqlDatabaseEndpoint +output AZURE_KEY_VAULT_ENDPOINT string = keyVault.outputs.keyVaultEndpoint +output WEB_URI string = web.outputs.WEB_URI +output API_URI string = '${web.outputs.WEB_URI}/api' diff --git a/.repo/bicep/repo.yaml b/.repo/bicep/repo.yaml new file mode 100644 index 0000000..c12bb79 --- /dev/null +++ b/.repo/bicep/repo.yaml @@ -0,0 +1,13 @@ +templateApi: 1.0.0 +metadata: + type: repo + name: azure-sql-prisma-vue + description: "Learn - A Full Stack Application with Azure SQL & Prisma: Full Course" + +repo: + includeProjectAssets: false + + remotes: + - name: azure-sql-prisma-vue-main + url: git@github.com:glaucia86/azure-sql-prisma-vue.git + diff --git a/README.md b/README.md index cd76363..09026d5 100644 --- a/README.md +++ b/README.md @@ -145,7 +145,7 @@ To create the necessary infrastructure on Azure: 1. Run the following command to initialize the project, provision Azure resources. ```bash -azd provision +azd up ``` For more details, [read the Azure Dev CLI documentation](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/). diff --git a/infra/main.bicep b/infra/main.bicep deleted file mode 100644 index e2aca0c..0000000 --- a/infra/main.bicep +++ /dev/null @@ -1,118 +0,0 @@ -targetScope = 'subscription' - -@minLength(1) -@maxLength(64) -@description('Name of the the environment which is used to generate a short unique hash used in all resources.') -param name string - -@minLength(1) -@description('Primary location for all resources') -param location string - -@description('Id of the user or app to assign app roles') -param principalId string = '' - -var resourceToken = toLower(uniqueString(subscription().id, name, location)) -var tags = { 'azd-env-name': name } - -resource resourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' = { - name: 'rg-${name}' - location: location - tags: tags -} - -// TODO: make KeyVault optional -var deployKeyVault = false - -// Note: Application Insights is required for "azd monitor" -var deployAppInsights = true - -module keyVaultResources 'resources/key-vault.bicep' = if (deployKeyVault) { - name: 'key-vault-resources' - scope: resourceGroup - params: { - enableSoftDelete: false - keyVaultName: 'kv-${resourceToken}' - location: location - roleAssignments: [ - { - // https://docs.microsoft.com/en-us/azure/key-vault/general/rbac-guide?tabs=azure-cli#azure-built-in-roles-for-key-vault-data-plane-operations - roleDefinitionId: '4633458b-17de-408a-b874-0445c86b69e6' - principalType: 'ServicePrincipal' - principalId: principalId - } - ] - tags: tags - } -} - -module swaResources 'resources/static-sites.bicep' = { - name: 'static-sites-resources' - scope: resourceGroup - params: { - appSettings: { - APPINSIGHTS_INSTRUMENTATIONKEY: deployAppInsights ? applicationInsightsResources.outputs.connectionString : null - STORAGE_CONNECTION_STRING: storageResources.outputs.connectionString - DATABASE_URL: databaseResources.outputs.url - SHADOW_DATABASE_URL: databaseResources.outputs.urlShadow - AzureWebJobsStorage: storageResources.outputs.connectionString - FUNCTIONS_EXTENSION_VERSION: '~4' - FUNCTIONS_WORKER_RUNTIME: 'node' - SCM_DO_BUILD_DURING_DEPLOYMENT: 'true' - } - buildProperties: { - skipGithubActionWorkflowGeneration: true - } - location: location - staticSiteName: 'stapp-${resourceToken}' - tags: tags - } -} - -module storageResources 'resources/storage.bicep' = { - name: 'storage-resources' - scope: resourceGroup - params: { - location: location - storageName: 'st${resourceToken}' - tags: tags - } -} - -module logAnalyticsResources 'resources/log-analytics.bicep' = { - name: 'log-analytics-resources' - scope: resourceGroup - params: { - logAnalyticsName: 'log-${resourceToken}' - location: location - tags: tags - } -} - -module applicationInsightsResources 'resources/applicationinsights.bicep' = if (deployAppInsights) { - name: 'applicationinsights-resources' - scope: resourceGroup - params: { - applicationInsightsName: resourceToken - location: location - workspaceId: logAnalyticsResources.outputs.workspaceId - tags: tags - } -} - -module databaseResources 'resources/sql-server.bicep' = { - name: 'database-resources' - scope: resourceGroup - params: { - sqlServerName: 'sql-${resourceToken}' - databaseName: 'sqldb-${resourceToken}' - administratorLogin: 'prisma-azure' - location: location - tags: tags - } -} - -output DATABASE_URL string = databaseResources.outputs.url -output SHADOW_DATABASE_URL string = databaseResources.outputs.urlShadow -output AZURE_LOCATION string = location -output AZURE_TENANT_ID string = tenant().tenantId diff --git a/infra/resources/applicationinsights.bicep b/infra/resources/applicationinsights.bicep deleted file mode 100644 index 4effaeb..0000000 --- a/infra/resources/applicationinsights.bicep +++ /dev/null @@ -1,17 +0,0 @@ -param applicationInsightsName string -param location string -param tags object -param workspaceId string - -resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = { - name: 'appi-${applicationInsightsName}' - location: location - tags: tags - kind: 'web' - properties: { - Application_Type: 'web' - WorkspaceResourceId: workspaceId - } -} - -output connectionString string = applicationInsights.properties.ConnectionString diff --git a/infra/resources/key-vault.bicep b/infra/resources/key-vault.bicep deleted file mode 100644 index db009af..0000000 --- a/infra/resources/key-vault.bicep +++ /dev/null @@ -1,66 +0,0 @@ -@description('Resource tags.') -param tags object - -@description('Resource location.') -param location string - -@description('The name of the key vault. e.g. kv-swa-sso') -param keyVaultName string - -@description('The name of the SKU for the key vault.') -param sku string = 'Standard' - -@description('Tenant id for the subscription.') -param tenant string = subscription().tenantId - -@description('Enables soft delete.') -param enableSoftDelete bool = true - -@description('Soft delete retention period.') -param softDeleteRetentionInDays int = 7 - -@description('An array of role assignment objects. Format: [{ roleDefinitionId: xxx, principalType: xxx, principalId: xxx }]') -param roleAssignments array -// E.g. -// [ -// { -// roleDefinitionId: 'replace' -// principalType: 'replace' -// principalId: 'replace' -// } -// ] - -resource keyVault 'Microsoft.KeyVault/vaults@2021-06-01-preview' = { - name: keyVaultName - location: location - tags: tags - properties: { - sku: { - name: sku - family: 'A' - } - tenantId: tenant - enabledForTemplateDeployment: true - enableRbacAuthorization: true - enableSoftDelete: enableSoftDelete - softDeleteRetentionInDays: softDeleteRetentionInDays - networkAcls: { - bypass: 'AzureServices' - defaultAction: 'Allow' - } - } -} - -resource keyVaultRoleAssignments 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = [for role in roleAssignments: { - name: guid(keyVault.name, role.roleDefinitionId, role.principalId) - scope: keyVault - properties: { - principalType: role.principalType - roleDefinitionId: resourceId('microsoft.authorization/roleDefinitions', role.roleDefinitionId) - principalId: role.principalId - } -}] - -output keyVaultName string = keyVault.name -output keyVaultResourceId string = keyVault.id -output keyVaultEndpoint string = keyVault.properties.vaultUri diff --git a/infra/resources/log-analytics.bicep b/infra/resources/log-analytics.bicep deleted file mode 100644 index e6bb1c9..0000000 --- a/infra/resources/log-analytics.bicep +++ /dev/null @@ -1,25 +0,0 @@ -@description('Resource tags.') -param tags object - -@description('Resource location.') -param location string - -@description('The name of the log Analytics workspace. e.g. log-demo') -param logAnalyticsName string - -resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = { - name: logAnalyticsName - location: location - tags: tags - properties: any({ - retentionInDays: 30 - features: { - searchVersion: 1 - } - sku: { - name: 'PerGB2018' - } - }) -} - -output workspaceId string = logAnalyticsWorkspace.id diff --git a/infra/resources/sql-server.bicep b/infra/resources/sql-server.bicep deleted file mode 100644 index 07299b0..0000000 --- a/infra/resources/sql-server.bicep +++ /dev/null @@ -1,105 +0,0 @@ -@minLength(1) -@description('Resource location') -param location string - -@description('Resource tags.') -param tags object - -@minLength(1) -@maxLength(63) -@description('The name of the SQL server account. e.g. sql-server-1') -param sqlServerName string - -@minLength(1) -@maxLength(128) -@description('The name of the SQL database. e.g. sqldb-db') -param databaseName string - -@allowed([ - 'DATABASE_DEFAULT' - 'SQL_Latin1_General_CP1_CI_AS' -]) -param catalogCollation string = 'SQL_Latin1_General_CP1_CI_AS' - -param maxSizeBytes int = 2147483648 - -@description('The SKU for the database.') -param sku object = { - name: 'Basic' - tier: 'Basic' - capacity: 5 -} - -@allowed([ - 'None' - 'SystemAssigned' - 'SystemAssigned,UserAssigned' - 'UserAssigned' -]) -param identityType string = 'SystemAssigned' - -@secure() -param administratorLogin string = '' - -@secure() -param administratorLoginPassword string = newGuid() - -var databaseProperties = { - collation: catalogCollation - maxSizeBytes: maxSizeBytes - catalogCollation: catalogCollation - zoneRedundant: false - readScale: 'Disabled' - requestedBackupStorageRedundancy: 'Geo' - maintenanceConfigurationId: '/subscriptions/${subscription().subscriptionId}/providers/Microsoft.Maintenance/publicMaintenanceConfigurations/SQL_Default' - isLedgerOn: false -} - -resource sqlServer 'Microsoft.Sql/servers@2022-02-01-preview' = { - name: sqlServerName - location: location - tags: tags - identity: { - type: identityType - } - properties: { - administratorLogin: administratorLogin - administratorLoginPassword: administratorLoginPassword - version: '12.0' - publicNetworkAccess: 'Enabled' - restrictOutboundNetworkAccess: 'Disabled' - } -} - -resource sqlDatabase 'Microsoft.Sql/servers/databases@2022-02-01-preview' = { - parent: sqlServer - name: toLower(databaseName) - location: location - sku: sku - properties: databaseProperties -} - -resource sqlDatabaseShadow 'Microsoft.Sql/servers/databases@2022-02-01-preview' = { - parent: sqlServer - name: toLower('${databaseName}-shadow') - location: location - sku: sku - properties: databaseProperties -} - -resource fwRule 'Microsoft.Sql/servers/firewallRules@2021-02-01-preview' = { - parent: sqlServer - name: 'AllowAllWindowsAzureIps' - properties: { - startIpAddress: '0.0.0.0' - endIpAddress: '0.0.0.0' - } -} - -#disable-next-line outputs-should-not-contain-secrets -output url string = 'sqlserver://${reference(sqlServer.id).fullyQualifiedDomainName}:1433;database=${sqlDatabase.name};user=${administratorLogin}@${sqlServerName};password=${administratorLoginPassword};encrypt=true;trustServerCertificate=false;hostNameInCertificate=*.database.windows.net;loginTimeout=30;' -output name string = sqlDatabase.name - -#disable-next-line outputs-should-not-contain-secrets -output urlShadow string = 'sqlserver://${reference(sqlServer.id).fullyQualifiedDomainName}:1433;database=${sqlDatabaseShadow.name};user=${administratorLogin}@${sqlServerName};password=${administratorLoginPassword};encrypt=true;trustServerCertificate=false;hostNameInCertificate=*.database.windows.net;loginTimeout=30;' -output nameShadow string = sqlDatabaseShadow.name diff --git a/infra/resources/static-sites.bicep b/infra/resources/static-sites.bicep deleted file mode 100644 index ec0fb54..0000000 --- a/infra/resources/static-sites.bicep +++ /dev/null @@ -1,60 +0,0 @@ -@description('Resource tags.') -param tags object - -@description('Resource location.') -param location string - -@description('The SKU for the static site.') -param sku object = { - name: 'Free' - tier: 'Free' -} - -@description('Lock the config file for this static web app. https://docs.microsoft.com/en-us/azure/templates/microsoft.web/staticsites?tabs=bicep#staticsite') -param allowConfigFileUpdates bool = true - -@description('The name of the static site resource. eg stapp-swa-app') -param staticSiteName string - -@secure() -@description('Configuration for the static site.') -param appSettings object = {} - -@description('Build properties for the static site.') -param buildProperties object = {} - -@allowed([ - 'Disabled' - 'Enabled' -]) -@description('State indicating whether staging environments are allowed or not allowed for a static web app.') -param stagingEnvironmentPolicy string = 'Enabled' - -@description('Template Options for the static site. https://docs.microsoft.com/en-us/azure/templates/microsoft.web/staticsites?tabs=bicep#staticsitetemplateoptions') -param templateProperties object = {} - -// https://docs.microsoft.com/en-us/azure/templates/microsoft.web/staticsites?tabs=bicep -resource staticSite 'Microsoft.Web/staticSites@2022-03-01' = { - name: staticSiteName - location: location - tags: union(tags, { 'azd-service-name': 'swa' }) - sku: sku - properties: { - provider: 'Custom' - allowConfigFileUpdates: allowConfigFileUpdates - buildProperties: empty(buildProperties) ? null : buildProperties - stagingEnvironmentPolicy: stagingEnvironmentPolicy - templateProperties: empty(templateProperties) ? null : templateProperties - } -} - -resource staticSiteAppsettings 'Microsoft.Web/staticSites/config@2022-03-01' = { - parent: staticSite - name: 'appsettings' - kind: 'config' - properties: appSettings -} - -output defaultHostName string = staticSite.properties.defaultHostname -output siteName string = staticSite.name -output siteResourceId string = staticSite.id diff --git a/infra/resources/storage.bicep b/infra/resources/storage.bicep deleted file mode 100644 index 4db0d60..0000000 --- a/infra/resources/storage.bicep +++ /dev/null @@ -1,90 +0,0 @@ -@description('Resource tags.') -param tags object - -@description('Resource location.') -param location string - -@description('The name of the Storage account. e.g. stswa-storage') -param storageName string - -resource storage 'Microsoft.Storage/storageAccounts@2021-09-01' = { - name: storageName - location: location - tags: tags - sku: { - name: 'Standard_RAGRS' - } - kind: 'StorageV2' - properties: { - minimumTlsVersion: 'TLS1_2' - networkAcls: { - bypass: 'AzureServices' - virtualNetworkRules: [] - ipRules: [] - defaultAction: 'Allow' - } - supportsHttpsTrafficOnly: true - encryption: { - services: { - file: { - keyType: 'Account' - enabled: true - } - blob: { - keyType: 'Account' - enabled: true - } - } - keySource: 'Microsoft.Storage' - } - accessTier: 'Hot' - } -} - -resource blob 'Microsoft.Storage/storageAccounts/blobServices@2021-04-01' = { - parent: storage - name: 'default' - properties: { - cors: { - corsRules: [ - { - allowedOrigins: [ - '*' - ] - allowedMethods: [ - 'GET' - 'PUT' - 'POST' - 'DELETE' - 'OPTIONS' - 'HEAD' - ] - maxAgeInSeconds: 3600 - exposedHeaders: [ - '*' - ] - allowedHeaders: [ - '*' - ] - } - ] - } - deleteRetentionPolicy: { - enabled: false - } - } -} - -resource container 'Microsoft.Storage/storageAccounts/blobServices/containers@2021-09-01' = { - parent: blob - name: storageName - properties: { - defaultEncryptionScope: '$account-encryption-key' - denyEncryptionScopeOverride: false - publicAccess: 'Blob' - } -} - -#disable-next-line outputs-should-not-contain-secrets -output connectionString string = storage.listKeys().keys[0].value -output name string = storage.name