Published on

Azure Container Apps infrastructure

5 min read
Authors
  • avatar
    Name
    Włodzimierz Kesler
    Twitter
  • avatar
    Name
    Kornel Warwas
    Twitter

Overview

Intro

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
azure-container-apps-infra-diagram

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: https://registry.terraform.io/providers/Azure/azapi/latest/docs

More about creating Azure Container Apps in Terraform: https://techcommunity.microsoft.com/t5/fasttrack-for-azure/can-i-create-an-azure-container-apps-in-terraform-yes-you-can/ba-p/3570694

Microsoft documentation for managing Azure Container Apps Environment using the AzAPI Terraform provider: https://learn.microsoft.com/en-us/azure/templates/microsoft.app/managedenvironments?pivots=deployment-language-terraform

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 = azurerm_resource_group.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              = azurerm_resource_group.resource_group.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           = azurerm_resource_group.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 = azurerm_resource_group.resource_group.name
  location            = azurerm_resource_group.resource_group.location
  tags                = local.tags
}

resource "azurerm_role_assignment" "role_assign" {
  scope                            = azurerm_container_registry.container_registry.id
  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: https://registry.terraform.io/providers/Azure/azapi/latest/docs/guides/frequently_asked_questions

Terraform Cloud

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

azure-container-apps-infra-terraform-cloud

Azure Portal

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

azure-container-apps-infra-azure-portal