Published on

Azure Container Apps infrastructure

5 min read
  • avatar
    Włodzimierz Kesler
  • avatar
    Kornel Warwas



This article shows how to create the required Azure infrastructure to run our applications under Azure Container Apps. For the Infrastructure as Code solution, we use Terraform scripts with Terraform Cloud free plan for state storage.

Required Azure resources

To deploy our applications under Azure Container Apps, we need additional Azure resources:

  • Resource Group
  • Container Apps Environment - an isolation boundary around groups of container apps
  • Log Analytics workspace - logs management, assign to Container Apps Environment
  • Azure Container Registry (ACR) - storage for docker images of our applications
  • User-Assigned Managed Identity - identity used by Azure Container Apps to pull images from ACR

The Terraform script will not manage Container Apps resources in our solution. It is possible to create them using Terraform, but we prefer using a PowerShell script. In the case of Azure Container App, the Container App definition contains a container image definition and application settings definition (via environment variables and secrets). That means that the Container App definition will be changed very often, probably during each deployment of a new version of our applications. Therefore, this is an application deployment process, and Terraform role is infrastructure management. Similarly to K8s, where Terraform is not used in most cases for deploying K8s objects, this is a job for other mechanisms like Kubernetes manifests or Helm Scripts.

Terraform AzAPI provider

At the moment of creating this article, the official Azure Terraform provider, the AzureRM provider, with the current version of 3.26.0, is not supporting Azure Container Apps nor Azure Container Apps Environment yet. However, while waiting for this support, it is possible to manage those Azure resources with Terraform using the Terraform AzAPI provider. The AzAPI provider is a very thin layer on top of the Azure ARM REST APIs. It can be used as an alternative solution for managing Azure resources that are not yet or may never be supported by the AzureRM provider.

More about the AzAPI provider:

More about creating Azure Container Apps in Terraform:

Microsoft documentation for managing Azure Container Apps Environment using the AzAPI Terraform provider:

In summary, in our Terraform script, we use two different providers:

  • AzAPI provider for managing Azure Container Apps Environment resource (inside Azure API called Managed Environment)
  • AzureRM provider for managing all other Azure resources

Terraform code

For Terraform state storage and script execution, we use Terraform Cloud service with a free plan. Therefore, it is essential to provide Azure tenant and subscription data along with the Service Principal data in configuration for every provider. Terraform Cloud agent will use this Service Principal to access and modify resources under a given Azure tenant and subscription.

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "3.26.0"
    azapi = {
      source  = "Azure/azapi"
      version = "1.0.0"
  cloud {
    organization = "dev-devops"

    workspaces {
      name = "azure_container_apps-dev"

provider "azurerm" {
  features {}
  subscription_id = "<your-azure-subscription-id>"
  client_id       = "<your-azure-service-principal-client-id>"
  client_secret   = "<your-azure-service-principal-client-secret>"
  tenant_id       = "<your-azure-tenant-id>"

provider "azapi" {
  subscription_id = "<your-azure-subscription-id>"
  client_id       = "<your-azure-service-principal-client-id>"
  client_secret   = "<your-azure-service-principal-client-secret>"
  tenant_id       = "<your-azure-tenant-id>"

locals {
  location             = "North Europe"
  organization_name    = "dev-devops"
  project_name         = "aca"
  environment_name     = "dev"
  resource_name_suffix = "${local.organization_name}-${local.project_name}-${local.environment_name}"
  tags = {
    Owner        = local.organization_name
    Env          = local.environment_name
    WorkloadName = local.project_name
    Source       = "Terraform"
  log_settings = {
    sku               = "PerGB2018"
    retention_in_days = 30
    daily_quota_gb    = 1

resource "azurerm_resource_group" "resource_group" {
  name     = "rg-${local.resource_name_suffix}"
  location = local.location
  tags     = local.tags

resource "azurerm_log_analytics_workspace" "log_analytics_workspace" {
  name                = "log-${local.resource_name_suffix}"
  resource_group_name =
  location            = azurerm_resource_group.resource_group.location
  sku                 = local.log_settings.sku
  retention_in_days   = local.log_settings.retention_in_days
  daily_quota_gb      = local.log_settings.daily_quota_gb
  tags                = local.tags

resource "azapi_resource" "resource_managed_environment" {
  type                   = "Microsoft.App/managedEnvironments@2022-03-01"
  name                   = "menv-${local.resource_name_suffix}"
  location               = azurerm_resource_group.resource_group.location
  parent_id              =
  tags                   = local.tags
  response_export_values = ["properties.staticIp", "properties.defaultDomain"]
  body = jsonencode({
    properties = {
      appLogsConfiguration = {
        destination = "log-analytics"
        logAnalyticsConfiguration = {
          customerId = azurerm_log_analytics_workspace.log_analytics_workspace.workspace_id
          sharedKey  = azurerm_log_analytics_workspace.log_analytics_workspace.primary_shared_key
      zoneRedundant = false
  ignore_missing_property = true

resource "azurerm_container_registry" "container_registry" {
  name                          = "cr${replace(local.resource_name_suffix, "-", "")}"
  resource_group_name           =
  location                      = azurerm_resource_group.resource_group.location
  sku                           = "Basic"
  admin_enabled                 = false
  public_network_access_enabled = true
  network_rule_bypass_option    = "None"
  tags                          = local.tags

resource "azurerm_user_assigned_identity" "user_assigned_identity" {
  name                = "id-${local.resource_name_suffix}"
  resource_group_name =
  location            = azurerm_resource_group.resource_group.location
  tags                = local.tags

resource "azurerm_role_assignment" "role_assign" {
  scope                            =
  role_definition_name             = "AcrPull"
  principal_id                     = azurerm_user_assigned_identity.user_assigned_identity.principal_id
  skip_service_principal_aad_check = true

Please mind the ignore_missing_property = true attribute setting for the managed environment resource. Adding this line is necessary to avoid problems with Terraform plan still founding changes after applying the configuration. More about this:

Terraform Cloud

This is how the applied configuration looks like inside Terraform Cloud workspace:


Azure Portal

This is how the applied configuration looks like on the Azure portal: