Although we can provision and manage Azure Container Apps with Project Bicep (as explained in this article), people keep on asking how to manage their Azure Container Apps using HashiCorp Terraform. This article demonstrates how to provision Azure Container Apps with Terraform utilizing a combination of the well-known AzureRM provider (azurerm) and the recently released AzAPI provider (azapi).



Before we dive into provisioning a sample workload, let’s look at what the AzAPI provider is and how it works.

What is the AzAPI provider

The recently released AzAPI provider for Terraform is a thin abstraction layer on top of the Azure ARM REST API. It allows you to use any API version provided by Azure’s ARM REST API, which means you can use the latest entities and their properties (including entities in public and private previews). The AzAPI provider also supports the same authentication mechanisms as the AzureRM provider. This means adding the AzAPI provider to existing Terraform projects already using the AzureRM provider is super easy.

The most interesting resource provided by the AzAPI provider is azapi_resource, which acts as a kind of meta resource. No matter which Azure entity you want to describe, azapi_resource is the unified interface that you will use:

resource "azapi_resource" "acr" {
  type = "Microsoft.ContainerRegistry/registries@2021-09-01"
  name = "sampleacr"
  location = "germanywestcentral"
  parent_id = var.resource_group_id
  body = jsonencode({
    sku = {
      name = "Standard"
    }
    properties = {
      adminUserEnabled = false
    }
  })
}

Every Azure resource has the following properties in common:

  • type: The type of the Azure ARM entity in the format <entity-type>@<api-version>
  • name: The name that this particular Azure resource should get
  • parent_id: The identifier of the parent resource. (Resource Group, Management Group, Subscription, or Extension)
  • location: Desired Azure Region where the Azure resource should exist

It’s the body property that makes azapi_resourceso special. We use the body property to specify the entire resource configuration. On top of the properties shown in the snippet above, additional properties exist; check the provider documentation to explore them.

Deploy Azure Container Apps with Terraform

Having a basic understanding of the AzAPI provider, we can explore how to deploy an actual workload to ACA using Terraform. The entire source code shown in this article is located in my Azure Container Apps samples repository on GitHub (011-deploy-aca-with-terraform).

Bootstrap the project with AzureRM and AzAPI providers

Every folder is a valid Terraform project. All .tf files in the folder (non-recursive) are recognized by Terraform and will be considered for mutating the cloud environment.

First, let’s quickly create the project folder and all necessary files that we will deal with for the scope of this article:

# create a project folder
mkdir aca-terraform && cd aca-terraform

# create necessary files
touch meta.tf deps.tf aca.tf variables.tf locals.tf

Next, we’ve to configure our Terraform project. I place configuration instructions typically in meta.tf. To use both providers (azapi and azurerm), we have to list them in terraform::required_providers and add the corresponding provider blocks as shown in the following snippet:

#meta.tf
terraform {
  required_version = "1.2.7"
  required_providers {
    azurerm = {
      source = "hashicorp/azurerm"
      version = "~>3.17.0"
    }
    azapi = {
      source = "Azure/azapi"
      version = "~>0.4.0"
    }
  }
}

provider "azurerm" {
  features {}
}

provider "azapi" { }

Having the configuration in place, it’s time to invoke terraform init, which will download all necessary providers.

Authenticate using an Service Principal

Although Terraform can re-use existing authentication tokens from Azure CLI, I highly recommend using a dedicated Service Principal (SP) for authentication. Check the provider documentation to spot other authentication patterns provided. We will now use client id and client secret from an SP. You can create a new SP quickly with Azure CLI:

# create a new Service Principal
az ad sp create-for-rbac --name sp-aca-terraform -ojson

{
  "appId": "00000000-0000-0000-0000-000000000000",
  "displayName": "sp-aca-terraform",
  "password": "seporfkwpoijfrwefnmwwfw",
  "tenant": "00000000-0000-0000-0000-000000000000"
}

With the SP in place, we must create a role assignment to allow infrastructure provisioning. Remember that actual permissions will differ due to the tasks you instruct Terraform to perform. For now, let’s assign the Contributor role to the SP on the scope of the desired Azure subscription:

# Assign the Contributor role to the SP
subId=$(az account show --query "id" -otsv)

# replace <APP_ID> with appId from the newly created SP
az role assignment create --scope /subscriptions/$subId --role Contributor --assignee <APP_ID>

Finally, let’s set necessary values as environment variables so that Terraform can pick them up:

# replace TENANT_ID, APP_ID, and PASSWORD with corresponding values from SP
export ARM_SUBSCRIPTION_ID=$subId
export ARM_TENANT_ID=<TENANT_ID>
export ARM_CLIENT_ID=<APP_ID>
export ARM_CLIENT_SECRET=<PASSWORD>

In central systems like GitHub Actions or Azure DevOps Pipelines, we should always use secrets to persist sensitive configuration values like client secret in this case.

Create Locals and Variables

Although we could provision our infrastructure without specifying neither variables nor locals, it’s a good practice, and we can cover some Terraform fundamentals on the go. To set the desired Azure Region used for all resources when applying the project, we can use a variable. Add the following code to variables.tf:

variable "location" {
  type = string
  default = "westeurope"
  description = "Desired Azure Region"
}

In contrast to variables, locals can’t be changed when applying a project with Terraform. We can think of them as local variables in the scope of a method. For demonstration purposes, we’ll create a local variable called tags in locals.tf and use it to set Azure Tags on every resource we’re going to describe:

locals {
  tags = {
    "com.thorstenhans.provisioner" = "terraform"
    "com.thorstenhans.responsible" = "[email protected]"
  }
}

Specify Resource Group and Log Analytics Workspace with AzureRM

Now that we have everything prepared, we can start coding your infrastructure. First, let’s describe all our dependencies (deps.tf). For ACA, we have two dependencies. We have to place all resources in an Azure Resource Group.

Additionally, we must provide a Log Analytics Workspace, which will store all logs generated by our application and its sidecar containers:

resource "azurerm_resource_group" "rg" {
  name      = "rg-aca-terraform"
  location  = var.location
  tags      = local.tags
}

resource "azurerm_log_analytics_workspace" "law" {
  name                = "law-aca-terraform"
  resource_group_name = azurerm_resource_group.rg.name
  location            = azurerm_resource_group.rg.location
  sku                 = "PerGB2018"
  retention_in_days   = 90
  tags                = local.tags
}

Hands-on: Use the AzAPI provider

Azure Container Apps and Azure Container App Environments are currently not supported by AzureRM. To describe both entities, we will use the new AzAPI provider.

Hint: VSCode extension for the AzAPI provider

If you’re using VSCode, you should install the Terraform AzApi Provider extension provided by Microsoft. The extension will boost your developer productivity because you get

  • code completion for specifying resource types
  • code completion for selecting resource API versions
  • code completion for specifying properties on individual resources (everything that goes into the body field of an actual azapi_resource)

The Terraform AzApi Provider extension for VSCode

Describe the Azure Container App Environment with AzAPI

The ACA Environment will be the first resource we specify using the AzAPI provider. As the following snippet outlines, we specify the resource type as Microsoft.App/managedEnvironments@2022-03-01and pull the necessary information from the AzureRM resources described in the previous paragraphs. Add the following code to aca.tf:

resource "azapi_resource" "aca_env" {
  type      = "Microsoft.App/managedEnvironments@2022-03-01"
  parent_id = azurerm_resource_group.rg.id
  location  = azurerm_resource_group.rg.location
  name      = "aca-env-terraform"
  tags      = local.tags
  
  body   = jsonencode({
    properties = {
      appLogsConfiguration = {
        destination               = "log-analytics"
        logAnalyticsConfiguration = {
          customerId = azurerm_log_analytics_workspace.law.workspace_id
          sharedKey  = azurerm_log_analytics_workspace.law.primary_shared_key
        }
      }
    }
 })
}

Describe the Azure Container Apps with AzAPI

Having the ACA Environment specified, we can move on and take care of the actual container apps. For the sake of this article, we’re going to create two container apps.

Let’s first create a new variable (in variables.tf), so we can instruct Terraform to provision multiple container apps (each with an individual configuration) without repeating ourselves (DRY):

variable "container_apps" {
  type = list(object({
    name = string
    image = string
    tag = string
    containerPort = number
    ingress_enabled = bool
    min_replicas = number
    max_replicas = number
    cpu_requests = number
    mem_requests = string
  }))

  default = [ {
   image = "thorstenhans/gopher"
   name = "herogopher"
   tag = "hero"
   containerPort = 80
   ingress_enabled = true
   min_replicas = 1
   max_replicas = 2
   cpu_requests = 0.5
   mem_requests = "1.0Gi"
  },
  {
   image = "thorstenhans/gopher"
   name = "devilgopher"
   tag = "devil"
   containerPort = 80
   ingress_enabled = true
   min_replicas = 1
   max_replicas = 2
   cpu_requests = 0.5
   mem_requests = "1.0Gi"
  }] 
}

Finally, we can create another azapi_resource in aca.tf. This time, we set its type to Microsoft.App/containerApps@2022-03-01 and use the Terraform for_each expression to iterate over the container_apps variable:

resource "azapi_resource" "aca" {
  for_each  = { for ca in var.container_apps: ca.name => ca}
  type      = "Microsoft.App/containerApps@2022-03-01"
  parent_id = azurerm_resource_group.rg.id
  location  = azurerm_resource_group.rg.location
  name      = each.value.name
  
  body = jsonencode({
    properties: {
      managedEnvironmentId = azapi_resource.aca_env.id
      configuration = {
        ingress = {
          external = each.value.ingress_enabled
          targetPort = each.value.ingress_enabled?each.value.containerPort: null
        }
      }
      template = {
        containers = [
          {
            name = "main"
            image = "${each.value.image}:${each.value.tag}"
            resources = {
              cpu = each.value.cpu_requests
              memory = each.value.mem_requests
            }
          }         
        ]
        scale = {
          minReplicas = each.value.min_replicas
          maxReplicas = each.value.max_replicas
        }
      }
    }
  })
  tags = local.tags
}

Provision Azure Container Apps with Terraform

Having all resources in place, we can provision the infrastructure. Let’s execute terraform apply. Terraform CLI will present the execution plan that outlines which resources will be created in Azure.At this point, we’ve to confirm the plan, and we’ll see Terraform provisioning our infrastructure in Azure.

As soon as provision has finished you can again use Azure CLI and grab the FQDNs for the newly provisioned container apps:

# list FQDNS for container apps
az containerapp list -g rg-aca-terraform \
  --query="[].{FQDN:properties.configuration.ingress.fqdn}" \
  -otable

# FQDN
# --------------------------------------------------------------------
# herogopher.wonderfulbeach-762ce195.westeurope.azurecontainerapps.io
# devilgopher.wonderfulbeach-762ce195.westeurope.azurecontainerapps.io

What we’ve covered in this article

Throughout the article, we covered quite some topics:

  • 💡Understand what the AzAPI provider is and when it’s an excellent addition to the AzureRM provider
  • 🔒 Authenticated with Azure using a Service Principal
  • 🧬 Described all infrastructure components using Terraform and leveraged language features such as variables, locals, and expressions (for_each)
  • ⚙️ (optionally) used the VS Code extension for AzAPI to drive productivity and get precise code completion
  • 🚀 Provisioned Azure Container Apps with Terraform

Conclusion

It’s a bummer that AzureRM providers still lack support for Azure Container Apps. Luckily, the new AzAPI provider brings support for all Azure ARM API entities. We can finally provision Azure Container Apps with Terraform by using both providers. I use the AzAPI provider for way more use-cases than just ACA. It’s super handy, and with the corresponding VS Code extension, it’s super convenient to describe all parts of a bigger infrastructure.