From 7434d503923f89b6de231f91e353125bccb12d2f Mon Sep 17 00:00:00 2001 From: Sven Aelterman <17446043+SvenAelterman@users.noreply.github.com> Date: Tue, 12 Nov 2024 10:19:34 -0600 Subject: [PATCH] Support for integrating with existing networking; deployment script fixes for cron and time zones (#88) * Existing VNet use for WebApp, Storage, KV, MySQL * Update deploy.sh: using env vars for DB settings Using /home/site/ini for additional PHP INI * Updated deployment scripts * Use conditional access .? * Bicep linting * Make web app PE optional * Organize web app settings * Update default SKU to B1ms * Only MySQL version 8.0.21 supported * Export db env vars to /etc/environment for cron * Configure `date.timezone` in php.ini on startup * Support for time zone setting --- deploy.ps1 | 2 +- main-sample.bicepparam | 2 + main.bicep | 115 ++++++++++++++++++++++++---------- modules/kv/kv.bicep | 25 +++++--- modules/kv/main.bicep | 14 +++-- modules/networking/main.bicep | 1 + modules/networking/vnet.bicep | 64 ++++++++++--------- modules/sql/main.bicep | 13 ++-- modules/storage/main.bicep | 8 ++- modules/webapp/main.bicep | 18 ++++-- modules/webapp/webapp.bicep | 36 ++++++++--- scripts/bash/deploy.sh | 45 +++++++------ scripts/bash/install.sh | 2 +- scripts/bash/startup.sh | 15 ++++- 14 files changed, 244 insertions(+), 116 deletions(-) diff --git a/deploy.ps1 b/deploy.ps1 index 26cbb0d..081e5af 100644 --- a/deploy.ps1 +++ b/deploy.ps1 @@ -67,7 +67,7 @@ $DeploymentResult = New-AzDeployment @CmdLetParameters if ($DeploymentResult.ProvisioningState -eq 'Succeeded') { Write-Host "🔥 Deployment succeeded." - $DeploymentResult.Outputs + $DeploymentResult.Outputs | Format-Table -Property Key, @{Name = 'Value'; Expression = { $_.Value.Value } } } else { $DeploymentResult diff --git a/main-sample.bicepparam b/main-sample.bicepparam index 7a9930a..5c97e2c 100644 --- a/main-sample.bicepparam +++ b/main-sample.bicepparam @@ -40,3 +40,5 @@ param smtpFromEmailAddress = '' // This parameter is required to ensure the parameter file is valid, but should be blank so the password doesn't leak. // A new password is generated for each deployment and stored in Key Vault. param sqlPassword = '' + +param appServiceTimeZone = 'UTC' diff --git a/main.bicep b/main.bicep index c9290b3..dddcc61 100644 --- a/main.bicep +++ b/main.bicep @@ -43,6 +43,8 @@ param prerequisiteCommand string = '/home/startup.sh' param deploymentTime string = utcNow() +param enableAppServicePrivateEndpoint bool = true + @description('The password to use for the MySQL Flexible Server admin account \'sqladmin\'.') @secure() param sqlPassword string @@ -57,9 +59,30 @@ param smtpPort string = '' @description('The email address to use as the sender for outgoing emails.') param smtpFromEmailAddress string = '' +param existingPrivateDnsZonesResourceGroupId string = '' +param existingVirtualNetworkId string = '' + +param appServiceTimeZone string = 'UTC' + var sequenceFormatted = format('{0:00}', sequence) -var rgNamingStructure = replace(replace(replace(replace(replace(namingConvention, '{rtype}', 'rg'), '{workloadName}', '${workloadName}-{rgName}'), '{loc}', location), '{seq}', sequenceFormatted), '{env}', environment) -var vnetName = nameModule[0].outputs.shortName +var rgNamingStructure = replace( + replace( + replace( + replace(replace(namingConvention, '{rtype}', 'rg'), '{workloadName}', '${workloadName}-{rgName}'), + '{loc}', + location + ), + '{seq}', + sequenceFormatted + ), + '{env}', + environment +) +// The name of the VNet is either a new name or the name of the existing VNet parsed from the resource ID +var vnetName = empty(existingVirtualNetworkId) + ? nameModule[0].outputs.shortName + : split(existingVirtualNetworkId, '/')[8] + var strgName = nameModule[1].outputs.shortName var webAppName = nameModule[2].outputs.shortName var kvName = nameModule[3].outputs.shortName @@ -71,8 +94,10 @@ var lawName = nameModule[8].outputs.shortName var deploymentNameStructure = '${workloadName}-${environment}-${sequenceFormatted}-{rtype}-${deploymentTime}' -var subnets = { +// TODO: Define type +param subnets object = { // TODO: Define securityRules + // TODO: Add existingSubnetName property for existing subnet PrivateLinkSubnet: { addressPrefix: cidrSubnet(vnetAddressSpace, 27, 0) serviceEndpoints: [ @@ -185,18 +210,20 @@ var resourceTypes = [ ] @batchSize(1) -module nameModule 'modules/common/createValidAzResourceName.bicep' = [for workload in resourceTypes: { - name: take(replace(deploymentNameStructure, '{rtype}', 'nameGen-${workload}'), 64) - params: { - location: location - environment: environment - namingConvention: namingConvention - resourceType: workload - sequence: sequence - workloadName: workloadName - addRandomChars: 4 +module nameModule 'modules/common/createValidAzResourceName.bicep' = [ + for workload in resourceTypes: { + name: take(replace(deploymentNameStructure, '{rtype}', 'nameGen-${workload}'), 64) + params: { + location: location + environment: environment + namingConvention: namingConvention + resourceType: workload + sequence: sequence + workloadName: workloadName + addRandomChars: 4 + } } -}] +] module rolesModule './modules/common/roles.bicep' = { name: take(replace(deploymentNameStructure, '{rtype}', 'roles'), 64) @@ -205,7 +232,7 @@ module rolesModule './modules/common/roles.bicep' = { var storageAccountKeySecretName = 'storageKey' // The secrets object is converted to an array using the items() function, which alphabetically sorts it var defaultSecretNames = map(items(secrets), s => s.key) -var additionalSecretNames = [ storageAccountKeySecretName ] +var additionalSecretNames = [storageAccountKeySecretName] var secretNames = concat(defaultSecretNames, additionalSecretNames) // The output will be in alphabetical order @@ -218,7 +245,7 @@ module kvSecretReferencesModule './modules/common/appSvcKeyVaultRefs.bicep' = { } } -module virtualNetworkModule './modules/networking/main.bicep' = { +module virtualNetworkModule './modules/networking/main.bicep' = if (empty(existingVirtualNetworkId)) { name: take(replace(deploymentNameStructure, '{rtype}', 'network'), 64) params: { resourceGroupName: replace(rgNamingStructure, '{rgName}', 'network') @@ -254,18 +281,29 @@ module monitoring './modules/monitoring/main.bicep' = { } } +var privateEndpointSubnetId = empty(existingVirtualNetworkId) + ? virtualNetworkModule.outputs.subnets.PrivateLinkSubnet.id + : '${existingVirtualNetworkId}/subnets/${subnets.PrivateLinkSubnet.existingSubnetName}' + +var virtualNetworkId = empty(existingVirtualNetworkId) + ? virtualNetworkModule.outputs.virtualNetworkId + : existingVirtualNetworkId + module storageAccountModule './modules/storage/main.bicep' = { name: take(replace(deploymentNameStructure, '{rtype}', 'storage'), 64) params: { resourceGroupName: replace(rgNamingStructure, '{rgName}', 'storage') location: location storageAccountName: strgName - peSubnetId: virtualNetworkModule.outputs.subnets.PrivateLinkSubnet.id + peSubnetId: privateEndpointSubnetId storageContainerName: 'redcap' kind: 'StorageV2' storageAccountSku: 'Standard_LRS' - virtualNetworkId: virtualNetworkModule.outputs.virtualNetworkId + + virtualNetworkId: virtualNetworkId privateDnsZoneName: 'privatelink.blob.${az.environment().suffixes.storage}' + existingPrivateDnsZonesResourceGroupId: existingPrivateDnsZonesResourceGroupId + tags: tags customTags: { workloadType: 'storageAccount' @@ -288,8 +326,9 @@ module keyVaultModule './modules/kv/main.bicep' = { customTags: { workloadType: 'keyVault' } - peSubnetId: virtualNetworkModule.outputs.subnets.PrivateLinkSubnet.id - virtualNetworkId: virtualNetworkModule.outputs.virtualNetworkId + peSubnetId: privateEndpointSubnetId + virtualNetworkId: virtualNetworkId + existingPrivateDnsZonesResourceGroupId: existingPrivateDnsZonesResourceGroupId roleAssignments: [ { RoleDefinitionId: rolesModule.outputs.roles['Key Vault Administrator'] @@ -298,6 +337,7 @@ module keyVaultModule './modules/kv/main.bicep' = { { RoleDefinitionId: rolesModule.outputs.roles['Key Vault Secrets User'] objectId: uamiModule.outputs.principalId + principtalType: 'ServicePrincipal' } ] privateDnsZoneName: 'privatelink.vaultcore.azure.net' @@ -318,12 +358,15 @@ module mySqlModule './modules/sql/main.bicep' = { customTags: { workloadType: 'mySqlFlexibleServer' } - skuName: 'Standard_B1s' + skuName: 'Standard_B1ms' SkuTier: 'Burstable' StorageSizeGB: 20 StorageIops: 396 - peSubnetId: virtualNetworkModule.outputs.subnets.MySQLFlexSubnet.id + peSubnetId: empty(existingVirtualNetworkId) + ? virtualNetworkModule.outputs.subnets.MySQLFlexSubnet.id + : '${existingVirtualNetworkId}/subnets/${subnets.MySQLFlexSubnet.existingSubnetName}' privateDnsZoneName: 'privatelink.mysql.database.azure.com' + existingPrivateDnsZonesResourceGroupId: existingPrivateDnsZonesResourceGroupId sqlAdminUser: sqlAdmin sqlAdminPasword: sqlPassword mysqlVersion: '8.0.21' @@ -341,7 +384,7 @@ module mySqlModule './modules/sql/main.bicep' = { database_charset: 'utf8' database_collation: 'utf8_general_ci' - virtualNetworkId: virtualNetworkModule.outputs.virtualNetworkId + virtualNetworkId: virtualNetworkId deploymentNameStructure: deploymentNameStructure } @@ -351,8 +394,8 @@ resource webAppResourceGroup 'Microsoft.Resources/resourceGroups@2023-07-01' = { name: replace(rgNamingStructure, '{rgName}', 'web') location: location tags: union(tags, { - workloadType: 'web' - }) + workloadType: 'web' + }) } module webAppModule './modules/webapp/main.bicep' = { @@ -362,10 +405,9 @@ module webAppModule './modules/webapp/main.bicep' = { webAppName: webAppName appServicePlanName: planName location: location - // TODO: Consider deploying as P0V3 to ensure the deployment runs on a scale unit that supports P_v3 for future upgrades. GH issue #50 - skuName: 'S1' - skuTier: 'Standard' - peSubnetId: virtualNetworkModule.outputs.subnets.PrivateLinkSubnet.id + // Deploy as P0V3 to ensure the deployment runs on a scale unit that supports P_v3 for future upgrades. GH issue #50 + skuName: 'P0V3' + peSubnetId: privateEndpointSubnetId appInsights_connectionString: monitoring.outputs.appInsightsResourceId appInsights_instrumentationKey: monitoring.outputs.appInsightsInstrumentationKey linuxFxVersion: 'php|8.2' @@ -373,8 +415,11 @@ module webAppModule './modules/webapp/main.bicep' = { customTags: { workloadType: 'webApp' } + + existingPrivateDnsZonesResourceGroupId: existingPrivateDnsZonesResourceGroupId privateDnsZoneName: 'privatelink.azurewebsites.net' - virtualNetworkId: virtualNetworkModule.outputs.virtualNetworkId + virtualNetworkId: virtualNetworkId + redcapZipUrl: redcapZipUrl dbHostName: mySqlModule.outputs.fqdn dbName: mySqlModule.outputs.databaseName @@ -390,7 +435,9 @@ module webAppModule './modules/webapp/main.bicep' = { storageAccountName: storageAccountModule.outputs.name // Enable VNet integration - integrationSubnetId: virtualNetworkModule.outputs.subnets.IntegrationSubnet.id + integrationSubnetId: empty(existingVirtualNetworkId) + ? virtualNetworkModule.outputs.subnets.IntegrationSubnet.id + : '${existingVirtualNetworkId}/subnets/${subnets.IntegrationSubnet.existingSubnetName}' scmRepoUrl: scmRepoUrl scmRepoBranch: scmRepoBranch @@ -403,6 +450,10 @@ module webAppModule './modules/webapp/main.bicep' = { deploymentNameStructure: deploymentNameStructure uamiId: uamiModule.outputs.id + + enablePrivateEndpoint: enableAppServicePrivateEndpoint + + timeZone: appServiceTimeZone } } @@ -416,5 +467,5 @@ module uamiModule 'modules/uami/main.bicep' = { } } -// The web app URL +// // The web app URL output webAppUrl string = webAppModule.outputs.webAppUrl diff --git a/modules/kv/kv.bicep b/modules/kv/kv.bicep index 5eb5e2f..f834bb8 100644 --- a/modules/kv/kv.bicep +++ b/modules/kv/kv.bicep @@ -11,10 +11,12 @@ param peSubnetId string param deploymentNameStructure string -param roleAssignments array = [ { +param roleAssignments array = [ + { RoleDefinitionId: '' objectId: '' - } ] + } +] param privateDnsZoneId string @secure() @@ -60,14 +62,19 @@ module keyVaultSecretsModule 'kvSecrets.bicep' = { } } -resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for roleAssignment in roleAssignments: { - scope: keyVault - name: guid(keyVault.id, roleAssignment.objectId, roleAssignment.RoleDefinitionId) - properties: { - roleDefinitionId: roleAssignment.RoleDefinitionId - principalId: roleAssignment.objectId +resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = [ + for roleAssignment in roleAssignments: { + scope: keyVault + name: guid(keyVault.id, roleAssignment.objectId, roleAssignment.RoleDefinitionId) + properties: { + roleDefinitionId: roleAssignment.RoleDefinitionId + principalId: roleAssignment.objectId + principalType: contains(roleAssignment, 'principalType') && !empty(roleAssignment.principalType) + ? roleAssignment.principalType + : null + } } -}] +] resource pekeyVault 'Microsoft.Network/privateEndpoints@2022-07-01' = { name: 'pe-${keyVaultName}' diff --git a/modules/kv/main.bicep b/modules/kv/main.bicep index 39b00f4..937b838 100644 --- a/modules/kv/main.bicep +++ b/modules/kv/main.bicep @@ -6,15 +6,19 @@ param tags object param customTags object param keyVaultName string param peSubnetId string -param roleAssignments array = [ { +param roleAssignments array = [ + { RoleDefinitionId: '' objectId: '' - } ] + } +] @secure() param secrets object param privateDnsZoneName string param virtualNetworkId string +param existingPrivateDnsZonesResourceGroupId string = '' + param deploymentNameStructure string var mergeTags = union(tags, customTags) @@ -33,14 +37,16 @@ module keyVaultModule './kv.bicep' = { location: location tags: tags peSubnetId: peSubnetId - privateDnsZoneId: keyVaultPrivateDnsModule.outputs.privateDnsId + privateDnsZoneId: empty(existingPrivateDnsZonesResourceGroupId) + ? keyVaultPrivateDnsModule.outputs.privateDnsId + : '${existingPrivateDnsZonesResourceGroupId}/providers/Microsoft.Network/privateDnsZones/${privateDnsZoneName}' secrets: secrets roleAssignments: roleAssignments deploymentNameStructure: deploymentNameStructure } } -module keyVaultPrivateDnsModule '../pdns/main.bicep' = { +module keyVaultPrivateDnsModule '../pdns/main.bicep' = if (empty(existingPrivateDnsZonesResourceGroupId)) { name: take(replace(deploymentNameStructure, '{rtype}', 'kv-dns'), 64) scope: resourceGroup params: { diff --git a/modules/networking/main.bicep b/modules/networking/main.bicep index 7b6fad2..32dc21f 100644 --- a/modules/networking/main.bicep +++ b/modules/networking/main.bicep @@ -34,3 +34,4 @@ module vNetModule 'vnet.bicep' = { output virtualNetworkId string = vNetModule.outputs.virtualNetworkId output subnets object = reduce(vNetModule.outputs.subnets, {}, (cur, next) => union(cur, next)) +output resourceGroupId string = resourceGroup.id diff --git a/modules/networking/vnet.bicep b/modules/networking/vnet.bicep index 257979a..356e96c 100644 --- a/modules/networking/vnet.bicep +++ b/modules/networking/vnet.bicep @@ -25,21 +25,25 @@ resource virtualNetwork 'Microsoft.Network/virtualNetworks@2021-05-01' = { vnetAddressPrefix ] } - subnets: [for (subnet, i) in subnetDefsArray: { - name: subnet.key - properties: { - addressPrefix: subnet.value.addressPrefix - serviceEndpoints: contains(subnet.value, 'serviceEndpoints') ? subnet.value.serviceEndpoints : null - delegations: contains(subnet.value, 'delegation') && !empty(subnet.value.delegation) ? [ - { - name: 'delegation' - properties: { - serviceName: subnet.value.delegation - } - } - ] : null + subnets: [ + for (subnet, i) in subnetDefsArray: { + name: subnet.key + properties: { + addressPrefix: subnet.value.addressPrefix + serviceEndpoints: subnet.value.?serviceEndpoints + delegations: contains(subnet.value, 'delegation') && !empty(subnet.value.delegation) + ? [ + { + name: 'delegation' + properties: { + serviceName: subnet.value.delegation + } + } + ] + : null + } } - }] + ] dhcpOptions: { dnsServers: customDnsIPs @@ -53,19 +57,23 @@ output virtualNetworkId string = virtualNetwork.id // Retrieve the subnets as an array of existing resources // This is important because we need to ensure subnet return value is matched to the name of the subnet correctly - order matters // This works because the parent property is set to the virtual network, which means this won't be attempted until the VNet is created -resource subnetRes 'Microsoft.Network/virtualNetworks/subnets@2022-05-01' existing = [for subnet in subnetDefsArray: { - name: subnet.key - parent: virtualNetwork -}] +resource subnetRes 'Microsoft.Network/virtualNetworks/subnets@2022-05-01' existing = [ + for subnet in subnetDefsArray: { + name: subnet.key + parent: virtualNetwork + } +] -output subnets array = [for i in range(0, length((subnetDefsArray))): { - '${subnetRes[i].name}': { - id: subnetRes[i].id - addressPrefix: subnetRes[i].properties.addressPrefix - // routeTableId: contains(subnetRes[i].properties, 'routeTable') ? subnetRes[i].properties.routeTable.id : null - // routeTableName: contains(subnetRes[i].properties, 'routeTable') ? routeTables[subnetRes[i].name].name : null - // networkSecurityGroupId: contains(subnetRes[i].properties, 'networkSecurityGroup') ? subnetRes[i].properties.networkSecurityGroup.id : null - // networkSecurityGroupName: contains(subnetRes[i].properties, 'networkSecurityGroup') ? networkSecurityGroups[subnetRes[i].name].name : null - // Add as many additional subnet properties as needed downstream +output subnets array = [ + for i in range(0, length((subnetDefsArray))): { + '${subnetRes[i].name}': { + id: subnetRes[i].id + addressPrefix: subnetRes[i].properties.addressPrefix + // routeTableId: contains(subnetRes[i].properties, 'routeTable') ? subnetRes[i].properties.routeTable.id : null + // routeTableName: contains(subnetRes[i].properties, 'routeTable') ? routeTables[subnetRes[i].name].name : null + // networkSecurityGroupId: contains(subnetRes[i].properties, 'networkSecurityGroup') ? subnetRes[i].properties.networkSecurityGroup.id : null + // networkSecurityGroupName: contains(subnetRes[i].properties, 'networkSecurityGroup') ? networkSecurityGroups[subnetRes[i].name].name : null + // Add as many additional subnet properties as needed downstream + } } -}] +] diff --git a/modules/sql/main.bicep b/modules/sql/main.bicep index da3a0b6..9fb2920 100644 --- a/modules/sql/main.bicep +++ b/modules/sql/main.bicep @@ -11,15 +11,14 @@ param privateDnsZoneName string param sqlAdminUser string param virtualNetworkId string +param existingPrivateDnsZonesResourceGroupId string = '' + param roles object param deploymentScriptName string @description('MySQL version') @allowed([ - // TODO: Remove 5.7 - '5.7' '8.0.21' - //'8.0.32' ]) param mysqlVersion string = '8.0.21' @@ -27,7 +26,7 @@ param mysqlVersion string = '8.0.21' param sqlAdminPasword string @description('Azure database for MySQL sku name ') -param skuName string = 'Standard_B1s' +param skuName string = 'Standard_B1ms' @description('Azure database for MySQL pricing tier') @allowed([ @@ -72,7 +71,9 @@ module mysqlDbserver './sql.bicep' = { StorageSizeGB: StorageSizeGB StorageIops: StorageIops peSubnetId: peSubnetId - privateDnsZoneId: privateDns.outputs.privateDnsId + privateDnsZoneId: empty(existingPrivateDnsZonesResourceGroupId) + ? privateDns.outputs.privateDnsId + : '${existingPrivateDnsZonesResourceGroupId}/providers/Microsoft.Network/privateDnsZones/${privateDnsZoneName}' adminUserName: sqlAdminUser adminPassword: sqlAdminPasword mysqlVersion: mysqlVersion @@ -87,7 +88,7 @@ module mysqlDbserver './sql.bicep' = { } } -module privateDns '../pdns/main.bicep' = { +module privateDns '../pdns/main.bicep' = if (empty(existingPrivateDnsZonesResourceGroupId)) { name: take(replace(deploymentNameStructure, '{rtype}', 'mysql-dns'), 64) scope: resourceGroup params: { diff --git a/modules/storage/main.bicep b/modules/storage/main.bicep index 3527ab2..72f559d 100644 --- a/modules/storage/main.bicep +++ b/modules/storage/main.bicep @@ -12,6 +12,8 @@ param virtualNetworkId string param tags object param customTags object +param existingPrivateDnsZonesResourceGroupId string + param deploymentNameStructure string @description('Resource ID of the Key Vault where the storage key secret should be created.') @@ -38,14 +40,16 @@ module storageAccount './storage.bicep' = { storageContainerName: storageContainerName kind: kind storageAccountSku: storageAccountSku - privateDnsZoneId: privateDns.outputs.privateDnsId + privateDnsZoneId: empty(existingPrivateDnsZonesResourceGroupId) + ? privateDns.outputs.privateDnsId + : '${existingPrivateDnsZonesResourceGroupId}/providers/Microsoft.Network/privateDnsZones/${privateDnsZoneName}' keyVaultId: keyVaultId keyVaultSecretName: keyVaultSecretName deploymentNameStructure: deploymentNameStructure } } -module privateDns '../pdns/main.bicep' = { +module privateDns '../pdns/main.bicep' = if (empty(existingPrivateDnsZonesResourceGroupId)) { name: take(replace(deploymentNameStructure, '{rtype}', 'st-dns'), 64) scope: resourceGroup params: { diff --git a/modules/webapp/main.bicep b/modules/webapp/main.bicep index 019f90a..59542a1 100644 --- a/modules/webapp/main.bicep +++ b/modules/webapp/main.bicep @@ -3,7 +3,6 @@ param location string = resourceGroup().location param webAppName string param appServicePlanName string param skuName string -param skuTier string param linuxFxVersion string = 'php|8.2' param dbHostName string #disable-next-line secure-secrets-in-params @@ -28,6 +27,8 @@ param storageAccountContainerName string param appInsights_connectionString string param appInsights_instrumentationKey string +param enablePrivateEndpoint bool + param scmRepoUrl string param scmRepoBranch string @secure() @@ -38,6 +39,10 @@ param redcapCommunityUsernameSecretRef string param redcapCommunityPasswordSecretRef string param prerequisiteCommand string +param existingPrivateDnsZonesResourceGroupId string = '' + +param timeZone string = 'UTC' + param uamiId string // Disabling this check because this is not a secret; it's a reference to Key Vault @@ -55,7 +60,6 @@ module appService 'webapp.bicep' = { appServicePlanName: appServicePlanName location: location skuName: skuName - skuTier: skuTier linuxFxVersion: linuxFxVersion tags: mergeTags dbHostName: dbHostName @@ -63,7 +67,9 @@ module appService 'webapp.bicep' = { dbPasswordSecretRef: dbPasswordSecretRef dbUserNameSecretRef: dbUserNameSecretRef peSubnetId: peSubnetId - privateDnsZoneId: privateDns.outputs.privateDnsId + privateDnsZoneId: empty(existingPrivateDnsZonesResourceGroupId) + ? privateDns.outputs.privateDnsId + : '${existingPrivateDnsZonesResourceGroupId}/providers/Microsoft.Network/privateDnsZones/${privateDnsZoneName}' integrationSubnetId: integrationSubnetId appInsights_connectionString: appInsights_connectionString @@ -86,10 +92,14 @@ module appService 'webapp.bicep' = { smtpPort: smtpPort uamiId: uamiId + + enablePrivateEndpoint: enablePrivateEndpoint + + timeZone: timeZone } } -module privateDns '../pdns/main.bicep' = { +module privateDns '../pdns/main.bicep' = if (empty(existingPrivateDnsZonesResourceGroupId)) { name: take(replace(deploymentNameStructure, '{rtype}', 'app-dns'), 64) params: { privateDnsZoneName: privateDnsZoneName diff --git a/modules/webapp/webapp.bicep b/modules/webapp/webapp.bicep index 2888c4e..9a8c574 100644 --- a/modules/webapp/webapp.bicep +++ b/modules/webapp/webapp.bicep @@ -2,7 +2,6 @@ param webAppName string param appServicePlanName string param location string param skuName string -param skuTier string param tags object param linuxFxVersion string @@ -29,10 +28,14 @@ param prerequisiteCommand string param appInsights_connectionString string param appInsights_instrumentationKey string +param enablePrivateEndpoint bool + param smtpFQDN string = '' param smtpPort string = '' param smtpFromEmailAddress string = '' +param timeZone string = 'UTC' + // This is not a secret, it's a Key Vault reference #disable-next-line secure-secrets-in-params param storageAccountKeySecretRef string @@ -48,7 +51,7 @@ resource appSrvcPlan 'Microsoft.Web/serverfarms@2022-03-01' = { tags: tags sku: { name: skuName - tier: skuTier + //tier: skuTier } kind: 'linux' properties: { @@ -76,10 +79,7 @@ resource webApp 'Microsoft.Web/sites@2022-03-01' = { ftpsState: 'FtpsOnly' appCommandLine: prerequisiteCommand appSettings: [ - { - name: 'redcapAppZip' - value: redcapZipUrl - } + // REDCap runtime settings { name: 'DBHostName' value: dbHostName @@ -96,6 +96,11 @@ resource webApp 'Microsoft.Web/sites@2022-03-01' = { name: 'DBPassword' value: dbPasswordSecretRef } + // REDCap deployment settings + { + name: 'redcapAppZip' + value: redcapZipUrl + } { name: 'redcapCommunityUsername' value: redcapCommunityUsernameSecretRef @@ -108,6 +113,7 @@ resource webApp 'Microsoft.Web/sites@2022-03-01' = { name: 'DBSslCa' value: DBSslCa } + // SMTP, possibly legacy settings { name: 'smtpFQDN' value: smtpFQDN @@ -120,6 +126,7 @@ resource webApp 'Microsoft.Web/sites@2022-03-01' = { name: 'fromEmailAddress' value: smtpFromEmailAddress } + // END SMTP { name: 'APPINSIGHTS_INSTRUMENTATIONKEY' value: appInsights_instrumentationKey @@ -132,6 +139,7 @@ resource webApp 'Microsoft.Web/sites@2022-03-01' = { name: 'SCM_DO_BUILD_DURING_DEPLOYMENT' value: '1' } + // EDOC configuration, used during deployment only { name: 'StorageKey' value: storageAccountKeySecretRef @@ -144,10 +152,20 @@ resource webApp 'Microsoft.Web/sites@2022-03-01' = { name: 'StorageContainerName' value: storageAccountContainerName } + // END EDOC { name: 'ENABLE_DYNAMIC_INSTALL' value: 'true' } + { + // Ensure /home/site/ini/redcap.ini and /home/site/ini/extensions.ini gets processed + name: 'PHP_INI_SCAN_DIR' + value: '/usr/local/etc/php/conf.d:/home/site/ini' + } + { + name: 'WEBSITE_TIME_ZONE' + value: timeZone + } ] } } @@ -177,10 +195,10 @@ resource sourcecontrol 'Microsoft.Web/sites/sourcecontrols@2022-09-01' = { branch: scmRepoBranch isManualIntegration: true } - dependsOn: [ privateDnsZoneGroupsWebApp ] + dependsOn: [privateDnsZoneGroupsWebApp] } -resource peWebApp 'Microsoft.Network/privateEndpoints@2022-07-01' = { +resource peWebApp 'Microsoft.Network/privateEndpoints@2022-07-01' = if (enablePrivateEndpoint) { name: 'pe-${webApp.name}' location: location properties: { @@ -201,7 +219,7 @@ resource peWebApp 'Microsoft.Network/privateEndpoints@2022-07-01' = { } } -resource privateDnsZoneGroupsWebApp 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2022-07-01' = { +resource privateDnsZoneGroupsWebApp 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2022-07-01' = if (enablePrivateEndpoint) { name: 'privatednszonegroup' parent: peWebApp properties: { diff --git a/scripts/bash/deploy.sh b/scripts/bash/deploy.sh index 0bbae9d..f642c74 100644 --- a/scripts/bash/deploy.sh +++ b/scripts/bash/deploy.sh @@ -20,10 +20,8 @@ stamp=$(date +%Y-%m-%d-%H-%M) #################################################################################### echo "Configuring mysqli extension" >> /home/site/log-$stamp.txt -cd /home/site -# echo "extension=/usr/local/lib/php/extensions/no-debug-non-zts-20190902/mysqlnd_azure.so -# extension=/usr/local/lib/php/extensions/no-debug-non-zts-20190902/mysqli.so" >> extensions.ini -echo "extension=/usr/local/lib/php/extensions/no-debug-non-zts-20220829/mysqli.so" >> extensions.ini +mkdir -p /home/site/ini +echo "extension=/usr/local/lib/php/extensions/no-debug-non-zts-20220829/mysqli.so" >> /home/site/ini/extensions.ini #################################################################################### # @@ -33,6 +31,8 @@ echo "extension=/usr/local/lib/php/extensions/no-debug-non-zts-20220829/mysqli.s # #################################################################################### +redcapZipPath="/tmp/redcap.zip" + cd /tmp if [ -z "$APPSETTING_redcapAppZip" ]; then echo "Downloading REDCap zip file from REDCap Community site" >> /home/site/log-$stamp.txt @@ -52,7 +52,7 @@ if [ -z "$APPSETTING_redcapAppZip" ]; then export APPSETTING_zipVersion="latest" fi - wget --method=post -O /tmp/redcap.zip -q --body-data="username=$APPSETTING_redcapCommunityUsername&password=$APPSETTING_redcapCommunityPassword&version=$APPSETTING_zipVersion&install=1" --header=Content-Type:application/x-www-form-urlencoded https://redcap.vanderbilt.edu/plugins/redcap_consortium/versions.php + wget --method=post -O $redcapZipPath -q --body-data="username=$APPSETTING_redcapCommunityUsername&password=$APPSETTING_redcapCommunityPassword&version=$APPSETTING_zipVersion&install=1" --header=Content-Type:application/x-www-form-urlencoded https://redcap.vanderbilt.edu/plugins/redcap_consortium/versions.php # check to see if the redcap.zip file contains the word error if [ -z "$(grep -i error redcap.zip)" ]; then @@ -64,14 +64,19 @@ if [ -z "$APPSETTING_redcapAppZip" ]; then else echo "Downloading REDCap zip file from storage" >> /home/site/log-$stamp.txt - wget -q -O /tmp/redcap.zip $APPSETTING_redcapAppZip + wget -q -O $redcapZipPath $APPSETTING_redcapAppZip fi -rm -f /home/site/wwwroot/hostingstart.html -unzip -oq /tmp/redcap.zip -d /tmp/wwwroot -mv /tmp/wwwroot/redcap/* /home/site/wwwroot/ +echo "Unzipping redcap.zip" >> /home/site/log-$stamp.txt + +rm -rf /home/site/wwwroot/* +unzip -oq $redcapZipPath -d /tmp/wwwroot + +echo "Moving REDCap files to wwwroot" >> /home/site/log-$stamp.txt + +mv -f /tmp/wwwroot/redcap/* /home/site/wwwroot/ rm -rf /tmp/wwwroot -rm /tmp/redcap.zip +rm -f $redcapZipPath #################################################################################### # @@ -85,11 +90,11 @@ cd /home/site/wwwroot wget --no-check-certificate https://dl.cacerts.digicert.com/DigiCertGlobalRootCA.crt.pem -sed -i "s|hostname[[:space:]]*= '';|hostname = '$APPSETTING_DBHostName';|" database.php -sed -i "s|db[[:space:]]*= '';|db = '$APPSETTING_DBName';|" database.php -sed -i "s|username[[:space:]]*= '';|username = '$APPSETTING_DBUserName';|" database.php -sed -i "s|password[[:space:]]*= '';|password = '$APPSETTING_DBPassword';|" database.php -sed -i "s|db_ssl_ca[[:space:]]*= '';|db_ssl_ca = '$APPSETTING_DBSslCa';|" database.php +sed -i "s|hostname[[:space:]]*= '';|hostname = getenv('DBHostName');|" database.php +sed -i "s|db[[:space:]]*= '';|db = getenv('DBName');|" database.php +sed -i "s|username[[:space:]]*= '';|username = getenv('DBUserName');|" database.php +sed -i "s|password[[:space:]]*= '';|password = getenv('DBPassword');|" database.php +sed -i "s|db_ssl_ca[[:space:]]*= '';|db_ssl_ca = getenv('DBSslCa');|" database.php sed -i "s/db_ssl_verify_server_cert = false;/db_ssl_verify_server_cert = true;/" database.php sed -i "s/$salt = '';/$salt = '$(echo $RANDOM | md5sum | head -c 20; echo;)';/" database.php @@ -101,11 +106,13 @@ sed -i "s/$salt = '';/$salt = '$(echo $RANDOM | md5sum | head -c 20; echo;)';/" #################################################################################### echo "Configuring REDCap recommended settings" >> /home/site/log-$stamp.txt + sed -i "s|SMTP[[:space:]]*= ''|SMTP = '$APPSETTING_smtpFQDN'|" /home/site/repository/Files/settings.ini sed -i "s|smtp_port[[:space:]]*= |smtp_port = $APPSETTING_smtpPort|" /home/site/repository/Files/settings.ini sed -i "s|sendmail_from[[:space:]]*= ''|sendmail_from = '$APPSETTING_fromEmailAddress'|" /home/site/repository/Files/settings.ini sed -i "s|sendmail_path[[:space:]]*= ''|sendmail_path = '/usr/sbin/sendmail -t -i'|" /home/site/repository/Files/settings.ini -cp /home/site/repository/Files/settings.ini /home/site/redcap.ini + +cp /home/site/repository/Files/settings.ini /home/site/ini/redcap.ini #################################################################################### # @@ -115,7 +122,7 @@ cp /home/site/repository/Files/settings.ini /home/site/redcap.ini #################################################################################### echo "For better security, it is recommended that you enable the session.cookie_secure option in your web server's PHP.INI file" >> /home/site/log-$stamp.txt -echo "session.cookie_secure = On" >> /home/site/redcap.ini +echo "session.cookie_secure = On" >> /home/site/ini/redcap.ini #################################################################################### # @@ -132,4 +139,6 @@ cp /home/site/repository/scripts/bash/postbuild.sh /home/site/deployments/tools/ # #################################################################################### -cp /home/site/repository/scripts/bash/startup.sh /home/startup.sh \ No newline at end of file +cp /home/site/repository/scripts/bash/startup.sh /home/startup.sh + +#echo "mysql: $(which mysql)" >> /home/site/log-$stamp.txt \ No newline at end of file diff --git a/scripts/bash/install.sh b/scripts/bash/install.sh index e2c07fa..410ae02 100644 --- a/scripts/bash/install.sh +++ b/scripts/bash/install.sh @@ -7,7 +7,7 @@ echo -e "\nHello from install.sh" -which mysql +echo "mysql: $(which mysql)" #################################################################################### # diff --git a/scripts/bash/startup.sh b/scripts/bash/startup.sh index fbb08ce..5a771a7 100644 --- a/scripts/bash/startup.sh +++ b/scripts/bash/startup.sh @@ -8,7 +8,7 @@ echo "Custom container startup" # #################################################################################### -apt-get update -qq && apt-get install sendmail cron -yqq +apt-get update -qq && apt-get install cron sendmail -yqq #################################################################################### # @@ -16,5 +16,16 @@ apt-get update -qq && apt-get install sendmail cron -yqq # #################################################################################### +# Export the database connection environment variables to /etc/environment so cron can use them +# We do this in startup.sh so that each container instance will get this file (it's outside of /home so not persisted) +# and also because then updates to the environment variables will be picked up by cron +echo "DBHostName=$DBHostName" > /etc/environment # Overwrite the file with the first statement +echo "DBName=$DBName" >> /etc/environment # Append all the other lines +echo "DBUserName=$DBUserName" >> /etc/environment +echo "DBPassword=$DBPassword" >> /etc/environment +echo "DBSslCa=$DBSslCa" >> /etc/environment + +sed -i "s|date.timezone=UTC|date.timezone=$WEBSITE_TIME_ZONE|" /usr/local/etc/php/conf.d/php.ini + service cron start -(crontab -l 2>/dev/null; echo "* * * * * /usr/local/bin/php /home/site/wwwroot/cron.php > /dev/null")|crontab +(crontab -l 2>/dev/null; echo "* * * * * /usr/local/bin/php /home/site/wwwroot/cron.php > /dev/null")|crontab