diff --git a/.gitignore b/.gitignore index 1148ecd7a..24076350c 100644 --- a/.gitignore +++ b/.gitignore @@ -257,3 +257,5 @@ pub/ #Ignore marker-file used to know which docker files we have. .eshopdocker_* + +.azure diff --git a/azure.yaml b/azure.yaml new file mode 100644 index 000000000..da4970832 --- /dev/null +++ b/azure.yaml @@ -0,0 +1,14 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json + +name: eShopOnWeb +services: + web: + project: ./src/Web + language: csharp + host: appservice +infra: + provider: "" + path: "" + module: "" +pipeline: + provider: "" diff --git a/infra/abbreviations.json b/infra/abbreviations.json new file mode 100644 index 000000000..a4fc9dfed --- /dev/null +++ b/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/infra/app/dbCatalog.bicep b/infra/app/dbCatalog.bicep new file mode 100644 index 000000000..e320f63bd --- /dev/null +++ b/infra/app/dbCatalog.bicep @@ -0,0 +1,25 @@ +param environmentName string +param location string = resourceGroup().location + +param databaseName string = 'CatalogDB' +param keyVaultName string + +@secure() +param sqlAdminPassword string +@secure() +param appUserPassword string + +module sqlServer1 '../core/database/sqlserver1.bicep' = { + name: 'sqlServer1' + params: { + environmentName: environmentName + location: location + dbName: databaseName + keyVaultName: keyVaultName + sqlAdminPassword: sqlAdminPassword + appUserPassword: appUserPassword + } +} + +output sqlConnectionStringKey string = sqlServer1.outputs.sqlConnectionStringKey +output sqlDatabase1Name string = databaseName diff --git a/infra/app/dbIdentity.bicep b/infra/app/dbIdentity.bicep new file mode 100644 index 000000000..a9cb7419d --- /dev/null +++ b/infra/app/dbIdentity.bicep @@ -0,0 +1,25 @@ +param environmentName string +param location string = resourceGroup().location + +param databaseName string = 'IdentityDB' +param keyVaultName string + +@secure() +param sqlAdminPassword string +@secure() +param appUserPassword string + +module sqlServer2 '../core/database/sqlserver2.bicep' = { + name: 'sqlServer2' + params: { + environmentName: environmentName + location: location + dbName: databaseName + keyVaultName: keyVaultName + sqlAdminPassword: sqlAdminPassword + appUserPassword: appUserPassword + } +} + +output sqlConnectionStringKey string = sqlServer2.outputs.sqlConnectionStringKey +output sqlDatabase2Name string = databaseName diff --git a/infra/app/web.bicep b/infra/app/web.bicep new file mode 100644 index 000000000..828b282bc --- /dev/null +++ b/infra/app/web.bicep @@ -0,0 +1,18 @@ +param environmentName string +param location string = resourceGroup().location +param appServicePlanId string + +param serviceName string = 'web' + +module web '../core/host/appservice-dotnet.bicep' = { + name: '${serviceName}-appservice-dotnet-module' + params: { + environmentName: environmentName + location: location + appServicePlanId: appServicePlanId + serviceName: serviceName + } +} + +output WEB_NAME string = web.outputs.name +output WEB_URI string = web.outputs.uri \ No newline at end of file diff --git a/infra/core/database/sqlserver1.bicep b/infra/core/database/sqlserver1.bicep new file mode 100644 index 000000000..671371cd5 --- /dev/null +++ b/infra/core/database/sqlserver1.bicep @@ -0,0 +1,131 @@ +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-CATALOG-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}-Catalog' + 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}-Catalog' + 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 diff --git a/infra/core/database/sqlserver2.bicep b/infra/core/database/sqlserver2.bicep new file mode 100644 index 000000000..430f48fe2 --- /dev/null +++ b/infra/core/database/sqlserver2.bicep @@ -0,0 +1,131 @@ +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-IDENTITY-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}-Identity' + 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}-Identity' + 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 diff --git a/infra/core/host/appservice-config-cosmos.bicep b/infra/core/host/appservice-config-cosmos.bicep new file mode 100644 index 000000000..65e91f9ae --- /dev/null +++ b/infra/core/host/appservice-config-cosmos.bicep @@ -0,0 +1,18 @@ +param appServiceName string +param cosmosConnectionStringKey string = '' +param cosmosDatabaseName string = '' +param cosmosEndpoint string = '' + +module appServiceConfigCosmosSettings 'appservice-config-union.bicep' = { + name: '${appServiceName}-appservice-config-cosmos-settings' + params: { + appServiceName: appServiceName + configName: 'appsettings' + currentConfigProperties: list(resourceId('Microsoft.Web/sites/config', appServiceName, 'appsettings'), '2022-03-01').properties + additionalConfigProperties: { + AZURE_COSMOS_CONNECTION_STRING_KEY: cosmosConnectionStringKey + AZURE_COSMOS_DATABASE_NAME: cosmosDatabaseName + AZURE_COSMOS_ENDPOINT: cosmosEndpoint + } + } +} diff --git a/infra/core/host/appservice-config-logs.bicep b/infra/core/host/appservice-config-logs.bicep new file mode 100644 index 000000000..fde8940db --- /dev/null +++ b/infra/core/host/appservice-config-logs.bicep @@ -0,0 +1,11 @@ +param appServiceName string + +resource siteConfigLogs 'Microsoft.Web/sites/config@2022-03-01' = { + name: '${appServiceName}/logs' + properties: { + applicationLogs: { fileSystem: { level: 'Verbose' } } + detailedErrorMessages: { enabled: true } + failedRequestsTracing: { enabled: true } + httpLogs: { fileSystem: { enabled: true, retentionInDays: 1, retentionInMb: 35 } } + } +} diff --git a/infra/core/host/appservice-config-sqlserver.bicep b/infra/core/host/appservice-config-sqlserver.bicep new file mode 100644 index 000000000..3d8c8d01f --- /dev/null +++ b/infra/core/host/appservice-config-sqlserver.bicep @@ -0,0 +1,14 @@ +param appServiceName string +param sqlConnectionStringKey string + +module appServiceConfigSqlServerSettings 'appservice-config-union.bicep' = { + name: '${appServiceName}-appservice-config-sqlserver-settings' + params: { + appServiceName: appServiceName + configName: 'appsettings' + currentConfigProperties: list(resourceId('Microsoft.Web/sites/config', appServiceName, 'appsettings'), '2022-03-01').properties + additionalConfigProperties: { + AZURE_SQL_CONNECTION_STRING_KEY: sqlConnectionStringKey + } + } +} diff --git a/infra/core/host/appservice-config-union.bicep b/infra/core/host/appservice-config-union.bicep new file mode 100644 index 000000000..3e0bcee42 --- /dev/null +++ b/infra/core/host/appservice-config-union.bicep @@ -0,0 +1,9 @@ +param additionalConfigProperties object +param appServiceName string +param configName string +param currentConfigProperties object + +resource siteConfigUnion 'Microsoft.Web/sites/config@2022-03-01' = { + name: '${appServiceName}/${configName}' + properties: union(currentConfigProperties, additionalConfigProperties) +} diff --git a/infra/core/host/appservice-dotnet.bicep b/infra/core/host/appservice-dotnet.bicep new file mode 100644 index 000000000..012a4521b --- /dev/null +++ b/infra/core/host/appservice-dotnet.bicep @@ -0,0 +1,35 @@ +param environmentName string +param location string = resourceGroup().location + +param allowedOrigins array = [] +param appCommandLine string = '' +param applicationInsightsName string = '' +param appServicePlanId string +param appSettings object = {} +param keyVaultName string = '' +param linuxFxVersion string = 'DOTNETCORE|6.0' +param managedIdentity bool = !(empty(keyVaultName)) +param scmDoBuildDuringDeployment bool = false +param serviceName string + +module appService 'appservice.bicep' = { + name: '${serviceName}-appservice-dotnet' + params: { + environmentName: environmentName + location: location + allowedOrigins: allowedOrigins + appCommandLine: appCommandLine + applicationInsightsName: applicationInsightsName + appServicePlanId: appServicePlanId + appSettings: appSettings + keyVaultName: keyVaultName + linuxFxVersion: linuxFxVersion + managedIdentity: managedIdentity + scmDoBuildDuringDeployment: scmDoBuildDuringDeployment + serviceName: serviceName + } +} + +output identityPrincipalId string = appService.outputs.identityPrincipalId +output name string = appService.outputs.name +output uri string = appService.outputs.uri diff --git a/infra/core/host/appservice-node.bicep b/infra/core/host/appservice-node.bicep new file mode 100644 index 000000000..6f56d46da --- /dev/null +++ b/infra/core/host/appservice-node.bicep @@ -0,0 +1,35 @@ +param environmentName string +param location string = resourceGroup().location + +param allowedOrigins array = [] +param appCommandLine string = '' +param applicationInsightsName string = '' +param appServicePlanId string +param appSettings object = {} +param keyVaultName string = '' +param linuxFxVersion string = 'NODE|16-lts' +param managedIdentity bool = !(empty(keyVaultName)) +param scmDoBuildDuringDeployment bool = false +param serviceName string + +module appService 'appservice.bicep' = { + name: '${serviceName}-appservice-node' + params: { + environmentName: environmentName + location: location + allowedOrigins: allowedOrigins + appCommandLine: appCommandLine + applicationInsightsName: applicationInsightsName + appServicePlanId: appServicePlanId + appSettings: appSettings + keyVaultName: keyVaultName + linuxFxVersion: linuxFxVersion + managedIdentity: managedIdentity + scmDoBuildDuringDeployment: scmDoBuildDuringDeployment + serviceName: serviceName + } +} + +output identityPrincipalId string = appService.outputs.identityPrincipalId +output name string = appService.outputs.name +output uri string = appService.outputs.uri diff --git a/infra/core/host/appservice-python.bicep b/infra/core/host/appservice-python.bicep new file mode 100644 index 000000000..767c1272a --- /dev/null +++ b/infra/core/host/appservice-python.bicep @@ -0,0 +1,35 @@ +param environmentName string +param location string = resourceGroup().location + +param allowedOrigins array = [] +param appCommandLine string = '' +param applicationInsightsName string = '' +param appServicePlanId string +param appSettings object = {} +param keyVaultName string = '' +param linuxFxVersion string = 'PYTHON|3.8' +param managedIdentity bool = !(empty(keyVaultName)) +param scmDoBuildDuringDeployment bool = true +param serviceName string + +module appService 'appservice.bicep' = { + name: '${serviceName}-appservice-python' + params: { + environmentName: environmentName + location: location + allowedOrigins: allowedOrigins + appCommandLine: appCommandLine + applicationInsightsName: applicationInsightsName + appServicePlanId: appServicePlanId + appSettings: appSettings + keyVaultName: keyVaultName + linuxFxVersion: linuxFxVersion + managedIdentity: managedIdentity + scmDoBuildDuringDeployment: scmDoBuildDuringDeployment + serviceName: serviceName + } +} + +output identityPrincipalId string = appService.outputs.identityPrincipalId +output name string = appService.outputs.name +output uri string = appService.outputs.uri diff --git a/infra/core/host/appservice.bicep b/infra/core/host/appservice.bicep new file mode 100644 index 000000000..3eece634a --- /dev/null +++ b/infra/core/host/appservice.bicep @@ -0,0 +1,100 @@ +param environmentName string +param location string = resourceGroup().location + +param allowedOrigins array = [] +param alwaysOn bool = true +param appCommandLine string = '' +param applicationInsightsName string = '' +param appServicePlanId string +param appSettings object = {} +param clientAffinityEnabled bool = false +param functionAppScaleLimit int = -1 +param keyVaultName string = '' +param kind string = 'app,linux' +param linuxFxVersion string = '' +param managedIdentity bool = !(empty(keyVaultName)) +param minimumElasticInstanceCount int = -1 +param numberOfWorkers int = -1 +param scmDoBuildDuringDeployment bool = false +param serviceName string +param use32BitWorkerProcess bool = false + +var abbrs = loadJsonContent('../../abbreviations.json') +var tags = { 'azd-env-name': environmentName } +var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) + +var prefix = contains(kind, 'function') ? abbrs.webSitesFunctions : abbrs.webSitesAppService + +resource appService 'Microsoft.Web/sites@2022-03-01' = { + name: '${prefix}${serviceName}-${resourceToken}' + location: location + tags: union(tags, { 'azd-service-name': serviceName }) + kind: kind + properties: { + serverFarmId: appServicePlanId + siteConfig: { + linuxFxVersion: linuxFxVersion + alwaysOn: alwaysOn + ftpsState: 'FtpsOnly' + appCommandLine: appCommandLine + numberOfWorkers: numberOfWorkers != -1 ? numberOfWorkers : null + minimumElasticInstanceCount: minimumElasticInstanceCount != -1 ? minimumElasticInstanceCount : null + use32BitWorkerProcess: use32BitWorkerProcess + functionAppScaleLimit: functionAppScaleLimit != -1 ? functionAppScaleLimit : null + cors: { + allowedOrigins: union([ 'https://portal.azure.com', 'https://ms.portal.azure.com' ], allowedOrigins) + } + } + clientAffinityEnabled: clientAffinityEnabled + httpsOnly: true + } + + identity: managedIdentity ? { type: 'SystemAssigned' } : null + + resource appSettings 'config' = { + name: 'appsettings' + properties: union({ + SCM_DO_BUILD_DURING_DEPLOYMENT: string(scmDoBuildDuringDeployment) + }, + !(empty(applicationInsightsName)) ? { APPLICATIONINSIGHTS_CONNECTION_STRING: applicationInsights.properties.ConnectionString } : {}, + !(empty(keyVaultName)) ? { AZURE_KEY_VAULT_ENDPOINT: keyVault.properties.vaultUri } : {}) + } +} + +module appSettingsUnion 'appservice-config-union.bicep' = if (!empty(appSettings)) { + name: '${serviceName}-app-settings-union' + params: { + appServiceName: appService.name + configName: 'appsettings' + currentConfigProperties: appService::appSettings.list().properties + additionalConfigProperties: appSettings + } +} + +module siteConfigLogs 'appservice-config-logs.bicep' = { + name: '${serviceName}-appservice-config-logs' + params: { + appServiceName: appService.name + } +} + +module keyVaultAccess '../security/keyvault-access.bicep' = if (!(empty(keyVaultName))) { + name: '${serviceName}-appservice-keyvault-access' + params: { + principalId: appService.identity.principalId + environmentName: environmentName + location: location + } +} + +resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = if (!(empty(keyVaultName))) { + name: keyVaultName +} + +resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = if (!(empty(applicationInsightsName))) { + name: applicationInsightsName +} + +output identityPrincipalId string = managedIdentity ? appService.identity.principalId : '' +output name string = appService.name +output uri string = 'https://${appService.properties.defaultHostName}' diff --git a/infra/core/host/appserviceplan-functions.bicep b/infra/core/host/appserviceplan-functions.bicep new file mode 100644 index 000000000..6693e6b8f --- /dev/null +++ b/infra/core/host/appserviceplan-functions.bicep @@ -0,0 +1,21 @@ +param environmentName string +param location string = resourceGroup().location + +param sku object = { + name: 'Y1' + tier: 'Dynamic' + size: 'Y1' + family: 'Y' +} + +module appServicePlanFunctions 'appserviceplan.bicep' = { + name: 'appserviceplan-functions' + params: { + environmentName: environmentName + location: location + sku: sku + kind: 'functionapp' + } +} + +output appServicePlanId string = appServicePlanFunctions.outputs.appServicePlanId diff --git a/infra/core/host/appserviceplan-sites.bicep b/infra/core/host/appserviceplan-sites.bicep new file mode 100644 index 000000000..bd3513740 --- /dev/null +++ b/infra/core/host/appserviceplan-sites.bicep @@ -0,0 +1,15 @@ +param environmentName string +param location string = resourceGroup().location + +param sku object = { name: 'B1' } + +module appServicePlanSites 'appserviceplan.bicep' = { + name: 'appserviceplan-sites' + params: { + environmentName: environmentName + location: location + sku: sku + } +} + +output appServicePlanId string = appServicePlanSites.outputs.appServicePlanId diff --git a/infra/core/host/appserviceplan.bicep b/infra/core/host/appserviceplan.bicep new file mode 100644 index 000000000..c72fcbda6 --- /dev/null +++ b/infra/core/host/appserviceplan.bicep @@ -0,0 +1,23 @@ +param environmentName string +param location string = resourceGroup().location + +param kind string = '' +param reserved bool = true +param sku object + +var abbrs = loadJsonContent('../../abbreviations.json') +var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) +var tags = { 'azd-env-name': environmentName } + +resource appServicePlan 'Microsoft.Web/serverfarms@2022-03-01' = { + name: '${abbrs.webServerFarms}${resourceToken}' + location: location + tags: tags + sku: sku + kind: kind + properties: { + reserved: reserved + } +} + +output appServicePlanId string = appServicePlan.id diff --git a/infra/core/host/container-app.bicep b/infra/core/host/container-app.bicep new file mode 100644 index 000000000..fdfcf6777 --- /dev/null +++ b/infra/core/host/container-app.bicep @@ -0,0 +1,79 @@ +param environmentName string +param location string = resourceGroup().location + +param containerAppsEnvironmentName string = '' +param containerRegistryName string = '' +param env array = [] +param external bool = true +param imageName string +param keyVaultName string = '' +param managedIdentity bool = !(empty(keyVaultName)) +param targetPort int = 80 +param serviceName string + +var abbrs = loadJsonContent('../../abbreviations.json') +var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) +var tags = { 'azd-env-name': environmentName } + +resource app 'Microsoft.App/containerApps@2022-03-01' = { + name: '${abbrs.appContainerApps}${serviceName}-${resourceToken}' + location: location + tags: union(tags, { 'azd-service-name': serviceName }) + identity: managedIdentity ? { type: 'SystemAssigned' } : null + properties: { + managedEnvironmentId: containerAppsEnvironment.id + configuration: { + activeRevisionsMode: 'single' + ingress: { + external: external + targetPort: targetPort + transport: 'auto' + } + secrets: [ + { + name: 'registry-password' + value: containerRegistry.listCredentials().passwords[0].value + } + ] + registries: [ + { + server: '${containerRegistry.name}.azurecr.io' + username: containerRegistry.name + passwordSecretRef: 'registry-password' + } + ] + } + template: { + containers: [ + { + image: imageName + name: 'main' + env: env + } + ] + } + } +} + +module keyVaultAccess '../security/keyvault-access.bicep' = if (!(empty(keyVaultName))) { + name: '${serviceName}-appservice-keyvault-access' + params: { + environmentName: environmentName + location: location + keyVaultName: keyVaultName + principalId: app.identity.principalId + } +} + +resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2022-03-01' existing = { + name: !empty(containerAppsEnvironmentName) ? containerAppsEnvironmentName : '${abbrs.appManagedEnvironments}${resourceToken}' +} + +// 2022-02-01-preview needed for anonymousPullEnabled +resource containerRegistry 'Microsoft.ContainerRegistry/registries@2022-02-01-preview' existing = { + name: !empty(containerRegistryName) ? containerRegistryName : '${abbrs.containerRegistryRegistries}${resourceToken}' +} + +output identityPrincipalId string = managedIdentity ? app.identity.principalId : '' +output name string = app.name +output uri string = 'https://${app.properties.configuration.ingress.fqdn}' diff --git a/infra/core/host/container-apps-environment.bicep b/infra/core/host/container-apps-environment.bicep new file mode 100644 index 000000000..b1e05786c --- /dev/null +++ b/infra/core/host/container-apps-environment.bicep @@ -0,0 +1,30 @@ +param environmentName string +param location string = resourceGroup().location + +param containerAppsEnvironmentName string = '' +param logAnalyticsWorkspaceName string + +var abbrs = loadJsonContent('../../abbreviations.json') +var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) +var tags = { 'azd-env-name': environmentName } + +resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2022-03-01' = { + name: !empty(containerAppsEnvironmentName) ? containerAppsEnvironmentName : '${abbrs.appManagedEnvironments}${resourceToken}' + location: location + tags: tags + properties: { + appLogsConfiguration: { + destination: 'log-analytics' + logAnalyticsConfiguration: { + customerId: logAnalyticsWorkspace.properties.customerId + sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey + } + } + } +} + +resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' existing = { + name: logAnalyticsWorkspaceName +} + +output containerAppsEnvironmentName string = containerAppsEnvironment.name diff --git a/infra/core/host/container-apps.bicep b/infra/core/host/container-apps.bicep new file mode 100644 index 000000000..a916c7f9f --- /dev/null +++ b/infra/core/host/container-apps.bicep @@ -0,0 +1,30 @@ +param environmentName string +param location string = resourceGroup().location + +param containerAppsEnvironmentName string = '' +param containerAppsGroupName string = 'app' +param containerRegistryName string = '' +param logAnalyticsWorkspaceName string = '' + +module containerAppsEnvironment 'container-apps-environment.bicep' = { + name: '${containerAppsGroupName}-container-apps-environment' + params: { + environmentName: environmentName + location: location + containerAppsEnvironmentName: containerAppsEnvironmentName + logAnalyticsWorkspaceName: logAnalyticsWorkspaceName + } +} + +module containerRegistry 'container-registry.bicep' = { + name: '${containerAppsGroupName}-container-registry' + params: { + environmentName: environmentName + location: location + containerRegistryName: containerRegistryName + } +} + +output containerAppsEnvironmentName string = containerAppsEnvironment.outputs.containerAppsEnvironmentName +output containerRegistryEndpoint string = containerRegistry.outputs.containerRegistryEndpoint +output containerRegistryName string = containerRegistry.outputs.containerRegistryName diff --git a/infra/core/host/container-registry.bicep b/infra/core/host/container-registry.bicep new file mode 100644 index 000000000..115da50d3 --- /dev/null +++ b/infra/core/host/container-registry.bicep @@ -0,0 +1,40 @@ +param environmentName string +param location string = resourceGroup().location + +param adminUserEnabled bool = true +param anonymousPullEnabled bool = false +param containerRegistryName string = '' +param dataEndpointEnabled bool = false +param encryption object = { + status: 'disabled' +} +param networkRuleBypassOptions string = 'AzureServices' +param publicNetworkAccess string = 'Enabled' +param sku object = { + name: 'Standard' +} +param zoneRedundancy string = 'Disabled' + +var abbrs = loadJsonContent('../../abbreviations.json') +var resourceToken = toLower(uniqueString(subscription().id, environmentName, location)) +var tags = { 'azd-env-name': environmentName } + +// 2022-02-01-preview needed for anonymousPullEnabled +resource containerRegistry 'Microsoft.ContainerRegistry/registries@2022-02-01-preview' = { + name: !empty(containerRegistryName) ? containerRegistryName : '${abbrs.containerRegistryRegistries}${resourceToken}' + location: location + tags: tags + sku: sku + properties: { + adminUserEnabled: adminUserEnabled + anonymousPullEnabled: anonymousPullEnabled + dataEndpointEnabled: dataEndpointEnabled + encryption: encryption + networkRuleBypassOptions: networkRuleBypassOptions + publicNetworkAccess: publicNetworkAccess + zoneRedundancy: zoneRedundancy + } +} + +output containerRegistryEndpoint string = containerRegistry.properties.loginServer +output containerRegistryName string = containerRegistry.name diff --git a/infra/core/host/functions-node.bicep b/infra/core/host/functions-node.bicep new file mode 100644 index 000000000..e4731f431 --- /dev/null +++ b/infra/core/host/functions-node.bicep @@ -0,0 +1,34 @@ +param environmentName string +param location string = resourceGroup().location + +param allowedOrigins array = [] +param applicationInsightsName string = '' +param appServicePlanId string +param appSettings object = {} +param keyVaultName string = '' +param linuxFxVersion string = 'NODE|16' +param managedIdentity bool = !(empty(keyVaultName)) +param serviceName string +param storageAccountName string + +module functions 'functions.bicep' = { + name: '${serviceName}-functions-node' + params: { + environmentName: environmentName + location: location + allowedOrigins: allowedOrigins + applicationInsightsName: applicationInsightsName + appServicePlanId: appServicePlanId + appSettings: appSettings + functionsWorkerRuntime: 'node' + keyVaultName: keyVaultName + linuxFxVersion: linuxFxVersion + managedIdentity: managedIdentity + serviceName: serviceName + storageAccountName: storageAccountName + } +} + +output identityPrincipalId string = functions.outputs.identityPrincipalId +output name string = functions.outputs.name +output uri string = functions.outputs.uri diff --git a/infra/core/host/functions-python.bicep b/infra/core/host/functions-python.bicep new file mode 100644 index 000000000..2ff79bd85 --- /dev/null +++ b/infra/core/host/functions-python.bicep @@ -0,0 +1,34 @@ +param environmentName string +param location string = resourceGroup().location + +param allowedOrigins array = [] +param applicationInsightsName string = '' +param appServicePlanId string +param appSettings object = {} +param keyVaultName string = '' +param linuxFxVersion string = 'PYTHON|3.8' +param managedIdentity bool = !(empty(keyVaultName)) +param serviceName string +param storageAccountName string + +module functions 'functions.bicep' = { + name: '${serviceName}-functions-python' + params: { + environmentName: environmentName + location: location + allowedOrigins: allowedOrigins + applicationInsightsName: applicationInsightsName + appServicePlanId: appServicePlanId + appSettings: appSettings + functionsWorkerRuntime: 'python' + keyVaultName: keyVaultName + linuxFxVersion: linuxFxVersion + managedIdentity: managedIdentity + serviceName: serviceName + storageAccountName: storageAccountName + } +} + +output identityPrincipalId string = functions.outputs.identityPrincipalId +output name string = functions.outputs.name +output uri string = functions.outputs.uri diff --git a/infra/core/host/functions.bicep b/infra/core/host/functions.bicep new file mode 100644 index 000000000..818485fcd --- /dev/null +++ b/infra/core/host/functions.bicep @@ -0,0 +1,58 @@ +param environmentName string +param location string = resourceGroup().location + +param allowedOrigins array = [] +param alwaysOn bool = false +param applicationInsightsName string = '' +param appServicePlanId string +param appSettings object = {} +param clientAffinityEnabled bool = false +param functionAppScaleLimit int = 200 +param functionsExtensionVersion string = '~4' +param functionsWorkerRuntime string +param kind string = 'functionapp,linux' +param linuxFxVersion string = '' +param keyVaultName string = '' +param managedIdentity bool = !(empty(keyVaultName)) +param minimumElasticInstanceCount int = 0 +param numberOfWorkers int = 1 +param scmDoBuildDuringDeployment bool = true +param serviceName string +param storageAccountName string +param use32BitWorkerProcess bool = false + +module functions 'appservice.bicep' = { + name: '${serviceName}-functions' + params: { + environmentName: environmentName + location: location + allowedOrigins: allowedOrigins + alwaysOn: alwaysOn + applicationInsightsName: applicationInsightsName + appServicePlanId: appServicePlanId + appSettings: union(appSettings, { + AzureWebJobsStorage: 'DefaultEndpointsProtocol=https;AccountName=${storage.name};AccountKey=${storage.listKeys().keys[0].value};EndpointSuffix=${environment().suffixes.storage}' + FUNCTIONS_EXTENSION_VERSION: functionsExtensionVersion + FUNCTIONS_WORKER_RUNTIME: functionsWorkerRuntime + }) + clientAffinityEnabled: clientAffinityEnabled + functionAppScaleLimit: functionAppScaleLimit + keyVaultName: keyVaultName + kind: kind + linuxFxVersion: linuxFxVersion + managedIdentity: managedIdentity + minimumElasticInstanceCount: minimumElasticInstanceCount + numberOfWorkers: numberOfWorkers + scmDoBuildDuringDeployment: scmDoBuildDuringDeployment + serviceName: serviceName + use32BitWorkerProcess: use32BitWorkerProcess + } +} + +resource storage 'Microsoft.Storage/storageAccounts@2021-09-01' existing = { + name: storageAccountName +} + +output identityPrincipalId string = managedIdentity ? functions.outputs.identityPrincipalId : '' +output name string = functions.outputs.name +output uri string = functions.outputs.uri diff --git a/infra/core/host/staticwebapp.bicep b/infra/core/host/staticwebapp.bicep new file mode 100644 index 000000000..3537fa9a7 --- /dev/null +++ b/infra/core/host/staticwebapp.bicep @@ -0,0 +1,25 @@ +param environmentName string +param location string = resourceGroup().location + +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: 'Custom' + } +} + +output name string = web.name +output uri string = 'https://${web.properties.defaultHostname}' diff --git a/infra/core/monitor/applicationinsights-dashboard.bicep b/infra/core/monitor/applicationinsights-dashboard.bicep new file mode 100644 index 000000000..11a6512b6 --- /dev/null +++ b/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/infra/core/monitor/applicationinsights.bicep b/infra/core/monitor/applicationinsights.bicep new file mode 100644 index 000000000..b9ce1fd7a --- /dev/null +++ b/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/infra/core/monitor/loganalytics.bicep b/infra/core/monitor/loganalytics.bicep new file mode 100644 index 000000000..a36912c54 --- /dev/null +++ b/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/infra/core/monitor/monitoring.bicep b/infra/core/monitor/monitoring.bicep new file mode 100644 index 000000000..1c5ae3e0e --- /dev/null +++ b/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/infra/core/security/keyvault-access.bicep b/infra/core/security/keyvault-access.bicep new file mode 100644 index 000000000..30c00f406 --- /dev/null +++ b/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/infra/core/security/keyvault.bicep b/infra/core/security/keyvault.bicep new file mode 100644 index 000000000..96b8e84f1 --- /dev/null +++ b/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/infra/core/storage/storage-account.bicep b/infra/core/storage/storage-account.bicep new file mode 100644 index 000000000..7d2eb91d9 --- /dev/null +++ b/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/infra/main.bicep b/infra/main.bicep new file mode 100644 index 000000000..a47a36f7d --- /dev/null +++ b/infra/main.bicep @@ -0,0 +1,46 @@ +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 = '' + +@secure() +@description('SQL Server administrator password') +param sqlAdminPassword string + +@secure() +@description('Application user password') +param appUserPassword 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 + sqlAdminPassword: sqlAdminPassword + appUserPassword: appUserPassword + } +} + +output AZURE_LOCATION string = location +output AZURE_TENANT_ID string = tenant().tenantId +output REACT_APP_WEB_BASE_URL string = resources.outputs.WEB_URI diff --git a/infra/main.parameters.json b/infra/main.parameters.json new file mode 100644 index 000000000..0ef1d9715 --- /dev/null +++ b/infra/main.parameters.json @@ -0,0 +1,21 @@ +{ + "$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}" + }, + "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/infra/resources.bicep b/infra/resources.bicep new file mode 100644 index 000000000..0eafb4443 --- /dev/null +++ b/infra/resources.bicep @@ -0,0 +1,82 @@ +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.bicep' = { + name: 'web' + params: { + environmentName: environmentName + location: location + appServicePlanId: appServicePlan.outputs.appServicePlanId + } +} + +// The application database: Catalog +module sqlServer1 './app/dbCatalog.bicep' = { + name: 'sqlCatalog' + params: { + environmentName: environmentName + location: location + sqlAdminPassword: sqlAdminPassword + appUserPassword: appUserPassword + keyVaultName: keyVault.outputs.keyVaultName + } +} + +// The application database: Identity +module sqlServer2 './app/dbIdentity.bicep' = { + name: 'sqlIdentity' + params: { + environmentName: environmentName + location: location + sqlAdminPassword: sqlAdminPassword + appUserPassword: appUserPassword + keyVaultName: keyVault.outputs.keyVaultName + } +} + +// Configure web to use sqlCatalog +module apiSqlServerConfig1 './core/host/appservice-config-sqlserver.bicep' = { + name: 'web-sqlserver-config-1' + params: { + appServiceName: web.outputs.WEB_NAME + sqlConnectionStringKey: sqlServer1.outputs.sqlConnectionStringKey + } +} + +// Configure web to use sqlIdentity +module apiSqlServerConfig2 './core/host/appservice-config-sqlserver.bicep' = { + name: 'web-sqlserver-config-2' + params: { + appServiceName: web.outputs.WEB_NAME + sqlConnectionStringKey: sqlServer2.outputs.sqlConnectionStringKey + } +} + +// Store secrets in a keyvault +module keyVault './core/security/keyvault.bicep' = { + name: 'keyvault' + params: { + environmentName: environmentName + location: location + principalId: principalId + } +} + +// Create an App Service Plan to group applications under the same payment plan and SKU +module appServicePlan './core/host/appserviceplan-sites.bicep' = { + name: 'appserviceplan' + params: { + environmentName: environmentName + location: location + } +} + +output WEB_URI string = web.outputs.WEB_URI