- Published on
Azure Container Apps infrastructure
- Authors
- Name
- Włodzimierz Kesler
- Name
- Kornel Warwas
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
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 Portal
This is how the applied configuration looks like on the Azure portal: