- Autoscaling capabilities based on external traffic, CPU and memory triggers.
- Intercommunication between Container Apps using either ingress or Dapr.
- Custom VNET integration.
- Split traffic over different versions of your app for easy Blue/Green deployments and A/B testing.
- Cost benefits where you have the option to scale to zero instances.
Bicep Overview and Azure Resources
Resource | Description |
Container Environment | Each container app requires a container environment which will hold one or many container apps. Under the hood, the environment is provisioned on the same virtual network. |
Container Registry | The container registry is where we will house the container images for our application which is then referenced by the container app. |
Container App | The container app resource itself. |
Log Analytics | We’ll use log analytics for logging various data from the container environment such as telemetry and performance logs. |
Key Vault | Key vault will be used to store secrets we’ll need for our app to access log analytics and credentials to pull images from the container registry. |

Bicep Files
- Main.bicep (contains the Key Vault resource)
- containerApp.bicep
- containerEnvironment.bicep
- containerRegistry.bicep
- logAnalytics.bicep
Handling Secrets in Bicep
- Use Managed Identities if you don’t want to worry about handling or dealing with secrets/credentials in your bicep files. It allows for authentication and authorization across other resources in Azure.
- Avoid secrets in your bicep files and pass in the value of the secrets from a CI/CD pipeline. An example of this would be storing credentials for the container registry in a GitHub actions secret.
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
# Checkout code
- uses: actions/checkout@main
# Log into Azure
- uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
# Deploy Bicep file
- name: deploy
uses: azure/arm-deploy@v1
with:
subscriptionId: ${{ secrets.AZURE_SUBSCRIPTION }}
resourceGroupName: ${{ secrets.AZURE_RG }}
template: ./deploy/main.bicep
parameters: sqlAdminUsername=${{ secrets.SQL_USERNAME }} sqlAdmin=${{ secrets.SQL_PASSWORD }}
failOnStdErr: false
- Use the @secure() decorator for all sensitive parameters. This will ensure the underlying Azure Resource Manager won’t log any passwords or sensitive variables. An example of this can be seen in the container registry bicep file below on line 6.
- Do not output secrets in your bicep files. I've often seen outputs which will include a username or password so it can be used by another module. The values could be compromised by anyone who has access to the deployment. To avoid this, you can add your secrets to Key Vault or use managed identities. An example of this would be:
output userName string = containerRegistry.listCredentials().username
Resource and Module Files
I've included abbreviated versions of the bicep files below. For the complete solution you can reference the files in the following GitHub repo: GitHub - jarrodgodfrey/containerapp-bicep//logAnalytics.bicep
resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = {
name: keyVaultName
}
resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2020-10-01' = {
name: logAnalyticsWorkspaceName
location: location
tags: tags
properties: {
retentionInDays: 30
features: {
searchVersion: 1
}
sku: {
name: 'PerGB2018'
}
}
}
//set up a shared secret in key vault which containts the log analytics primary shared key
resource sharedKeySecret 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = {
name: sharedKeyName
parent: keyVault
properties: {
value: logAnalytics.listKeys().primarySharedKey
}
}
Here you can see the log analytics resource defined on line 7. What’s more interesting is I’ve defined another resource referencing the existing Key Vault in main.bicep on line 3. I’m using it to set the parent when we add the log analytics shared key to the vault on line 25.
//containerAppEnvironment.bicep
param containerEnvironmentName string
param location string
param logAnalyticsCustomerId string
@secure()
param logAnalyticsSharedKey string
param tags object
resource env 'Microsoft.App/managedEnvironments@2022-03-01' = {
name: containerEnvironmentName
location: location
tags: tags
properties: {
appLogsConfiguration: {
destination: 'log-analytics'
logAnalyticsConfiguration: {
customerId: logAnalyticsCustomerId
sharedKey: logAnalyticsSharedKey
}
}
}
}
output containerAppEnvId string = env.id
Notice on line 7 we have a parameter for the log analytics shared key which is used to configure the container environment on line 19.
//containerRegistry.bicep
resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' existing = {
name: keyVaultName
}
resource containerRegistry 'Microsoft.ContainerRegistry/registries@2022-02-01-preview' = {
name: crName
location: location
tags: tags
sku: {
name: 'Basic'
}
properties: {
adminUserEnabled: true
}
identity: {
type: 'SystemAssigned'
}
}
//adding container registry username to keyvault
resource acrUsername 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = {
name: usernameSecret
parent: keyVault
properties: {
value: containerRegistry.listCredentials().username
}
}
//adding container registry password to key vault
resource acrPasswordSecret1 'Microsoft.KeyVault/vaults/secrets@2022-07-01' = {
name: primaryPasswordSecret
parent: keyVault
properties: {
value: containerRegistry.listCredentials().passwords[0].value
}
}
Similar to what was done in logAnalytics.bicep, the username and password are being added to key vault starting on line 23. This is important because the container app itself will utilize those secrets from Key Vault to pull the images from the registry.
//containerApp.bicep
resource containerApp 'Microsoft.App/containerApps@2022-03-01' = {
name: containerAppName
location: location
tags: tags
properties: {
managedEnvironmentId: containerAppEnvId
configuration: {
activeRevisionsMode: 'Single'
ingress: {
external: true
transport: 'http'
targetPort: 3500
allowInsecure: false
traffic: [
{
latestRevision: true
weight: 100
}
]
}
secrets: [
{
name: 'container-registry-password'
value: acrPasswordSecret
}
]
registries: [
{
server: acrServerName
username: acrUsername
passwordSecretRef: 'container-registry-password'
}
]
}
template: {
containers: [
{
name: containerAppName
image: '${acrServerName}/epic-app:latest'
env: envVariables
resources: {
cpu: 1
memory: '2.0Gi'
}
}
]
scale: {
minReplicas: 1
maxReplicas: 10
}
}
}
identity: {
type: 'SystemAssigned'
}
}
Notice we’re passing in the container registry credentials and using them on line 32 so the container app will have access to pull the images. Another important line is 41 where the image is specified. We’re providing the container app the server name of the registry and the name of the image.
//main.bicep
resource keyVault 'Microsoft.KeyVault/vaults@2022-07-01' = {
name: keyVaultName
location: location
tags: tags
properties: {
sku: {
family: 'A'
name: 'standard'
}
tenantId: tenant().tenantId
enabledForDeployment: true
enabledForTemplateDeployment: true
enableSoftDelete: false
accessPolicies: [
]
}
}
//module invocations:
module logAnalytics 'logAnalytics.bicep' = {
name: 'log-analytics'
params: {
tags: tags
keyVaultName: keyVault.name
location: location
logAnalyticsWorkspaceName: logAnalyticsWorkspaceName
}
}
module containerEnv 'containerAppEnvironment.bicep' = {
name: 'container-app-env'
params: {
containerEnvironmentName: containerEnvironmentName
location: location
logAnalyticsCustomerId: logAnalytics.outputs.customerId
logAnalyticsSharedKey: keyVault.getSecret('law-shared-key')
tags: tags
}
}
module containerRegistry 'containerRegistry.bicep' = {
name: 'acr'
params: {
tags: tags
crName: containerRegistryName
keyVaultName: keyVault.name
location: location
}
}
module containerApp 'containerapp.bicep' = if (isContainerImagePresent){
name: 'container-app'
params: {
tags: tags
location: location
containerAppName: containerAppName
envVariables: containerAppEnvVariables
containerAppEnvId: containerEnv.outputs.containerAppEnvId
acrServerName: containerRegistry.outputs.serverName
acrUsername: keyVault.getSecret('acr-username-shared-key')
acrPasswordSecret: keyVault.getSecret('acr-password-shared-key')
}
}
You can see here where the Key Vault resource is defined and how all the modules are being invoked and deployed.
az deployment group create --resource-group rg-epic-app --template-file main.bicep --parameters isContainerImagePresent=t
Conclusion
I’m extremely excited about the future of Azure Container Apps. As I’ve said I think this technology will really take off once it’s fully adopted by the community. This post has targeted the approach to deploy all the resources you need to get started with a container app. I’ve also explained how to configure the Bicep files so each resource can read and store secrets in a secure way where applicable. I’ll plan to write another post which will be more of a deeper dive into the capabilities of Azure Container Apps… stay tuned! Learn more about how Xpirit can help you transform your business !
Esteban Garcia
Managing Director at Xebia Microsoft Services US and a recognized expert in DevOps, GitHub Advanced Security, and GitHub Copilot. As a Microsoft Regional Director (RD) and Most Valuable Professional (MVP), he leads his team in adopting cutting-edge Microsoft technologies to enhance client services across various industries. Esteemed for his contributions to the tech community, Esteban is a prominent speaker and advocate for innovative software development and security solutions.
Contact