{
"$type": "site.standard.document",
"canonicalUrl": "https://johnnyreilly.com/posts/using-azd-for-faster-incremental-azure-container-app-deployments-in-azure-devops",
"description": "Learn how to speed up deployments of Azure Container Apps in GitHub Actions using the AZD command.",
"path": "/posts/using-azd-for-faster-incremental-azure-container-app-deployments-in-azure-devops",
"publishedAt": "2024-07-15T00:00:00.000Z",
"site": "at://did:plc:yy3apqjlms24kso7ahn7lbmb/site.standard.publication/3mova7c4nho2b",
"tags": [
"azure",
"bicep",
"azure container apps",
"azure devops",
"azure pipelines"
],
"textContent": "When deploying Azure Container Apps from Azure DevOps, you can use the azd command to speed up deployments that do not affect infrastructure. Given that when you're deploying, it's far more common to be making a code and / or content change and not an infrastructure one, this can be a significant time saver.\n\nIf you're looking for information on how to use azd to speed up deployments of Azure Static Web Apps in GitHub Actions, then you might want to read this post.\n\n\n\nFaster deployments from azd 1.4 and beyond\n\nThe azd v1.4.0 release contained a significant feature: azd provision is now faster when there are no infrastructure changes.\n\nTo quote a trimmed down version of the announcement:\n\n> If you’ve been using the Azure Developer CLI for a while, you may have noticed that sometimes azd provision takes a long time to complete when it may not need to. The wait time was because, prior to version 1.4.0, azd provision would always reprovision regardless of whether the underlying Infrastructure as Code had changed... As of today’s 1.4.0 release, azd provision now checks the most recent deployment upstream on Azure to see if the state is the same as what’s represented in the Infrastructure as Code that’s been used to provision. If the state is the same, the provision is skipped... with this new experience, you should also notice improved performance when running azd up in a CI/CD pipeline as provisioning will be automatically skipped when there are no changes.\n\nI want this. We're going to unpack how to use this feature in the context of an Azure DevOps pipeline with Azure Container Apps. It turns out that there's a little more to it than just running azd provision and hoping for the best. In fact, there's gotchas aplenty - but it's totally achievable.\n\nWhat about Bicep what-if?\n\nYou might be thinking at this point: \"What about Bicep what-if?\" It's a good question. what-if is a feature of the Azure CLI that allows you to see what changes would be made if you were to deploy a Bicep file. Unfortunately, my own experience of using what-if has been that it's quite unreliable. It will detect changes where there are none, and it will fail to detect changes where there are some. I'd love to use it, but I can't trust it. If you'd like to watch a more in depth discussion of the issue, this video is a good place to start.\n\nThere appear to be some known issues with what-if that you can follow the progress of here:\n\n- https://github.com/Azure/arm-template-whatif/issues/83\n- https://github.com/Azure/arm-template-whatif/issues/157\n\nGiven that what-if is not reliable, we're going to use azd to speed up our deployments.\n\nEmbracing azd in an existing Azure DevOps pipeline\n\nI'm going to start with a pre-existing Azure Pipeline that deploys an Azure Container App. It uses the classic AzureResourceManagerTemplateDeployment@3 ARM template deployment v3 task to deploy our infrastructure in the form of a main.bicep (and it's submodules) file.\n\nThis existing pipeline and infrastructure as code payload is in a good state. But it's slow. Every time the pipeline runs, the bicep section takes 8 minutes. We're going to make it faster. Spoiler: we're going to get it down to 1 minute.\n\nHello azure.yml\n\nOur project has no integration with azd. But we need azd to take advantage of the new azd provision feature. We're going to add a new file to our project: azure.yml. This file is going to contain the configuration for our azd project.\n\nThe yaml above describes a container app service called app that uses an image from an Azure Container Registry. The APP_VERSION_TAG is a variable that we'll need to provide in our Azure DevOps pipeline. It's worth noticing the link at the top to the schema for the azure.yml file: https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json - much of the work around figuring out how to use azd was achieved by looking at the schema for the azure.yml file.\n\nOne thing we learned, as we looked at the schema, was that many parameters support environment variable substitution at runtime:\n\nThe screenshot above is taken from the Docker section of the configuration where environment variable substitution is widely supported. Originally the image parameter did not support substitution. It does as of v1.10.0.\n\nYou'll notice that we pass resourceName: ${CONTAINER_APP_NAME}. You'll see later that we supply the CONTAINER_APP_NAME and this will be consumed by the azd deploy stage.\n\nIncidentally, we're using an approach whereby the image is built and pushed to the registry independently of azd. You could equally use azd to build and push the image. But we're not doing that here.\n\nBicep modifications\n\nI mentioned that we're adding a level of azd support to an existing pipeline. As part of that, we need to make modifications to our existing Bicep modules.\n\nUsing resource group scoped deployments with azd\n\nWe're going to start off with a minor tweak to our main.bicep file; the entry point to our Bicep deployments.\n\nThe change above allows us to use azd deployments targeted at existing resource groups. The default mode of operation for azd deployments is deploying a resource group to a subscription. We are seeking to deploy to an existing resource group.\n\nNow, strictly speaking, this isn't necessary for speeding up deployments with azd. But if you're not one for creating a resource group per deployment (as I am not), then this is a good idea. This kind of deployment requires less permissions and may well better align with your organisation's security posture.\n\nWe'll need to opt into using this feature with azd later on in the pipeline; at present resource group scoped deployments are considered \"beta\".\n\nThe \"does your service exist?\" parameter\n\nWe're going to add a \"magic\" parameter to our main.bicep file. This parameter is going to be used to determine whether the container app we're deploying already exists. This is important because if the container app already exists, we will reuse the existing deployed container image during the azd provision stage. If it does not, then we'll deploy a new container image.\n\nWe'll look later at where this value comes from, but for now, we're just adding it to our main.bicep file. How do we use this parameter? In the module where we deploy our container app, we're going to make a couple of changes:\n\nYou can see we tag the container app with the service name from the azure.yml file (web). This is important because it allows azd to determine whether the container app already exists and so power the containerAppExists parameter we added to our main.bicep file.\n\nWe're using the containerAppExists parameter to determine whether we should fetch the currently deployed image from the existing container app. If the container app exists, we'll use the existing image. If it does not, we'll use a default image. We'd typically only expect to use the default image when we're deploying the container app to an environment for the first time. (You might be wondering how the new version of the application gets deployed; given that we're not using the new container image. It turns out that azd deploy is the command responsible for deploying the new image; we'll get to that later.)\n\nYou'll have noticed that we're using a new module called fetch-container-image.bicep. This module is responsible for attempting to fetch the existing image from the currently deployed container app:\n\nThis is based on what files are generated when you perform an azd init, but has been customised for the specific version of the Microsoft.App/containerApps resource we're using.\n\nTagging resources with the environment name\n\nAnother addition we're going to make to our main.bicep file is to tag our resources with the environment name. This allows azd to determine the environment of a given resource. It's achieved by using the our already existing envName parameter and adding it to the tags of our resources:\n\nParameters in main.bicep must be immutable per environment\n\nIt's gotcha time! One thing we learned the hard way is that parameters in main.bicep must be immutable per environment. This means that you can't change the value of a parameter in a main.bicep file between deployments to an environment. This is because azd uses the main.bicep file to determine whether a deployment is incremental or not. If the parameters change, then azd will assume that the deployment requires infrastructure changes, and will reprovision the resources.\n\nWhat's more, as things stand, azd only has the ability to fully reprovision your resources. If your app consists of a database and a container app, and you only want to deploy a new version of the container app, you're out of luck. azd will deploy the database again too. This is a limitation of azd at the time of writing.\n\nI've raised a GitHub issue in the hope that this feature might one day land. Perhaps it's super hard - quite possibly.\n\nWelcome main.bicepparam\n\nPrior to using azd, we were using a main.bicep file to deploy our infrastructure and we provided parameters to this file via our Azure DevOps pipeline. We're going to make a change to our pipeline to use a main.bicepparam file instead.\n\nThe main.bicepparam file is going to contain the parameters that we were previously providing to our main.bicep file. It's going to pick these up from environment variables that we'll declare. We're also going to add our new containerAppExists parameter to this file, which will also collect its value from an environment variable. But it won't be us that provides that value; it will be azd.\n\nConsider the following (cut down) main.bicepparam file:\n\nThe containerAppExists parameter is determined by the SERVICE_APP_RESOURCE_EXISTS environment variable to provide this value. What's happening here is that we're picking up on a convention that azd uses to provide confirmation that a service already exists. SERVICE_[SERVICENAME]_RESOURCE_EXISTS is the pattern that azd uses to provide this information; where [SERVICENAME] is the name of the service as defined in the azure.yml file. In our case, it's app (or rather APP).\n\nIf you're curious about how this actually works you can read the source code here in the azd project. Here's the relevant code snippet:\n\nNow that we have our main.bicepparam file, we're ready to migrate to our pipeline to use azd.\n\nWell, one extra bit first.\n\nWorkaround for SERVICE_APP_RESOURCE_EXISTS not being supplied\n\nAt the time of writing, there is an issue that means that the SERVICE_APP_RESOURCE_EXISTS environment variable is not being set by azd. This is a known issue and is being worked on.\n\nIn the meantime, we have a workaround. We're going to set the SERVICE_APP_RESOURCE_EXISTS environment variable in our pipeline with a little bash and Azure CLI magic. We're going to set our manually created SERVICE_APP_RESOURCE_EXISTS environment variable to true if we detect a service already exists and false if not.\n\nYou'll note this as the Check container app exists step in the modifications to our pipeline below. This is a workaround and will be removed when the issue is resolved in azd itself.\n\nAzure DevOps pipeline modifications\n\nThere's no two ways about it; our Azure DevOps pipeline modifications are pretty extensive. Where we were previously using the AzureResourceManagerTemplateDeployment@3 task to deploy our Bicep files, we're now going to use the azd command to deploy our infrastructure and our container app.\n\nHere's a cut down version of our pipeline replacing the single AzureResourceManagerTemplateDeployment@3 task with a series of tasks that use the azd command:\n\nWhat's happening here? We'll take it step by step:\n\n- We're installing azd using the setup-azd@0 task.\n- We're configuring azd to use the Azure CLI for authentication and to enable resource group scoped deployments.\n- We're logging into our Azure Container Registry. (If you're not building your image independently of azd, then you may not need this step.)\n- We're provisioning our infrastructure using azd provision --no-prompt. Note that we're providing a number of environment variables to azd which will be detected in our main.bicepparam file.\n- We're deploying our application using azd deploy --no-prompt. As we do that, we pass the CONTAINER_APP_NAME environment variable which will substitute into the azure.yaml file\n\nYou'll note that as we use azd, we make heavy use of environment variables. These environment variables will be picked up in the main.bicepparam file and passed through to the main.bicep. And of course there's the runtime SERVICE_[SERVICENAME]_RESOURCE_EXISTS parameter which azd will provide. Much of what you see here is inspired by this documentation.\n\nWhat does it look like when it works?\n\nThat is the question! Like this:\n\nThe magic sentence in the above screenshot is: SUCCESS: There are no changes to provision for your application. This is what we're looking for. This is what makes our deployments faster.\n\nConclusion\n\nSo we've done it, we've speeded up our subsequent deployments by using azd in our Azure DevOps pipeline to avoid unnecessary infrastructure provisioning when there are no changes. This is a significant time saver. However, as we've also seen, this is very easy to get wrong and quite hard to get right! Hopefully this will help you implement azd in your Azure DevOps pipelines.\n\nI couldn't have written this without Marcel Michau who did much of the heavy lifting on this project. I am the Boswell to his Johnson. Or something like that.",
"title": "Using AZD for faster incremental Azure Container App deployments in Azure DevOps"
}