diff --git a/avm/res/app/managed-environment/README.md b/avm/res/app/managed-environment/README.md index 3d5b5191c4..45e2659dca 100644 --- a/avm/res/app/managed-environment/README.md +++ b/avm/res/app/managed-environment/README.md @@ -174,6 +174,11 @@ module managedEnvironment 'br/public:avm/res/app/managed-environment:' name: 'amemax001' // Non-required parameters appInsightsConnectionString: '' + certificateKeyVaultProperties: { + identityResourceId: '' + keyVaultUrl: '' + } + dnsSuffix: 'contoso.com' dockerBridgeCidr: '172.16.0.1/28' infrastructureResourceGroupName: '' infrastructureSubnetId: '' @@ -276,6 +281,15 @@ module managedEnvironment 'br/public:avm/res/app/managed-environment:' "appInsightsConnectionString": { "value": "" }, + "certificateKeyVaultProperties": { + "value": { + "identityResourceId": "", + "keyVaultUrl": "" + } + }, + "dnsSuffix": { + "value": "contoso.com" + }, "dockerBridgeCidr": { "value": "172.16.0.1/28" }, @@ -400,6 +414,11 @@ param logAnalyticsWorkspaceResourceId = '' param name = 'amemax001' // Non-required parameters param appInsightsConnectionString = '' +param certificateKeyVaultProperties = { + identityResourceId: '' + keyVaultUrl: '' +} +param dnsSuffix = 'contoso.com' param dockerBridgeCidr = '172.16.0.1/28' param infrastructureResourceGroupName = '' param infrastructureSubnetId = '' @@ -712,6 +731,7 @@ param workloadProfiles = [ | Parameter | Type | Description | | :-- | :-- | :-- | | [`appInsightsConnectionString`](#parameter-appinsightsconnectionstring) | securestring | Application Insights connection string. | +| [`certificateKeyVaultProperties`](#parameter-certificatekeyvaultproperties) | object | A key vault reference to the certificate to use for the custom domain. | | [`certificatePassword`](#parameter-certificatepassword) | securestring | Password of the certificate used by the custom domain. | | [`certificateValue`](#parameter-certificatevalue) | securestring | Certificate to use for the custom domain. PFX or PEM. | | [`daprAIConnectionString`](#parameter-dapraiconnectionstring) | securestring | Application Insights connection string used by Dapr to export Service to Service communication telemetry. | @@ -807,6 +827,34 @@ Application Insights connection string. - Type: securestring - Default: `''` +### Parameter: `certificateKeyVaultProperties` + +A key vault reference to the certificate to use for the custom domain. + +- Required: No +- Type: object + +**Required parameters** + +| Parameter | Type | Description | +| :-- | :-- | :-- | +| [`identityResourceId`](#parameter-certificatekeyvaultpropertiesidentityresourceid) | string | The resource ID of the identity. This is the identity that will be used to access the key vault. | +| [`keyVaultUrl`](#parameter-certificatekeyvaultpropertieskeyvaulturl) | string | A key vault URL referencing the wildcard certificate that will be used for the custom domain. | + +### Parameter: `certificateKeyVaultProperties.identityResourceId` + +The resource ID of the identity. This is the identity that will be used to access the key vault. + +- Required: Yes +- Type: string + +### Parameter: `certificateKeyVaultProperties.keyVaultUrl` + +A key vault URL referencing the wildcard certificate that will be used for the custom domain. + +- Required: Yes +- Type: string + ### Parameter: `certificatePassword` Password of the certificate used by the custom domain. diff --git a/avm/res/app/managed-environment/main.bicep b/avm/res/app/managed-environment/main.bicep index 02a51e0485..32283e9539 100644 --- a/avm/res/app/managed-environment/main.bicep +++ b/avm/res/app/managed-environment/main.bicep @@ -67,6 +67,9 @@ param certificatePassword string = '' @secure() param certificateValue string = '' +@description('Optional. A key vault reference to the certificate to use for the custom domain.') +param certificateKeyVaultProperties certificateKeyVaultPropertiesType + @description('Optional. DNS suffix for the environment domain.') param dnsSuffix string = '' @@ -171,6 +174,12 @@ resource managedEnvironment 'Microsoft.App/managedEnvironments@2024-02-02-previe certificatePassword: certificatePassword certificateValue: !empty(certificateValue) ? certificateValue : null dnsSuffix: dnsSuffix + certificateKeyVaultProperties: !empty(certificateKeyVaultProperties) + ? { + identity: certificateKeyVaultProperties!.identityResourceId + keyVaultUrl: certificateKeyVaultProperties!.keyVaultUrl + } + : null } openTelemetryConfiguration: !empty(openTelemetryConfiguration) ? openTelemetryConfiguration : null peerTrafficConfiguration: { @@ -315,6 +324,14 @@ type roleAssignmentType = { delegatedManagedIdentityResourceId: string? }[]? +type certificateKeyVaultPropertiesType = { + @description('Required. The resource ID of the identity. This is the identity that will be used to access the key vault.') + identityResourceId: string + + @description('Required. A key vault URL referencing the wildcard certificate that will be used for the custom domain.') + keyVaultUrl: string +}? + type storageType = { @description('Required. Access mode for storage: "ReadOnly" or "ReadWrite".') accessMode: ('ReadOnly' | 'ReadWrite') diff --git a/avm/res/app/managed-environment/main.json b/avm/res/app/managed-environment/main.json index 867286f2d8..ae3d233828 100644 --- a/avm/res/app/managed-environment/main.json +++ b/avm/res/app/managed-environment/main.json @@ -5,8 +5,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.29.47.4906", - "templateHash": "5062482221653261403" + "version": "0.30.23.60470", + "templateHash": "2437611645646369754" }, "name": "App ManagedEnvironments", "description": "This module deploys an App Managed Environment (also known as a Container App Environment).", @@ -134,6 +134,24 @@ }, "nullable": true }, + "certificateKeyVaultPropertiesType": { + "type": "object", + "properties": { + "identityResourceId": { + "type": "string", + "metadata": { + "description": "Required. The resource ID of the identity. This is the identity that will be used to access the key vault." + } + }, + "keyVaultUrl": { + "type": "string", + "metadata": { + "description": "Required. A key vault URL referencing the wildcard certificate that will be used for the custom domain." + } + } + }, + "nullable": true + }, "storageType": { "type": "array", "items": { @@ -313,6 +331,12 @@ "description": "Optional. Certificate to use for the custom domain. PFX or PEM." } }, + "certificateKeyVaultProperties": { + "$ref": "#/definitions/certificateKeyVaultPropertiesType", + "metadata": { + "description": "Optional. A key vault reference to the certificate to use for the custom domain." + } + }, "dnsSuffix": { "type": "string", "defaultValue": "", @@ -441,7 +465,8 @@ "customDomainConfiguration": { "certificatePassword": "[parameters('certificatePassword')]", "certificateValue": "[if(not(empty(parameters('certificateValue'))), parameters('certificateValue'), null())]", - "dnsSuffix": "[parameters('dnsSuffix')]" + "dnsSuffix": "[parameters('dnsSuffix')]", + "certificateKeyVaultProperties": "[if(not(empty(parameters('certificateKeyVaultProperties'))), createObject('identity', parameters('certificateKeyVaultProperties').identityResourceId, 'keyVaultUrl', parameters('certificateKeyVaultProperties').keyVaultUrl), null())]" }, "openTelemetryConfiguration": "[if(not(empty(parameters('openTelemetryConfiguration'))), parameters('openTelemetryConfiguration'), null())]", "peerTrafficConfiguration": { diff --git a/avm/res/app/managed-environment/tests/e2e/max/dependencies.bicep b/avm/res/app/managed-environment/tests/e2e/max/dependencies.bicep index 6c836f75d6..becf14af2e 100644 --- a/avm/res/app/managed-environment/tests/e2e/max/dependencies.bicep +++ b/avm/res/app/managed-environment/tests/e2e/max/dependencies.bicep @@ -13,6 +13,19 @@ param virtualNetworkName string @description('Required. The name of the Managed Identity to create.') param managedIdentityName string +@description('Required. The name of the Key Vault to create.') +param keyVaultName string + +@description('Required. The name of the Deployment Script to create for the Certificate generation.') +param certDeploymentScriptName string + +@secure() +@description('Required. The name for the SSL certificate.') +param certname string + +var certPWSecretName = 'pfxCertificatePassword' +var certSecretName = 'pfxBase64Certificate' + @description('Required. The name of the Storage Account to create.') param storageAccountName string @@ -80,6 +93,55 @@ resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018- location: location } +resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' = { + name: keyVaultName + location: location + properties: { + sku: { + family: 'A' + name: 'standard' + } + tenantId: tenant().tenantId + enablePurgeProtection: null + enabledForTemplateDeployment: true + enabledForDiskEncryption: true + enabledForDeployment: true + enableRbacAuthorization: true + accessPolicies: [] + } +} + +resource keyPermissions 'Microsoft.Authorization/roleAssignments@2022-04-01' = { + name: guid('msi-${managedIdentity.name}-KeyVault-Admin-RoleAssignment') + scope: keyVault + properties: { + principalId: managedIdentity.properties.principalId + roleDefinitionId: subscriptionResourceId( + 'Microsoft.Authorization/roleDefinitions', + '00482a5a-887f-4fb3-b363-3b7fe8e74483' + ) // Key Vault Administrator + principalType: 'ServicePrincipal' + } +} + +resource certDeploymentScript 'Microsoft.Resources/deploymentScripts@2020-10-01' = { + name: certDeploymentScriptName + location: location + kind: 'AzurePowerShell' + identity: { + type: 'UserAssigned' + userAssignedIdentities: { + '${managedIdentity.id}': {} + } + } + properties: { + azPowerShellVersion: '8.0' + retentionInterval: 'P1D' + arguments: '-KeyVaultName "${keyVault.name}" -CertName "${certname}" -CertSubjectName "CN=*.contoso.com"' + scriptContent: loadTextContent('../../../../../../utilities/e2e-template-assets/scripts/Set-CertificateInKeyVault.ps1') + } +} + resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = { name: storageAccountName location: location @@ -134,6 +196,21 @@ output managedIdentityPrincipalId string = managedIdentity.properties.principalI @description('The resource ID of the created Managed Identity.') output managedIdentityResourceId string = managedIdentity.id +@description('The resource ID of the created Key Vault.') +output keyVaultResourceId string = keyVault.id + +@description('The URI of the created Key Vault.') +output keyVaultUri string = keyVault.properties.vaultUri + +@description('The URL of the created certificate.') +output certificateSecretUrl string = certDeploymentScript.properties.outputs.secretUrl + +@description('The name of the certification password secret.') +output certPWSecretName string = certPWSecretName + +@description('The name of the certification secret.') +output certSecretName string = certSecretName + @description('The Connection String of the created Application Insights Component.') output appInsightsConnectionString string = appInsightsComponent.properties.ConnectionString diff --git a/avm/res/app/managed-environment/tests/e2e/max/main.test.bicep b/avm/res/app/managed-environment/tests/e2e/max/main.test.bicep index 582c1734c7..b915042b75 100644 --- a/avm/res/app/managed-environment/tests/e2e/max/main.test.bicep +++ b/avm/res/app/managed-environment/tests/e2e/max/main.test.bicep @@ -38,6 +38,9 @@ module nestedDependencies 'dependencies.bicep' = { virtualNetworkName: 'dep-${namePrefix}-vnet-${serviceShort}' location: resourceLocation managedIdentityName: 'dep-${namePrefix}-msi-${serviceShort}' + keyVaultName: 'dep-${namePrefix}-kv-${serviceShort}' + certname: 'dep-${namePrefix}-cert-${serviceShort}' + certDeploymentScriptName: 'dep-${namePrefix}-ds-${serviceShort}' appInsightsComponentName: 'dep-${namePrefix}-appinsights-${serviceShort}' storageAccountName: 'dep${namePrefix}sa${serviceShort}' } @@ -66,6 +69,11 @@ module testDeployment '../../../main.bicep' = [ } ] internal: true + dnsSuffix: 'contoso.com' + certificateKeyVaultProperties: { + identityResourceId: nestedDependencies.outputs.managedIdentityResourceId + keyVaultUrl: '${nestedDependencies.outputs.keyVaultUri}secrets/${split(nestedDependencies.outputs.certificateSecretUrl, '/')[4]}' + } dockerBridgeCidr: '172.16.0.1/28' peerTrafficEncryption: true platformReservedCidr: '172.17.17.0/24'