Blog

Azure Container Apps and Bicep – Deploying Dependent Resources

11 May, 2023
Xebia Background Header Wave
One of the relatively new technologies Azure offers today is Azure Container Apps (ACA). It first became generally available in May 2022. I believe this service will gain more traction and become very popular for organizations in the future. At a high level, if you’re looking to build microservices or API endpoints or really any background tasks for applications, ACA could be a great fit. The primary benefit it offers is really a serverless container service where you don’t have to worry about any of the complexity with infrastructure or container orchestration like you do with Azure Kubernetes Service (AKS). It’s very easy to provision and standup but also gives you powerful features you can customize. Here are just a few features ACA offers:
  • 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.
This blog post is not a deep dive on Azure Container Apps. I will provide those details in another post.  What we’ll cover here is how to use Bicep to provision a container app with all the related resources you’ll need to get started.

Bicep Overview and Azure Resources

First off, if you’re not familiar with Bicep, it’s a declarative language which enables you to deploy Azure resources.  I recommend reading this article from Microsoft if you would like to learn more about Bicep before moving forward. We’ll be deploying the following resources using Bicep which will provide all we need for our initial Azure Container App:
ResourceDescription
Container EnvironmentEach 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 RegistryThe container registry is where we will house the container images for our application which is then referenced by the container app.
Container AppThe container app resource itself.
Log AnalyticsWe’ll use log analytics for logging various data from the container environment such as telemetry and performance logs.
Key VaultKey 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.
The dependent resources are illustrated in the following diagram:

Bicep Files

I’ve structured the resources into five files. One file for each resource which are Bicep modules and a main file. Main.bicep will call into the other modules to run the deployment: Note: A module is used to organize and break down the bicep code into smaller components.  It helps with readability and organization so you don’t have to have all your deployment details in one file. It’s also useful to reuse certain modules with other deployments as well.
  1. Main.bicep (contains the Key Vault resource)
  2. containerApp.bicep
  3. containerEnvironment.bicep
  4. containerRegistry.bicep
  5. logAnalytics.bicep
Note: When deploying bicep resources, the Azure Resource Manager will evaluate dependencies and will deploy them in the correct order. When there are no dependencies, they will be deployed in parallel. I’ll address how we deploy the container app using a new container registry (with no images) later in the post.

Handling Secrets in Bicep

Before we look at the bicep for each module, one of the important points to understand is how secrets are handled. There are resources which will be dependent on one another. For example, when we configure the container app environment, we’ll need to specify the log analytics shared key which the container app requires for the app logging. To solve this, we’ll be using key vault to house the secrets which can be used across our bicep files. In addition to using Key Vault there are also other options which can be used to handle secrets:
  1. 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.
  2. 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.
I prefer keeping all my secrets in Key Vault so they are securely managed in a central resource. You can also store other keys, certificates or passwords which future resources can use. The following example is how you would deploy an Azure SQL database and provide the bicep file the username and password as parameters in a GitHub Actions workflow:
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
Other things to keep in mind:
  1. 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.
  2. 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: https://github.com/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. 
Note: Since we are deploying a new container registry there will be no images when it’s first deployed.  I’ve included a conditional parameter which will bypass the deployment of the container app for this reason. In other words, if you try to deploy a container app which is referencing an image that doesn’t exist it will fail. To navigate through this issue, you can set the ‘isContainerImagePresent’ parameter to false when executing main.bicep for the first time. Once the first image is deployed either manually using docker or running a GitHub Actions workflow, you can run main.bicep a second time and set ‘isContainerImagePresent’ to true. Please review this article to understand how to push an image to the container registry. To execute the deployment via the Azure CLI run (the ‘t’ parameter will work and will be interpreted as ‘true’):
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 !
Follow me on Twitter here!
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.
Questions?

Get in touch with us to learn more about the subject and related solutions

Explore related posts