Preventing Azure Container Apps from restarting when deploying IaC

Wessel Loth Wessel Loth 10 minutes

Azure Container Apps is a very powerful Kubernetes-like compute offering, where you can scale containers horizontally in an easy and accessible way, without having to have a deep understanding of the underlying Kubernetes platform. However, as it's a relatively new option in Azure, there's not tons of examples and best practices defined. The coming months I'll be shared tidbits of knowledge and useful examples for those diving deeper into Azure Container Apps!

Article hero image

The Problem

If you’ve deployed a number of Azure Container Apps, you’ve likely run into this issue at some point. Your Bicep files are all set up cleanly, you managed to figure out how to pull images neatly from your container registry using managed identities, and now you’re running into the following: even though you changed nothing in the container app infrastructure, you notice that container apps are restarted/reprovisioned when you deploy your Bicep.

This is because of one well known oddity about deploying Azure Container Apps for the first time: a new container app always needs an image to run, even during your first deployment. This means you’ll find lots of resources on the internet about providing a “hello world” image during your first deployment, and later changing it to your actual application image. The restarting problem stems for this: because if you always need to provide an image during deployment, this means you’ll also need to provide an image tag. And what image tag is better suited than latest when writing your IaC?

In this case it doesn’t matter whether you’re deploying a single container app at a time, or whether you’ve got one global repository containing all ACA definitions that is rolled out in one big deployment. The issue lies in the fact that there are differences between your IaC and the actual instance running in Azure. In my experience, this is because your IaC contains different tag when compared to the running instance!

There’s multiple possible solutions to this problem, and I want to showcase two in this blogpost.

Base setup

I’ve prepared a small sample base setup: one container app environment with one container app. In a real-world scenario, there’s multiple ways to set this up: you could keep the creation of the environment in a central infrastructure repository and keep the container app code inside of the repo of the actual container you want to deploy. Or you could set up everything inside of one big repository, where you manage everything about your cloud environment from a single space.

Whatever you choose, you’re going to have to create a Container App Environment, and within that environment create a Container App at some point. This code snippet below shows a very basic setup of such an environment plus one app.

targetScope = 'resourceGroup'

param environmentName string
param appName string
param location string

resource containerAppEnvironment 'Microsoft.App/managedEnvironments@2023-05-01' = {
  name: environmentName
  location: location
  properties: {
    workloadProfiles: [
      {
        name: 'Consumption'
        workloadProfileType: 'Consumption'
      }
    ]
  }
}

resource containerApp 'Microsoft.App/containerApps@2023-05-01' = {
  name: appName
  location: location
  properties: {
    managedEnvironmentId: containerAppEnvironment.id
    configuration: {
      ingress: {
        targetPort: 80
        external: true
      }
    }
    template: {
      containers: [
        {
          image: 'mcr.microsoft.com/azuredocs/containerapps-helloworld:latest'
          name: 'simple-hello-world-container'
        }
      ]
    }
  }
}

As you can see in the highlighted line, this basic sample shows you need to specify a registry, image and image tag when you create a new Container App. Herein lies the root of all our problems, because if we want our infrastructure as code to be a ‘static’ reflection of our infrastructure, how do we account for the image tag being updated continuously by the application teams?

Solution 1: Checking pre-deployment using Azure CLI

One simple way to achieve this is to fetch information about the currently running revision, and pass that information to your deployment as parameters. For this, I’ve slightly changed the base setup shown earlier to accept a new tag parameter which can be passed when you create the deployment.

...
param tag string

resource containerApp 'Microsoft.App/containerApps@2023-05-01' = {
  ...
    template: {
      containers: [
        {
          image: 'mcr.microsoft.com/azuredocs/containerapps-helloworld:${tag}'
          name: 'simple-hello-world-container'
        }
      ]
    }
  }
}

The next step is to retrieve the current running revision of the Container App first, get information about the container and from there get the tag so it can be passed to the deployment. For this purpose, you can invoke the deployment using a small bash script like shown below (credits to the PlatformPlatform sample repository for having tons of great reference resources!), which you can easily embed into your CI/CD pipeline.

resourceGroupName=...
environmentName=...
appName=...
location=...

image=$(az containerapp revision list --name $appName --resource-group \
      $resourceGroupName --query "[0].properties.template.containers[0].image" --output tsv 2>/dev/null)
