diff --git a/README.md b/README.md index 74a149c..729133f 100644 --- a/README.md +++ b/README.md @@ -94,13 +94,13 @@ Note: Take into account that sql database enforce [password complexity](https:// - You choose a valid resource group name ```bash - LOCATION=westus3 + LOCATION=westeurope BASE_NAME= RESOURCE_GROUP= az group create --location $LOCATION --resource-group $RESOURCE_GROUP - az deployment group create --template-file ./infra-as-code/bicep/main.bicep \ + az deployment group what-if --template-file ./infra-as-code/bicep/main.bicep \ --resource-group $RESOURCE_GROUP \ --parameters @./infra-as-code/bicep/parameters.json \ --parameters baseName=$BASE_NAME diff --git a/infra-as-code/bicep/database.bicep b/infra-as-code/bicep/database.bicep index 5235e34..ef714ff 100644 --- a/infra-as-code/bicep/database.bicep +++ b/infra-as-code/bicep/database.bicep @@ -1,5 +1,5 @@ /* - Deploy a SQL server with a sample database, a private endpoint and a private DNS zone + Deploy a SQL server with a database, a private endpoint and a private DNS zone */ @description('This is the base name for each Azure resource name (6-12 chars)') param baseName string @@ -9,29 +9,30 @@ param location string = resourceGroup().location @description('The administrator username of the SQL server') param sqlAdministratorLogin string + @description('The administrator password of the SQL server.') @secure() param sqlAdministratorLoginPassword string -// existing resource name params +// existing resource name params param vnetName string param privateEndpointsSubnetName string // variables var sqlServerName = 'sql-${baseName}' -var sampleSqlDatabaseName = 'sqldb-adventureworks' +var sqlDatabaseName = 'sqldb-${baseName}' var sqlPrivateEndpointName = 'pep-${sqlServerName}' var sqlDnsGroupName = '${sqlPrivateEndpointName}/default' var sqlDnsZoneName = 'privatelink${environment().suffixes.sqlServerHostname}' -var sqlConnectionString = 'Server=tcp:${sqlServerName}${environment().suffixes.sqlServerHostname},1433;Initial Catalog=${sampleSqlDatabaseName};Persist Security Info=False;User ID=${sqlAdministratorLogin};Password=${sqlAdministratorLoginPassword};MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;' +var sqlConnectionString = 'Server=tcp:${sqlServerName}${environment().suffixes.sqlServerHostname},1433;Initial Catalog=${sqlDatabaseName};Persist Security Info=False;User ID=${sqlAdministratorLogin};Password=${sqlAdministratorLoginPassword};MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;' // ---- Existing resources ---- -resource vnet 'Microsoft.Network/virtualNetworks@2022-11-01' existing = { +resource vnet 'Microsoft.Network/virtualNetworks@2022-11-01' existing = { name: vnetName resource privateEndpointsSubnet 'subnets' existing = { name: privateEndpointsSubnetName - } + } } // ---- Sql resources ---- @@ -53,7 +54,7 @@ resource sqlServer 'Microsoft.Sql/servers@2021-11-01' = { //database resource slqDatabase 'Microsoft.Sql/servers/databases@2021-11-01' = { - name: sampleSqlDatabaseName + name: sqlDatabaseName parent: sqlServer location: location @@ -63,12 +64,11 @@ resource slqDatabase 'Microsoft.Sql/servers/databases@2021-11-01' = { capacity: 5 } tags: { - displayName: sampleSqlDatabaseName + displayName: sqlDatabaseName } properties: { collation: 'SQL_Latin1_General_CP1_CI_AS' maxSizeBytes: 104857600 - sampleName: 'AdventureWorksLT' } } @@ -128,5 +128,5 @@ resource sqlServerDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZon ] } -@description('The connection string to the sample database.') +@description('The connection string to the database.') output sqlConnectionString string = sqlConnectionString diff --git a/infra-as-code/bicep/gateway.bicep b/infra-as-code/bicep/gateway.bicep index 0632929..4eedf99 100644 --- a/infra-as-code/bicep/gateway.bicep +++ b/infra-as-code/bicep/gateway.bicep @@ -8,16 +8,12 @@ param baseName string @description('The resource group location') param location string = resourceGroup().location -@description('Optional. When true will deploy a cost-optimised environment for development purposes.') -param developmentEnvironment bool - @description('Domain name to use for App Gateway') param customDomainName string -param availabilityZones array param gatewayCertSecretUri string -// existing resource name params +// existing resource name params param vnetName string param appGatewaySubnetName string param appName string @@ -29,12 +25,12 @@ var appGateWayName = 'agw-${baseName}' var appGatewayManagedIdentityName = 'id-${appGateWayName}' var appGatewayPublicIpName = 'pip-${baseName}' var appGateWayFqdn = 'fe-${baseName}' -var wafPolicyName= 'waf-${baseName}' +var wafPolicyName = 'waf-${baseName}' // ---- Existing resources ---- -resource vnet 'Microsoft.Network/virtualNetworks@2022-11-01' existing = { +resource vnet 'Microsoft.Network/virtualNetworks@2022-11-01' existing = { name: vnetName - + resource appGatewaySubnet 'subnets' existing = { name: appGatewaySubnetName } @@ -56,7 +52,7 @@ resource keyVaultSecretsUserRole 'Microsoft.Authorization/roleDefinitions@2022-0 // ---- App Gateway resources ---- -// Managed Identity for App Gateway. +// Managed Identity for App Gateway. resource appGatewayManagedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = { name: appGatewayManagedIdentityName location: location @@ -76,7 +72,6 @@ module appGatewaySecretsUserRoleAssignmentModule './modules/keyvaultRoleAssignme resource appGatewayPublicIp 'Microsoft.Network/publicIPAddresses@2022-11-01' = { name: appGatewayPublicIpName location: location - zones: !developmentEnvironment ? availabilityZones : null sku: { name: 'Standard' } @@ -121,7 +116,6 @@ resource wafPolicy 'Microsoft.Network/ApplicationGatewayWebApplicationFirewallPo resource appGateWay 'Microsoft.Network/applicationGateways@2022-11-01' = { name: appGateWayName location: location - zones: !developmentEnvironment ? availabilityZones : null identity: { type: 'UserAssigned' userAssignedIdentities: { @@ -134,17 +128,14 @@ resource appGateWay 'Microsoft.Network/applicationGateways@2022-11-01' = { tier: 'WAF_v2' } sslPolicy: { - policyType: 'Custom' - cipherSuites: [ - 'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384' - 'TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256' - ] - minProtocolVersion: 'TLSv1_2' + policyType: 'CustomV2' + cipherSuites: [] + minProtocolVersion: 'TLSv1_3' } gatewayIPConfigurations: [ { - name: 'appGatewayIpConfig' + name: 'app-gateway-ip-config' properties: { subnet: { id: vnet::appGatewaySubnet.id @@ -154,7 +145,7 @@ resource appGateWay 'Microsoft.Network/applicationGateways@2022-11-01' = { ] frontendIPConfigurations: [ { - name: 'appGwPublicFrontendIp' + name: 'app-gateway-public-ip' properties: { privateIPAllocationMethod: 'Dynamic' publicIPAddress: { @@ -173,10 +164,10 @@ resource appGateWay 'Microsoft.Network/applicationGateways@2022-11-01' = { ] probes: [ { - name: 'probe-web${baseName}' + name: 'probe-https-${baseName}' properties: { protocol: 'Https' - path: '/favicon.ico' + path: '/health' interval: 30 timeout: 30 unhealthyThreshold: 3 @@ -218,7 +209,7 @@ resource appGateWay 'Microsoft.Network/applicationGateways@2022-11-01' = { ] backendHttpSettingsCollection: [ { - name: 'WebAppBackendHttpSettings' + name: 'backend-https-${baseName}' properties: { port: 443 protocol: 'Https' @@ -226,26 +217,34 @@ resource appGateWay 'Microsoft.Network/applicationGateways@2022-11-01' = { pickHostNameFromBackendAddress: true requestTimeout: 20 probe: { - id: resourceId('Microsoft.Network/applicationGateways/probes', appGateWayName, 'probe-web${baseName}') + id: resourceId('Microsoft.Network/applicationGateways/probes', appGateWayName, 'probe-https-${baseName}') } } } ] httpListeners: [ { - name: 'WebAppListener' + name: 'listener-https-${baseName}' properties: { frontendIPConfiguration: { - id: resourceId('Microsoft.Network/applicationGateways/frontendIPConfigurations', appGateWayName, 'appGwPublicFrontendIp') + id: resourceId( + 'Microsoft.Network/applicationGateways/frontendIPConfigurations', + appGateWayName, + 'app-gateway-public-ip' + ) } frontendPort: { id: resourceId('Microsoft.Network/applicationGateways/frontendPorts', appGateWayName, 'port-443') } protocol: 'Https' sslCertificate: { - id: resourceId('Microsoft.Network/applicationGateways/sslCertificates', appGateWayName, '${appGateWayName}-ssl-certificate') + id: resourceId( + 'Microsoft.Network/applicationGateways/sslCertificates', + appGateWayName, + '${appGateWayName}-ssl-certificate' + ) } - hostName: 'www.${customDomainName}' + hostName: customDomainName hostNames: [] requireServerNameIndication: true } @@ -253,25 +252,37 @@ resource appGateWay 'Microsoft.Network/applicationGateways@2022-11-01' = { ] requestRoutingRules: [ { - name: 'WebAppRoutingRule' + name: 'https' properties: { ruleType: 'Basic' priority: 100 httpListener: { - id: resourceId('Microsoft.Network/applicationGateways/httpListeners', appGateWayName, 'WebAppListener') + id: resourceId( + 'Microsoft.Network/applicationGateways/httpListeners', + appGateWayName, + 'listener-https-${baseName}' + ) } backendAddressPool: { - id: resourceId('Microsoft.Network/applicationGateways/backendAddressPools', appGateWayName, 'pool-${appName}') + id: resourceId( + 'Microsoft.Network/applicationGateways/backendAddressPools', + appGateWayName, + 'pool-${appName}' + ) } backendHttpSettings: { - id: resourceId('Microsoft.Network/applicationGateways/backendHttpSettingsCollection', appGateWayName, 'WebAppBackendHttpSettings') + id: resourceId( + 'Microsoft.Network/applicationGateways/backendHttpSettingsCollection', + appGateWayName, + 'backend-https-${baseName}' + ) } } } ] autoscaleConfiguration: { - minCapacity: developmentEnvironment ? 2 : 3 - maxCapacity: developmentEnvironment ? 3 : 5 + minCapacity: 2 + maxCapacity: 3 } } dependsOn: [ @@ -281,7 +292,7 @@ resource appGateWay 'Microsoft.Network/applicationGateways@2022-11-01' = { // App Gateway diagnostics resource appGatewayDiagSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = { - name: '${appGateWay.name}-diagnosticSettings' + name: '${appGateWay.name}-diagnostic-settings' scope: appGateWay properties: { workspaceId: logWorkspace.id diff --git a/infra-as-code/bicep/main.bicep b/infra-as-code/bicep/main.bicep index 33c385c..8f64d1c 100644 --- a/infra-as-code/bicep/main.bicep +++ b/infra-as-code/bicep/main.bicep @@ -13,8 +13,11 @@ param sqlAdministratorLogin string @secure() param sqlAdministratorLoginPassword string +@description('Docker image to use for deployment') +param dockerImage string + @description('Domain name to use for App Gateway') -param customDomainName string = 'contoso.com' +param domainName string @description('The certificate data for app gateway TLS termination. The value is base64 encoded') @secure() @@ -23,14 +26,8 @@ param appGatewayListenerCertificate string @description('Optional. When true will deploy a cost-optimised environment for development purposes. Note that when this param is true, the deployment is not suitable or recommended for Production environments. Default = false.') param developmentEnvironment bool = false -@description('The name of the web deploy file. The file should reside in a deploy container in the storage account. Defaults to SimpleWebApp.zip') -param publishFileName string = 'SimpleWebApp.zip' - -// ---- Availability Zones ---- -var availabilityZones = [ '1', '2', '3' ] var logWorkspaceName = 'log-${baseName}' - // ---- Log Analytics workspace ---- resource logWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' = { name: logWorkspaceName @@ -49,18 +46,6 @@ module networkModule 'network.bicep' = { params: { location: location baseName: baseName - developmentEnvironment: developmentEnvironment - } -} - -// Deploy storage account with private endpoint and private DNS zone -module storageModule 'storage.bicep' = { - name: 'storageDeploy' - params: { - location: location - baseName: baseName - vnetName: networkModule.outputs.vnetNName - privateEndpointsSubnetName: networkModule.outputs.privateEndpointsSubnetName } } @@ -96,15 +81,14 @@ module webappModule 'webapp.bicep' = { params: { location: location baseName: baseName + dockerImage: dockerImage developmentEnvironment: developmentEnvironment - publishFileName: publishFileName keyVaultName: secretsModule.outputs.keyVaultName - storageName: storageModule.outputs.storageName vnetName: networkModule.outputs.vnetNName appServicesSubnetName: networkModule.outputs.appServicesSubnetName privateEndpointsSubnetName: networkModule.outputs.privateEndpointsSubnetName logWorkspaceName: logWorkspace.name - } + } } //Deploy an Azure Application Gateway with WAF v2 and a custom domain name. @@ -113,15 +97,12 @@ module gatewayModule 'gateway.bicep' = { params: { location: location baseName: baseName - developmentEnvironment: developmentEnvironment - availabilityZones: availabilityZones - customDomainName: customDomainName + customDomainName: domainName appName: webappModule.outputs.appName vnetName: networkModule.outputs.vnetNName appGatewaySubnetName: networkModule.outputs.appGatewaySubnetName keyVaultName: secretsModule.outputs.keyVaultName gatewayCertSecretUri: secretsModule.outputs.gatewayCertSecretUri logWorkspaceName: logWorkspace.name - } + } } - diff --git a/infra-as-code/bicep/network.bicep b/infra-as-code/bicep/network.bicep index 1c58637..96b1469 100644 --- a/infra-as-code/bicep/network.bicep +++ b/infra-as-code/bicep/network.bicep @@ -8,37 +8,21 @@ param baseName string @description('The resource group location') param location string = resourceGroup().location -param developmentEnvironment bool - // variables var vnetName = 'vnet-${baseName}' -var ddosPlanName = 'ddos-${baseName}' - -var vnetAddressPrefix = '10.0.0.0/16' -var appGatewaySubnetPrefix = '10.0.1.0/24' -var appServicesSubnetPrefix = '10.0.0.0/24' -var privateEndpointsSubnetPrefix = '10.0.2.0/27' -var agentsSubnetPrefix = '10.0.2.32/27' -//Temp disable DDoS protection -var enableDdosProtection = !developmentEnvironment +var vnetAddressPrefix = '172.18.48.0/20' +var appServicesSubnetPrefix = '172.18.48.0/24' +var appGatewaySubnetPrefix = '172.18.49.0/24' +var privateEndpointsSubnetPrefix = '172.18.50.0/24' // ---- Networking resources ---- -// DDoS Protection Plan -resource ddosProtectionPlan 'Microsoft.Network/ddosProtectionPlans@2022-11-01' = if (enableDdosProtection) { - name: ddosPlanName - location: location - properties: {} -} - //vnet and subnets resource vnet 'Microsoft.Network/virtualNetworks@2022-11-01' = { name: vnetName location: location properties: { - enableDdosProtection: enableDdosProtection - ddosProtectionPlan: enableDdosProtection ? { id: ddosProtectionPlan.id } : null addressSpace: { addressPrefixes: [ vnetAddressPrefix @@ -47,7 +31,7 @@ resource vnet 'Microsoft.Network/virtualNetworks@2022-11-01' = { subnets: [ { //App services plan subnet - name: 'snet-appServicePlan' + name: 'snet-app-service' properties: { addressPrefix: appServicesSubnetPrefix networkSecurityGroup: { @@ -65,7 +49,7 @@ resource vnet 'Microsoft.Network/virtualNetworks@2022-11-01' = { } { //App Gateway subnet - name: 'snet-appGateway' + name: 'snet-app-gateway' properties: { addressPrefix: appGatewaySubnetPrefix networkSecurityGroup: { @@ -77,7 +61,7 @@ resource vnet 'Microsoft.Network/virtualNetworks@2022-11-01' = { } { //Private endpoints subnet - name: 'snet-privateEndpoints' + name: 'snet-private-endpoints' properties: { addressPrefix: privateEndpointsSubnetPrefix networkSecurityGroup: { @@ -85,39 +69,25 @@ resource vnet 'Microsoft.Network/virtualNetworks@2022-11-01' = { } } } - { - // Build agents subnet - name: 'snet-agents' - properties: { - addressPrefix: agentsSubnetPrefix - networkSecurityGroup: { - id: agentsSubnetNsg.id - } - } - } ] } resource appGatewaySubnet 'subnets' existing = { - name: 'snet-appGateway' + name: 'snet-app-gateway' } resource appServiceSubnet 'subnets' existing = { - name: 'snet-appServicePlan' + name: 'snet-app-service' } - resource privateEnpointsSubnet 'subnets' existing = { - name: 'snet-privateEndpoints' + resource privateEndpointsSubnet 'subnets' existing = { + name: 'snet-private-endpoints' } - - resource agentsSubnet 'subnets' existing = { - name: 'snet-agents' - } } //App Gateway subnet NSG resource appGatewaySubnetNsg 'Microsoft.Network/networkSecurityGroups@2022-11-01' = { - name: 'nsg-appGatewaySubnet' + name: 'nsg-app-gateway-subnet' location: location properties: { securityRules: [ @@ -149,6 +119,20 @@ resource appGatewaySubnetNsg 'Microsoft.Network/networkSecurityGroups@2022-11-01 direction: 'Inbound' } } + { + name: 'AppGw.In.Allow8443.Internet' + properties: { + description: 'Allow ALL inbound web traffic on port 8443' + protocol: 'Tcp' + sourcePortRange: '*' + destinationPortRange: '8443' + sourceAddressPrefix: 'Internet' + destinationAddressPrefix: appGatewaySubnetPrefix + access: 'Allow' + priority: 111 + direction: 'Inbound' + } + } { name: 'AppGw.In.Allow.LoadBalancer' properties: { @@ -162,7 +146,7 @@ resource appGatewaySubnetNsg 'Microsoft.Network/networkSecurityGroups@2022-11-01 priority: 120 direction: 'Inbound' } - } + } { name: 'DenyAllInBound' properties: { @@ -175,7 +159,7 @@ resource appGatewaySubnetNsg 'Microsoft.Network/networkSecurityGroups@2022-11-01 priority: 1000 direction: 'Inbound' } - } + } { name: 'AppGw.Out.Allow.PrivateEndpoints' properties: { @@ -210,7 +194,7 @@ resource appGatewaySubnetNsg 'Microsoft.Network/networkSecurityGroups@2022-11-01 //App service subnet nsg resource appServiceSubnetNsg 'Microsoft.Network/networkSecurityGroups@2022-11-01' = { - name: 'nsg-appServicesSubnet' + name: 'nsg-app-service-subnet' location: location properties: { securityRules: [ @@ -248,7 +232,7 @@ resource appServiceSubnetNsg 'Microsoft.Network/networkSecurityGroups@2022-11-01 //Private endpoints subnets NSG resource privateEndpointsSubnetNsg 'Microsoft.Network/networkSecurityGroups@2022-11-01' = { - name: 'nsg-privateEndpointsSubnet' + name: 'nsg-private-endpoints-subnet' location: location properties: { securityRules: [ @@ -265,30 +249,6 @@ resource privateEndpointsSubnetNsg 'Microsoft.Network/networkSecurityGroups@2022 priority: 100 direction: 'Outbound' } - } - ] - } -} - -//Build agents subnets NSG -resource agentsSubnetNsg 'Microsoft.Network/networkSecurityGroups@2022-11-01' = { - name: 'nsg-agentsSubnet' - location: location - properties: { - securityRules: [ - { - name: 'DenyAllOutBound' - properties: { - description: 'Deny outbound traffic from the build agents subnet. Note: adjust rules as needed after adding resources to the subnet' - protocol: '*' - sourcePortRange: '*' - destinationPortRange: '*' - sourceAddressPrefix: appGatewaySubnetPrefix - destinationAddressPrefix: '*' - access: 'Deny' - priority: 1000 - direction: 'Outbound' - } } ] } @@ -304,4 +264,4 @@ output appServicesSubnetName string = vnet::appServiceSubnet.name output appGatewaySubnetName string = vnet::appGatewaySubnet.name @description('The name of the private endpoints subnet.') -output privateEndpointsSubnetName string = vnet::privateEnpointsSubnet.name +output privateEndpointsSubnetName string = vnet::privateEndpointsSubnet.name diff --git a/infra-as-code/bicep/parameters.json b/infra-as-code/bicep/parameters.json deleted file mode 100644 index 31be189..0000000 --- a/infra-as-code/bicep/parameters.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", - "contentVersion": "1.0.0.0", - "parameters": { - "baseName": { - "value": "" - }, - "sqlAdministratorLogin": { - "value": "" - }, - "sqlAdministratorLoginPassword": { - "value": "" - }, - "developmentEnvironment": { - "value": true - }, - "appGatewayListenerCertificate": { - "value": "[base64 cert data from $APP_GATEWAY_LISTENER_CERTIFICATE_APPSERV_BASELINE]" - } - } -} \ No newline at end of file diff --git a/infra-as-code/bicep/secrets.bicep b/infra-as-code/bicep/secrets.bicep index d403a69..e86e468 100644 --- a/infra-as-code/bicep/secrets.bicep +++ b/infra-as-code/bicep/secrets.bicep @@ -13,7 +13,7 @@ param location string = resourceGroup().location param appGatewayListenerCertificate string param sqlConnectionString string -// existing resource name params +// existing resource name params param vnetName string param privateEndpointsSubnetName string @@ -24,12 +24,12 @@ var keyVaultDnsGroupName = '${keyVaultPrivateEndpointName}/default' var keyVaultDnsZoneName = 'privatelink.vaultcore.azure.net' //Cannot use 'privatelink${environment().suffixes.keyvaultDns}', per https://github.com/Azure/bicep/issues/9708 // ---- Existing resources ---- -resource vnet 'Microsoft.Network/virtualNetworks@2022-11-01' existing = { +resource vnet 'Microsoft.Network/virtualNetworks@2022-11-01' existing = { name: vnetName resource privateEndpointsSubnet 'subnets' existing = { name: privateEndpointsSubnetName - } + } } // ---- Key Vault resources ---- @@ -126,7 +126,7 @@ resource sqlConnectionStringSecret 'Microsoft.KeyVault/vaults/secrets@2023-02-01 } @description('The name of the key vault account.') -output keyVaultName string= keyVault.name +output keyVaultName string = keyVault.name @description('Uri to the secret holding the cert.') output gatewayCertSecretUri string = keyVault::kvsGatewayPublicCert.properties.secretUri diff --git a/infra-as-code/bicep/storage.bicep b/infra-as-code/bicep/storage.bicep deleted file mode 100644 index e538317..0000000 --- a/infra-as-code/bicep/storage.bicep +++ /dev/null @@ -1,119 +0,0 @@ -/* - Deploy storage account with private endpoint and private DNS zone -*/ - -@description('This is the base name for each Azure resource name (6-12 chars)') -param baseName string - -@description('The resource group location') -param location string = resourceGroup().location - -// existing resource name params -param vnetName string -param privateEndpointsSubnetName string - -// variables -var storageName = 'st${baseName}' -var storageSkuName = 'Standard_LRS' -var storageDnsGroupName = '${storagePrivateEndpointName}/default' -var storagePrivateEndpointName = 'pep-${storageName}' -var blobStorageDnsZoneName = 'privatelink.blob.${environment().suffixes.storage}' - -// ---- Existing resources ---- -resource vnet 'Microsoft.Network/virtualNetworks@2022-11-01' existing = { - name: vnetName - - resource privateEndpointsSubnet 'subnets' existing = { - name: privateEndpointsSubnetName - } -} - -// ---- Storage resources ---- -resource storage 'Microsoft.Storage/storageAccounts@2022-09-01' = { - name: storageName - location: location - sku: { - name: storageSkuName - } - kind: 'StorageV2' - properties: { - accessTier: 'Hot' - allowBlobPublicAccess: false - allowSharedKeyAccess: false - encryption: { - keySource: 'Microsoft.Storage' - requireInfrastructureEncryption: false - services: { - blob: { - enabled: true - keyType: 'Account' - } - } - } - minimumTlsVersion: 'TLS1_2' - networkAcls: { - bypass: 'AzureServices' - defaultAction: 'Deny' - } - supportsHttpsTrafficOnly: true - } -} - -resource storagePrivateEndpoint 'Microsoft.Network/privateEndpoints@2022-11-01' = { - name: storagePrivateEndpointName - location: location - properties: { - subnet: { - id: vnet::privateEndpointsSubnet.id - } - privateLinkServiceConnections: [ - { - name: storagePrivateEndpointName - properties: { - groupIds: [ - 'blob' - ] - privateLinkServiceId: storage.id - } - } - ] - } -} - -resource storageDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = { - name: blobStorageDnsZoneName - location: 'global' - properties: {} -} - -resource storageDnsZoneLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = { - parent: storageDnsZone - name: '${blobStorageDnsZoneName}-link' - location: 'global' - properties: { - registrationEnabled: false - virtualNetwork: { - id: vnet.id - } - } -} - -resource storageDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2022-11-01' = { - name: storageDnsGroupName - properties: { - privateDnsZoneConfigs: [ - { - name: blobStorageDnsZoneName - properties: { - privateDnsZoneId: storageDnsZone.id - } - } - ] - } - dependsOn: [ - storagePrivateEndpoint - ] -} - -@description('The name of the storage account.') -output storageName string = storage.name diff --git a/infra-as-code/bicep/webapp.bicep b/infra-as-code/bicep/webapp.bicep index b2cfb04..7a96cf9 100644 --- a/infra-as-code/bicep/webapp.bicep +++ b/infra-as-code/bicep/webapp.bicep @@ -8,35 +8,33 @@ param baseName string @description('The resource group location') param location string = resourceGroup().location +param dockerImage string param developmentEnvironment bool -param publishFileName string -// existing resource name params +// existing resource name params param vnetName string param appServicesSubnetName string param privateEndpointsSubnetName string -param storageName string param keyVaultName string param logWorkspaceName string // variables var appName = 'app-${baseName}' -var appServicePlanName = 'asp-${appName}${uniqueString(subscription().subscriptionId)}' +var appServicePlanName = 'asp-${appName}' var appServiceManagedIdentityName = 'id-${appName}' -var packageLocation = 'https://${storageName}.blob.${environment().suffixes.storage}/deploy/${publishFileName}' var appServicePrivateEndpointName = 'pep-${appName}' -var appInsightsName= 'appinsights-${appName}' +var appInsightsName = 'appinsights-${appName}' var appServicePlanPremiumSku = 'Premium' -var appServicePlanStandardSku = 'Standard' +var appServicePlanBasicSku = 'Basic' var appServicePlanSettings = { - Standard: { - name: 'S1' + Basic: { + name: 'B3' capacity: 1 } Premium: { - name: 'P2v2' - capacity: 3 + name: 'P1V3' + capacity: 1 } } @@ -44,41 +42,31 @@ var appServicesDnsZoneName = 'privatelink.azurewebsites.net' var appServicesDnsGroupName = '${appServicePrivateEndpointName}/default' // ---- Existing resources ---- -resource vnet 'Microsoft.Network/virtualNetworks@2022-11-01' existing = { +resource vnet 'Microsoft.Network/virtualNetworks@2022-11-01' existing = { name: vnetName resource appServicesSubnet 'subnets' existing = { name: appServicesSubnetName - } + } resource privateEndpointsSubnet 'subnets' existing = { name: privateEndpointsSubnetName - } + } } -resource keyVault 'Microsoft.KeyVault/vaults@2023-02-01' existing = { +resource keyVault 'Microsoft.KeyVault/vaults@2023-02-01' existing = { name: keyVaultName } -resource storage 'Microsoft.Storage/storageAccounts@2022-09-01' existing = { - name: storageName -} - resource logWorkspace 'Microsoft.OperationalInsights/workspaces@2022-10-01' existing = { name: logWorkspaceName } -// Built-in Azure RBAC role that is applied to a Key Vault to grant secrets content read permissions. +// Built-in Azure RBAC role that is applied to a Key Vault to grant secrets content read permissions. resource keyVaultSecretsUserRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { name: '4633458b-17de-408a-b874-0445c86b69e6' scope: subscription() } -// Built-in Azure RBAC role that is applied to a Key storage to grant data reader permissions. -resource blobDataReaderRole 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = { - name: '2a2b9908-6ea1-4ae2-8e65-a410df84e7d1' - scope: subscription() -} - // ---- Web App resources ---- // Managed Identity for App Service @@ -97,33 +85,24 @@ module appServiceSecretsUserRoleAssignmentModule './modules/keyvaultRoleAssignme } } -// Grant the App Service managed identity storage data reader role permissions -resource blobDataReaderRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { - scope: storage - name: guid(resourceGroup().id, appServiceManagedIdentity.name, blobDataReaderRole.id) - properties: { - roleDefinitionId: blobDataReaderRole.id - principalType: 'ServicePrincipal' - principalId: appServiceManagedIdentity.properties.principalId - } -} - //App service plan -resource appServicePlan 'Microsoft.Web/serverfarms@2022-09-01' = { +resource appServicePlan 'Microsoft.Web/serverfarms@2024-04-01' = { name: appServicePlanName location: location - sku: developmentEnvironment ? appServicePlanSettings[appServicePlanStandardSku] : appServicePlanSettings[appServicePlanPremiumSku] + kind: 'linux' + sku: developmentEnvironment + ? appServicePlanSettings[appServicePlanBasicSku] + : appServicePlanSettings[appServicePlanPremiumSku] properties: { - zoneRedundant: !developmentEnvironment + reserved: true // Forces Linux OS } - kind: 'app' } // Web App -resource webApp 'Microsoft.Web/sites@2022-09-01' = { +resource webApp 'Microsoft.Web/sites@2024-04-01' = { name: appName location: location - kind: 'app' + kind: 'app,linux,container' identity: { type: 'UserAssigned' userAssignedIdentities: { @@ -132,30 +111,35 @@ resource webApp 'Microsoft.Web/sites@2022-09-01' = { } properties: { serverFarmId: appServicePlan.id + vnetRouteAllEnabled: true + vnetImagePullEnabled: false + vnetContentShareEnabled: true virtualNetworkSubnetId: vnet::appServicesSubnet.id httpsOnly: false keyVaultReferenceIdentity: appServiceManagedIdentity.id hostNamesDisabled: false siteConfig: { + linuxFxVersion: 'DOCKER|${dockerImage}' + acrUseManagedIdentityCreds: true + acrUserManagedIdentityID: appServiceManagedIdentity.id vnetRouteAllEnabled: true http20Enabled: true + minTlsVersion: '1.3' + minTlsCipherSuite: 'TLS_AES_256_GCM_SHA384' publicNetworkAccess: 'Disabled' alwaysOn: true } } dependsOn: [ appServiceSecretsUserRoleAssignmentModule - blobDataReaderRoleAssignment ] } // App Settings -resource appsettings 'Microsoft.Web/sites/config@2022-09-01' = { +resource appsettings 'Microsoft.Web/sites/config@2024-04-01' = { name: 'appsettings' parent: webApp properties: { - WEBSITE_RUN_FROM_PACKAGE: packageLocation - WEBSITE_RUN_FROM_PACKAGE_BLOB_MI_RESOURCE_ID: appServiceManagedIdentity.id AZURE_SQL_CONNECTIONSTRING: '@Microsoft.KeyVault(SecretUri=https://${keyVault.name}${environment().suffixes.keyvaultDns}/secrets/adWorksConnString)' APPINSIGHTS_INSTRUMENTATIONKEY: appInsights.properties.InstrumentationKey APPLICATIONINSIGHTS_CONNECTION_STRING: appInsights.properties.ConnectionString @@ -266,51 +250,6 @@ resource webAppDiagSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-pr } } -// App service plan auto scale settings -resource appServicePlanAutoScaleSettings 'Microsoft.Insights/autoscalesettings@2022-10-01' = { - name: '${appServicePlan.name}-autoscale' - location: location - properties: { - enabled: true - targetResourceUri: appServicePlan.id - profiles: [ - { - name: 'Scale out condition' - capacity: { - maximum: '5' - default: '1' - minimum: '1' - } - rules: [ - { - scaleAction: { - type: 'ChangeCount' - direction: 'Increase' - cooldown: 'PT5M' - value: '1' - } - metricTrigger: { - metricName: 'CpuPercentage' - metricNamespace: 'microsoft.web/serverfarms' - operator: 'GreaterThan' - timeAggregation: 'Average' - threshold: 70 - metricResourceUri: appServicePlan.id - timeWindow: 'PT10M' - timeGrain: 'PT1M' - statistic: 'Average' - } - } - ] - } - ] - } - dependsOn: [ - webApp - appServicePlanDiagSettings - ] -} - // create application insights resource resource appInsights 'Microsoft.Insights/components@2020-02-02' = { name: appInsightsName