Skip to content

Commit

Permalink
EES-4765 Provision API container app
Browse files Browse the repository at this point in the history
  • Loading branch information
benoutram committed Dec 21, 2023
1 parent 3571afc commit ad5c3a3
Show file tree
Hide file tree
Showing 9 changed files with 322 additions and 4 deletions.
15 changes: 15 additions & 0 deletions .azdo/pipelines/azure-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,21 @@ jobs:
scriptLocation: inlineScript
inlineScript: |
azd provision --no-prompt
env:
AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID)
AZURE_ENV_NAME: $(Environment.Name)
AZURE_LOCATION: $(AZURE_LOCATION)
AZURE_RESOURCE_GROUP_NAME: ${AZURE_RESOURCE_GROUP_NAME}
PRODUCT_NAME: ${PRODUCT_NAME}
DEPLOY_ROLE_ASSIGNMENTS: ${{ parameters.deployRoleAssignments }}
- task: AzureCLI@2
displayName: Azure Dev Deploy
inputs:
azureSubscription: $(SERVICE_CONNECTION_NAME)
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
azd deploy --no-prompt
env:
AZURE_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID)
AZURE_ENV_NAME: $(Environment.Name)
Expand Down
7 changes: 7 additions & 0 deletions azure.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,10 @@ metadata:
pipeline:
provider: azdo
services:
api:
project: response_automater
host: containerapp
docker:
path: ./Dockerfile
context: ../
language: python
89 changes: 88 additions & 1 deletion infra/app/api.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,105 @@ param location string = resourceGroup().location
param tags object = {}

param identityName string

param deployRoleAssignments bool = true
param containerRegistryName string
param keyVaultName string
param serviceName string = 'api'
param containerAppsEnvironmentName string
param applicationInsightsName string
param corsAcaUrl string
param exists bool
@secure()
param appDefinition object

var appSettingsArray = filter(array(appDefinition.settings), i => i.name != '')
var secrets = map(filter(appSettingsArray, i => i.?secret != null), i => {
name: i.name
value: i.value
secretRef: i.?secretRef ?? take(replace(replace(toLower(i.name), '_', '-'), '.', '-'), 32)
})
var env = map(filter(appSettingsArray, i => i.?secret == null), i => {
name: i.name
value: i.value
})

resource apiIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
name: identityName
location: location
tags: tags
}

resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing = {
name: applicationInsightsName
}

resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = {
name: keyVaultName
}

module containerRegistryAccess '../shared/container-registry-access.bicep' = if (deployRoleAssignments) {
name: '${name}-container-registry-access'
params: {
principalId: apiIdentity.properties.principalId
}
}

// Give the API access to KeyVault
module apiKeyVaultAccess '../shared/keyvault-access.bicep' = {
name: '${name}-keyvault-access'
params: {
keyVaultName: keyVaultName
principalId: apiIdentity.properties.principalId
}
}

module app '../shared/container-app.bicep' = {
name: '${serviceName}-container-app'
dependsOn: [ apiKeyVaultAccess ]
params: {
name: name
location: location
tags: union(tags, { 'azd-service-name': serviceName })
identityName: apiIdentity.name
exists: exists
containerAppsEnvironmentName: containerAppsEnvironmentName
containerRegistryName: containerRegistryName
containerCpuCoreCount: '1.0'
containerMemory: '2.0Gi'
containerMinReplicas: 1
containerMaxReplicas: 10
env: union([
{
name: 'AZURE_CLIENT_ID'
value: apiIdentity.properties.clientId
}
{
name: 'AZURE_KEY_VAULT_ENDPOINT'
value: keyVault.properties.vaultUri
}
{
name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
value: applicationInsights.properties.ConnectionString
}
{
name: 'API_ALLOW_ORIGINS'
value: corsAcaUrl
}
],
env,
map(secrets, secret => {
name: secret.name
secretRef: secret.secretRef
}))
secrets: union([
],
map(secrets, secret => {
name: secret.name
secretRef: secret.secretRef
}))
targetPort: 8010
}
}

output name string = app.name
output uri string = app.outputs.uri
1 change: 0 additions & 1 deletion infra/app/web.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ param location string = resourceGroup().location
param tags object = {}

param identityName string

param deployRoleAssignments bool = true

resource webIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
Expand Down
27 changes: 27 additions & 0 deletions infra/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ param resourceGroupName string
@description('Specify if role assignments should be deployed')
param deployRoleAssignments bool = true

param apiAppExists bool = false
@secure()
param apiAppDefinition object

// Tags that should be applied to all resources.
//
// Note that 'azd-service-name' tags should be applied separately to service host resources.
Expand All @@ -35,6 +39,9 @@ var tags = {
Product: productName
}

var apiContainerAppNameOrDefault = '${abbrs.appContainerApps}web'
var corsAcaUrl = 'https://${apiContainerAppNameOrDefault}.${containerAppsEnv.outputs.defaultDomain}'

var abbrs = loadJsonContent('./abbreviations.json')

// Organize resources in a resource group
Expand Down Expand Up @@ -79,6 +86,19 @@ module monitoring './shared/monitoring.bicep' = {
scope: rg
}

// Container apps host
module containerAppsEnv './shared/container-apps-environment.bicep' = {
name: 'container-apps-environment'
params: {
name: '${resourceGroupName}-${abbrs.appManagedEnvironments}01'
location: location
tags: tags
applicationInsightsName: monitoring.outputs.applicationInsightsName
logAnalyticsWorkspaceName: monitoring.outputs.logAnalyticsWorkspaceName
}
scope: rg
}

// Api container app
module api './app/api.bicep' = {
name: 'api'
Expand All @@ -88,6 +108,13 @@ module api './app/api.bicep' = {
tags: tags
identityName: '${resourceGroupName}-${abbrs.managedIdentityUserAssignedIdentities}api'
deployRoleAssignments: deployRoleAssignments
applicationInsightsName: monitoring.outputs.applicationInsightsName
containerAppsEnvironmentName: containerAppsEnv.outputs.name
containerRegistryName: containerRegistry.outputs.name
keyVaultName: keyVault.outputs.name
corsAcaUrl: corsAcaUrl
exists: apiAppExists
appDefinition: apiAppDefinition
}
scope: rg
}
Expand Down
8 changes: 8 additions & 0 deletions infra/main.parameters.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@
},
"deployRoleAssignments": {
"value": "${DEPLOY_ROLE_ASSIGNMENTS=true}"
},
"apiAppExists": {
"value": "${SERVICE_API_RESOURCE_EXISTS=false}"
},
"apiAppDefinition": {
"value": {
"settings": []
}
}
}
}
150 changes: 150 additions & 0 deletions infra/shared/container-app.bicep
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
metadata description = 'Creates a container app in an Azure Container App environment.'
param name string
param location string = resourceGroup().location
param tags object = {}

@description('Allowed origins')
param allowedOrigins array = []

@description('Name of the environment for container apps')
param containerAppsEnvironmentName string

@description('CPU cores allocated to a single container instance, e.g., 0.5')
param containerCpuCoreCount string = '0.5'

@description('The maximum number of replicas to run. Must be at least 1.')
@minValue(1)
param containerMaxReplicas int = 10

@description('Memory allocated to a single container instance, e.g., 1Gi')
param containerMemory string = '1.0Gi'

@description('The minimum number of replicas to run. Must be at least 1.')
param containerMinReplicas int = 1

@description('The name of the container')
param containerName string = 'main'

@description('The name of the container registry')
param containerRegistryName string = ''

@description('The protocol used by Dapr to connect to the app, e.g., http or grpc')
@allowed([ 'http', 'grpc' ])
param daprAppProtocol string = 'http'

@description('The Dapr app ID')
param daprAppId string = containerName

@description('Enable Dapr')
param daprEnabled bool = false

@description('The environment variables for the container')
param env array = []

@description('Specifies if the resource ingress is exposed externally')
param external bool = true

@description('The name of the user-assigned identity')
param identityName string = ''

@description('The name of the container image')
param imageName string = ''

@description('Specifies if the resource already exists')
param exists bool = false

@description('Specifies if Ingress is enabled for the container app')
param ingressEnabled bool = true

param revisionMode string = 'Single'

@description('The secrets required for the container')
param secrets array = []

@description('The service binds associated with the container')
param serviceBinds array = []

@description('The name of the container apps add-on to use. e.g. redis')
param serviceType string = ''

@description('The target port for the container')
param targetPort int = 80

resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-04-01-preview' existing = {
name: containerAppsEnvironmentName
}

resource existingApp 'Microsoft.App/containerApps@2023-04-01-preview' existing = if (exists) {
name: name
}

resource userIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' existing = {
name: identityName
}

resource app 'Microsoft.App/containerApps@2023-04-01-preview' = {
name: name
location: location
tags: tags
// It is critical that the identity is granted ACR pull access before the app is created
// otherwise the container app will throw a provision error
// This also forces us to use an user assigned managed identity since there would no way to
// provide the system assigned identity with the ACR pull access before the app is created
identity: {
type: 'UserAssigned'
userAssignedIdentities: { '${userIdentity.id}': {} }
}
properties: {
managedEnvironmentId: containerAppsEnvironment.id
configuration: {
activeRevisionsMode: revisionMode
ingress: ingressEnabled ? {
external: external
targetPort: targetPort
transport: 'auto'
corsPolicy: {
allowedOrigins: union([ 'https://portal.azure.com', 'https://ms.portal.azure.com' ], allowedOrigins)
}
} : null
dapr: daprEnabled ? {
enabled: true
appId: daprAppId
appProtocol: daprAppProtocol
appPort: ingressEnabled ? targetPort : 0
} : { enabled: false }
secrets: secrets
service: !empty(serviceType) ? { type: serviceType } : null
registries: [
{
server: '${containerRegistryName}.azurecr.io'
identity: userIdentity.id
}
]
}
template: {
serviceBinds: !empty(serviceBinds) ? serviceBinds : null
containers: [
{
image: !empty(imageName) ? imageName : exists ? existingApp.properties.template.containers[0].image : 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest'
name: containerName
env: env
resources: {
cpu: json(containerCpuCoreCount)
memory: containerMemory
}
}
]
scale: {
minReplicas: containerMinReplicas
maxReplicas: containerMaxReplicas
}
}
}
}

output defaultDomain string = containerAppsEnvironment.properties.defaultDomain
output identityPrincipalId string = userIdentity.properties.principalId
output imageName string = imageName
output name string = app.name
output serviceBind object = !empty(serviceType) ? { serviceId: app.id, name: name } : {}
output uri string = ingressEnabled ? 'https://${app.properties.configuration.ingress.fqdn}' : ''
7 changes: 5 additions & 2 deletions infra/shared/container-apps-environment.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ param name string
param location string = resourceGroup().location
param tags object = {}

@description('Name of the Log Analytics workspace')
param logAnalyticsWorkspaceName string
param applicationInsightsName string = ''

@description('Name of the Application Insights resource')
param applicationInsightsName string

resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-04-01-preview' = {
name: name
Expand All @@ -30,4 +33,4 @@ resource applicationInsights 'Microsoft.Insights/components@2020-02-02' existing
}

output name string = containerAppsEnvironment.name
output domain string = containerAppsEnvironment.properties.defaultDomain
output defaultDomain string = containerAppsEnvironment.properties.defaultDomain
Loading

0 comments on commit ad5c3a3

Please sign in to comment.