tag=${image##*:}

echo Found revision running with image $image and tag $tag

az deployment group create \
    --resource-group $resourceGroupName \
    --template-file withtagparameter.bicep \
    --parameters environmentName="$environmentName" appName="$appName" location="$location" tag="$tag"

Now, when you run your deployment you’ll notice it first checks for the running revision and passes the image tag to the deployment! For real production scenarios, you’ll of course want to make this a bit more resilient: for example, what if you have multiple revisions running with different tag versions? But with this script acting as a base that’s something you can easily iterate on.

Solution 2: Checking during deployment using Deployment Scripts

The previous example works fine, but there’s another way to do this without having to rely on scripting/logic outside of your infrastructure as code templates. I also pushed further to try out this solution because I’m personally working in an environment where all Container App infrastructure is deployed at the same time from one global infrastructure repository. This means I’d have to collect a lot of information on a lot of Container Apps at the same time before starting my deployment. The additional risk in a bigger-scale set-up is running into tag differences from when I started the infrastructure deployment compared to when the actual singular Container Apps are deployed, which might be ten to thirty minutes based on how many global infrastructure resources you’re deploying first, like Azure Firewall and SQL Server, which is enough time for an application team to release a new version of their Container App.

Deployment Scripts to the rescue

Which leads me to Azure Deployment Scripts: a way to run this same revision tag checking logic, but as close as possibile (in time and location) to your individual Container App deployment, even when orchestrating tens or hundreds of Container App deployments at the same time. Deployment scripts actually take your Azure Powershell or Azure CLI Bash script, package them in a lightweight container and run them in an Azure Container Instance which is stopped and deleted after the script executes. Outputs are written to a JSON file stored in a temporary storage account, from where they are passed back to the deployment to pass along to other parts of your deployment.

Illustration showing how a deployment script runs in an Azure Container Instance

Preparing the stage

First of all, you’re going to create a User Assigned Identity, an identity that can get assigned roles at different scopes. Using this identity, you can set up a new deployment script that acts on behalf of the identity. This means that you will also need to assign the right role at the right scope for your identity. For this script, you’re going to need to give it a Reader role at the Subscription level.

Deploy this identity somewhere in advance and give it the correct role assignments at your desired scope. In my project, I’ve got one identity that has reader permissions on all subscriptions where it has to be able to check information on Container Apps, but you could also narrow this down to one per subscription, or even one per resource group where you’re deploying a Container App.

resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
  name: 'mi-aca-get-image-tag'
  location: location
}

It’s showtime!

Now that we’ve got our identity all set up, you can finally create your deployment script.

param currentTime string = utcNow()
resource deploymentScript 'Microsoft.Resources/deploymentScripts@2023-08-01' = {
  name: 'GetContainerAppImageTag-${appName}'
  location: location
  kind: 'AzurePowerShell'
  identity: {
    type: 'UserAssigned'
    userAssignedIdentities: {
      '${identity.id}': {}
    }
  }
  properties: {
    azPowerShellVersion: '9.7'
    environmentVariables: [
      {
        name: 'appName'
        value: appName
      }
      {
        name: 'resourceGroupName'
        value: resourceGroup().name
      }
    ]
    scriptContent: '''
      Write-Host "Installing Azure Container Apps Powershell module..."
      Install-Module -Name Az.App -Force
    
      Write-Host "Checking container app '$env:appName' in resource group '$env:resourceGroupName' for active revision..."
      try {
        $containerAppRevision = Get-AzContainerAppRevision -ResourceGroupName $env:resourceGroupName -ContainerAppName $env:appName
        $image = $containerAppRevision.TemplateContainer.Image
        $version = $image.Split(":")[-1]
        Write-Host "Found image: $image with tag: $version"
        $DeploymentScriptOutputs = @{}
        $DeploymentScriptOutputs['tag'] = $version
      } catch {
        Write-Host "Failed to get active revision. Error:"
        Write-Host $_
        Write-Host "No active revision found. Is this the first deployment? Using 'latest' as tag."
        $DeploymentScriptOutputs['tag'] = "latest"
      }
    '''
    cleanupPreference: 'Always'
    retentionInterval: 'PT1H'
    forceUpdateTag: currentTime
  }
}

output currentImageTag string = deploymentScript.properties.outputs.tag

There’s a lot to break down here. Let’s start with the basics: the kind of script you want to run and the identity you want to assign to the script execution.

resource deploymentScript 'Microsoft.Resources/deploymentScripts@2023-08-01' = {
  ...
  kind: 'AzurePowerShell' // Can be AzurePowershell or AzureCLI (which uses Bash)
  ...
  identity: {
    type: 'UserAssigned'
    userAssignedIdentities: {
      '${identity.id}': {} // Your newly created User Assigned Identity ID is passed here
    }
  }
}

Next, you can easily pass values along to the script using environment variables, which can be read in the script using $env:variableName. Good to note is that you can also pass script arguments, which work slightly differently from environment variables.

resource deploymentScript 'Microsoft.Resources/deploymentScripts@2023-08-01' = {
  ...
  properties: {
    ...
    environmentVariables: [
      {
        name: 'appName'
        value: appName
      }
      {
        name: 'resourceGroupName'
        value: resourceGroup().name
      }
    ]
  }
}

Now, just a little bit more until we can finally execute our script. The following set of parameters will help you decide what the behavior of the deployment script will be in case of failure or success.

resource deploymentScript 'Microsoft.Resources/deploymentScripts@2023-08-01' = {
  ...
  properties: {
    ...
    azPowerShellVersion: '9.7' // The version of Azure Powershell you want to use
    cleanupPreference: 'Always' // Always clean up, or keep it alive after running?
    retentionInterval: 'PT1H' // Keep it around for an hour after it finishes
    forceUpdateTag: currentTime // Ensures script will run every time, even if a previous instance still exists
  }
}

Finally, the script itself is using Azure Powershell to do the same thing as the Bash script shown in Solution #1. After installing the Az.App module, you can use Get-AzContainerAppRevision to get information about the currently running Container App revision. If you’ve assigned the right role to the User Assigned Identity, it should be able to retrieve this information from your subscription.

Write-Host "Installing Azure Container Apps Powershell module..."
Install-Module -Name Az.App -Force

Write-Host "Checking container app '$env:appName' in resource group '$env:resourceGroupName' for active revision..."
try {
  $containerAppRevision = Get-AzContainerAppRevision -ResourceGroupName $env:resourceGroupName -ContainerAppName $env:appName
  $image = $containerAppRevision.TemplateContainer.Image
  $version = $image.Split(":")[-1]
  Write-Host "Found image: $image with tag: $version"
  $DeploymentScriptOutputs = @{}
  $DeploymentScriptOutputs['tag'] = $version
} catch {
  Write-Host "Failed to get active revision. Error:"
  Write-Host $_
  Write-Host "No active revision found. Is this the first deployment? Using 'latest' as tag."
  $DeploymentScriptOutputs['tag'] = "latest"
}

Tip: Do you have one User Assigned Identity reading Container Apps in multiple subscriptions? You might have to explicitly call Set-AzContext -SubscriptionId <subscriptionId> at the start of the Powershell script. You can pass the subscription ID in as an environment parameter to make this dynamic as well.

Finally, you can pass outputs from the deployment script using the $DeploymentScriptsOutput dictionary, which you can use in your Container App deployment using the deploymentScript output property.

resource containerApp 'Microsoft.App/containerApps@2023-05-01' = {
  ...
  properties: {
    ...
    template: {
      containers: [
        {
          image: 'mcr.microsoft.com/azuredocs/containerapps-helloworld:${deploymentScript.properties.outputs.tag}'
          name: 'simple-hello-world-container'
        }
      ]
    }
  }
}

Now that everything is set up and connected, let’s try it out!

Running it

Now, when running the deployment, you’ll see the deployment script resource being created and executed.

Status overview showing succeeded deployment

And when clicking on the deployment script, you can see it succesfully managed to retrieve information about the running Container App Revision!

Status overview showing succeeded deployment

Wrapping up

I hope you found something useful in these examples! Even if you’re not running into this exact problem, I hope this shows you the power of deployment scripts. Keep an eye on my socials/blog, because I’ll be posting more content soon!

Want to see both complete solutions? Check out my Azure Container App samples repository on GitHub!

Got any questions or suggestions? Tag me on Twitter or mail me at wessel@loth.io to chat further!