Skip to content

Latest commit

Β 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

☁️ Azure Container Group Terraform Module

Provisions a single Azure Container Instances container group (azurerm_container_group) β€” one or more containers scheduled together, sharing a lifecycle, network, and storage β€” hardened by default and targeting hashicorp/azurerm ~> 4.0.

Terraform azurerm Module Version Type Resources


🧩 Overview

  • πŸ“¦ Deploys one azurerm_container_group β€” a set of containers that run on the same host, share an IP, and start and stop together.
  • 🧱 Renders one or more container blocks from a keyed map, plus optional init_container blocks that run to completion first.
  • πŸ” Locks exposure down by default: the minimal call produces a group with no inbound IP (ip_address_type = "None").
  • 🌐 Supports private VNet injection (subnet_ids + ip_address_type = "Private") as the hardened path for reachable workloads, and public exposure as a deliberate opt-in.
  • πŸͺͺ Prefers a managed identity {} for pulling private images and reaching Key Vault over embedded registry credentials.
  • πŸ“ˆ Ships container logs to Log Analytics through an optional diagnostics block.
  • 🧩 Exposes per-container ports, liveness_probe / readiness_probe, security, and volume (Azure File, empty_dir, secret, git_repo) blocks.

πŸ’‘ Why it matters: A container group is the smallest deployable unit in Azure Container Instances, and nearly every field on it is force-new. Getting the network exposure, identity, and secret handling right at authoring time β€” instead of after a public IP is already live β€” is the difference between a serverless container that is safe by construction and one that is a standing liability.

❀️ Support this project

If this module saves you time, please consider supporting its continued development:


πŸ—ΊοΈ Where this fits in the family

flowchart LR
  classDef sib fill:#E6EEF5,stroke:#8AA9C4,color:#1A2A3A;
  RG["terraform-azurerm-resource-group"]:::sib
  ACR["terraform-azurerm-container-registry"]:::sib
  UAI["terraform-azurerm-user-assigned-identity"]:::sib
  VNET["terraform-azurerm-virtual-network"]:::sib
  LAW["terraform-azurerm-log-analytics-workspace"]:::sib
  CG["terraform-azurerm-container-group"]
  RG -->|"resource_group_name + location"| CG
  ACR -->|"private image source"| CG
  UAI -->|"identity_ids for ACR pull"| CG
  VNET -->|"subnet_ids (private injection)"| CG
  LAW -->|"diagnostics workspace"| CG
  style CG fill:#0078D4,color:#fff
Loading

🧬 What this module builds

flowchart TB
  classDef io fill:#E6EEF5,stroke:#8AA9C4,color:#1A2A3A;
  subgraph inputs["Inputs"]
    direction TB
    I1["containers (map) + init_containers"]:::io
    I2["identity (managed identity)"]:::io
    I3["image_registry_credentials"]:::io
    I4["ip_address_type + subnet_ids"]:::io
    I5["diagnostics (Log Analytics)"]:::io
  end
  CG["azurerm_container_group.this"]
  subgraph outputs["Outputs"]
    direction TB
    O1["id"]:::io
    O2["name"]:::io
    O3["ip_address + fqdn"]:::io
    O4["identity_principal_id"]:::io
  end
  I1 -->|"container / init_container blocks"| CG
  I2 -->|"identity block"| CG
  I3 -->|"image_registry_credential blocks"| CG
  I4 -->|"network exposure"| CG
  I5 -->|"diagnostics block"| CG
  CG -->|"emits"| O1
  CG -->|"emits"| O2
  CG -->|"emits"| O3
  CG -->|"emits"| O4
  style CG fill:#004578,color:#fff
Loading

Resource inventory

Resource Role Cardinality
azurerm_container_group.this The keystone container group 1
container (block) A running container 1..n (keyed map, min 1)
init_container (block) A run-to-completion setup container 0..n (keyed map)
identity (block) System- / user-assigned managed identity 0..1
image_registry_credential (block) Private-registry pull credential 0..n (keyed map)
diagnostics.log_analytics (block) Container-log shipping to Log Analytics 0..1
dns_config (block) Custom resolver settings 0..1

βœ… Provider / Versions

Requirement Value
Terraform >= 1.12.0
hashicorp/azurerm ~> 4.0 (>= 4.0, < 5.0)
Provider block None in this module β€” the caller configures provider "azurerm" { features {} }, auth, subscription, and region

Schema notes that bite (verified against the live provider schema):

  • The container group is effectively immutable. Changing name, resource_group_name, location, os_type, sku, zones, subnet_ids, restart_policy, priority, dns_name_label, or any container/init-container definition forces the whole group to be replaced.
  • At least one container is required. The containers map must be non-empty; this module fails at plan time with a clear message if it is.
  • ip_address_type gates several fields. "Private" requires subnet_ids and rejects a dns_name_label; "Public" is what a dns_name_label and a public FQDN attach to; "None" (the default) exposes no inbound IP.
  • dns_name_label_reuse_policy only applies when a dns_name_label is set.
  • Secret-bearing fields are redacted by the provider, not by variable-level sensitivity β€” see Architecture Notes.
  • A delegated subnet is required for private injection (Microsoft.ContainerInstance/containerGroups delegation).

πŸ”‘ Required Azure RBAC Roles / Permissions

  • Create/manage the container group: Contributor on the target resource group, or a custom role granting Microsoft.ContainerInstance/containerGroups/* scoped to that resource group.
  • Private VNet injection (ip_address_type = "Private"): Network Contributor (or Microsoft.Network/virtualNetworks/subnets/join/action) on the target subnet.
  • Identity-based image pulls: the referenced user-assigned identity needs AcrPull on the source registry; the deploying principal needs Managed Identity Operator on that identity to assign it.
  • Customer-managed-key encryption: the identity in key_vault_user_assigned_identity_id needs get / wrapKey / unwrapKey on the Key Vault key (Key Vault Crypto Service Encryption User under RBAC authorization).

Azure Prerequisites

  • An existing resource group in a supported US Azure region.
  • The Microsoft.ContainerInstance resource provider registered on the target subscription.
  • For private injection: an existing, delegated subnet in the same region.
  • For identity-based pulls: an existing user-assigned identity holding AcrPull on the registry.
  • For diagnostics: an existing Log Analytics workspace and its shared key, provisioned out of band.
  • The caller configures the provider "azurerm" { features {} } block, auth, and subscription; this module declares none of them.

πŸ“ Module Structure

terraform-azurerm-container-group/
β”œβ”€β”€ providers.tf     # required_version >= 1.12.0; azurerm ~> 4.0; no provider block
β”œβ”€β”€ variables.tf     # deeply-typed object() schemas + secure defaults + tags/timeouts tail
β”œβ”€β”€ main.tf          # azurerm_container_group.this β€” dynamic container/init_container/identity/... blocks
β”œβ”€β”€ outputs.tf       # id first, then name, ip_address, fqdn, identity principal/tenant
β”œβ”€β”€ README.md        # this document
β”œβ”€β”€ SCOPE.md         # the cross-module contract
β”œβ”€β”€ LICENSE          # MIT, Copyright (c) 2026 Casey Wood
└── .gitignore       # canonical library ignore set

βš™οΈ Quick Start

The smallest real call runs a single container with no inbound IP. The caller configures the provider, auth, and the mandatory features {} block.

provider "azurerm" {
  features {}
}

module "container_group" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-container-group.git?ref=v1.0.0"

  name                = "aci-batch-01"
  resource_group_name = "rg-workloads-eastus"
  location            = "eastus"
  os_type             = "Linux"

  containers = {
    worker = {
      name   = "worker"
      image  = "mcr.microsoft.com/azuredocs/aci-helloworld:latest"
      cpu    = 0.5
      memory = 1.0
    }
  }

  tags = {
    environment = "prod"
    owner       = "platform-team"
  }
}

πŸ”’ With ip_address_type left at its "None" default, this group has no inbound IP address. Outbound connectivity still works β€” ideal for batch/job workloads.


πŸ”Œ Cross-Module Contract

Consumes

Input Type Typical source
resource_group_name string terraform-azurerm-resource-group (name)
location string caller / resource-group module (location)
subnet_ids set(string) terraform-azurerm-virtual-network (subnet id)
identity.identity_ids / image_registry_credentials[*].user_assigned_identity_id string terraform-azurerm-user-assigned-identity (id)
diagnostics.log_analytics.workspace_id string terraform-azurerm-log-analytics-workspace (workspace_id)
key_vault_key_id string terraform-azurerm-key-vault (key id)

Emits

Output Description
id Container group Resource ID (emitted first)
name Container group name
ip_address Allocated IP (empty when ip_address_type = "None")
fqdn Public FQDN (only for a public IP with a dns_name_label)
identity_principal_id Managed-identity principal ID, or null
identity_tenant_id Managed-identity tenant ID, or null

πŸ“š Example Library

1 Β· Minimal call β€” a single container, no inbound IP
module "job" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-container-group.git?ref=v1.0.0"

  name                = "aci-job-01"
  resource_group_name = "rg-workloads-eastus"
  location            = "eastus"
  os_type             = "Linux"

  containers = {
    main = {
      name   = "main"
      image  = "mcr.microsoft.com/azuredocs/aci-helloworld:latest"
      cpu    = 0.5
      memory = 1.0
    }
  }
}

πŸ”’ ip_address_type defaults to "None": the group is reachable outbound only. This is the secure baseline.

2 Β· Public-facing container with a DNS label
module "web" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-container-group.git?ref=v1.0.0"

  name                = "aci-web-01"
  resource_group_name = "rg-web-eastus"
  location            = "eastus"
  os_type             = "Linux"

  ip_address_type = "Public"
  dns_name_label  = "my-aci-web-01"

  containers = {
    nginx = {
      name   = "nginx"
      image  = "mcr.microsoft.com/azuredocs/aci-helloworld:latest"
      cpu    = 1.0
      memory = 1.5
      ports = {
        http = { port = 80, protocol = "TCP" }
      }
    }
  }
}

⚠️ ip_address_type = "Public" allocates a public IP and exposes the declared ports to the internet. This is a deliberate opt-in; the FQDN becomes my-aci-web-01.<region>.azurecontainer.io.

3 Β· Multi-container sidecar pattern
module "app_with_sidecar" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-container-group.git?ref=v1.0.0"

  name                = "aci-app-01"
  resource_group_name = "rg-app-eastus"
  location            = "eastus"
  os_type             = "Linux"

  containers = {
    app = {
      name   = "app"
      image  = "myregistry.azurecr.io/app:1.4.0"
      cpu    = 1.0
      memory = 2.0
      ports  = { http = { port = 8080 } }
    }
    log_forwarder = {
      name   = "log-forwarder"
      image  = "myregistry.azurecr.io/fluent-bit:2.2"
      cpu    = 0.25
      memory = 0.5
    }
  }
}

ℹ️ Containers in a group share the same network namespace, so the sidecar reaches the app over localhost.

4 Β· Private VNet-injected group (the hardened path)
module "private_app" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-container-group.git?ref=v1.0.0"

  name                = "aci-private-01"
  resource_group_name = "rg-app-eastus"
  location            = "eastus"
  os_type             = "Linux"

  ip_address_type = "Private"
  subnet_ids      = [var.aci_subnet_id]

  containers = {
    api = {
      name   = "api"
      image  = "myregistry.azurecr.io/api:2.0.0"
      cpu    = 1.0
      memory = 2.0
      ports  = { https = { port = 443 } }
    }
  }
}

πŸ”’ The group receives a private IP inside the delegated subnet and is unreachable from the public internet. The subnet must carry the Microsoft.ContainerInstance/containerGroups delegation.

5 Β· Pull from ACR with a user-assigned managed identity
module "identity_pull" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-container-group.git?ref=v1.0.0"

  name                = "aci-idpull-01"
  resource_group_name = "rg-app-eastus"
  location            = "eastus"
  os_type             = "Linux"

  identity = {
    type         = "UserAssigned"
    identity_ids = [var.pull_identity_id]
  }

  image_registry_credentials = {
    acr = {
      server                    = "myregistry.azurecr.io"
      user_assigned_identity_id = var.pull_identity_id
    }
  }

  containers = {
    api = {
      name   = "api"
      image  = "myregistry.azurecr.io/api:2.0.0"
      cpu    = 1.0
      memory = 2.0
    }
  }
}

πŸ”’ No registry password appears anywhere. The identity needs AcrPull on the registry. This is the preferred way to pull private images.

6 Β· Registry pull with username/password (opt-out)
module "password_pull" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-container-group.git?ref=v1.0.0"

  name                = "aci-pwpull-01"
  resource_group_name = "rg-app-eastus"
  location            = "eastus"
  os_type             = "Linux"

  image_registry_credentials = {
    acr = {
      server   = "myregistry.azurecr.io"
      username = "pull-sp"
      password = var.registry_password # provision out of band; never commit
    }
  }

  containers = {
    api = {
      name   = "api"
      image  = "myregistry.azurecr.io/api:2.0.0"
      cpu    = 1.0
      memory = 2.0
    }
  }
}

⚠️ Prefer identity-based pulls (Example 5). Where a password is unavoidable, pass a reference (e.g. a data.azurerm_key_vault_secret) β€” never a literal β€” and let the provider redact it.

7 Β· Secret environment variables and an Azure File volume
module "stateful" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-container-group.git?ref=v1.0.0"

  name                = "aci-stateful-01"
  resource_group_name = "rg-app-eastus"
  location            = "eastus"
  os_type             = "Linux"

  containers = {
    app = {
      name   = "app"
      image  = "myregistry.azurecr.io/app:1.4.0"
      cpu    = 1.0
      memory = 2.0

      environment_variables = {
        LOG_LEVEL = "info"
      }
      secure_environment_variables = {
        DB_CONNECTION = var.db_connection_string # redacted by the provider
      }

      volumes = {
        data = {
          name                 = "data"
          mount_path           = "/mnt/data"
          share_name           = "appdata"
          storage_account_name = var.storage_account_name
          storage_account_key  = var.storage_account_key # provision out of band
        }
      }
    }
  }
}

πŸ”’ Put secrets in secure_environment_variables, never environment_variables β€” the latter is visible in state and API responses. storage_account_key and secret volumes are redacted by the provider.

8 Β· Liveness and readiness probes
module "probed" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-container-group.git?ref=v1.0.0"

  name                = "aci-probed-01"
  resource_group_name = "rg-app-eastus"
  location            = "eastus"
  os_type             = "Linux"

  containers = {
    api = {
      name   = "api"
      image  = "myregistry.azurecr.io/api:2.0.0"
      cpu    = 1.0
      memory = 2.0
      ports  = { http = { port = 8080 } }

      liveness_probe = {
        initial_delay_seconds = 15
        period_seconds        = 20
        failure_threshold     = 3
        http_get = {
          path   = "/healthz"
          port   = 8080
          scheme = "Http"
        }
      }

      readiness_probe = {
        initial_delay_seconds = 5
        period_seconds        = 10
        http_get = {
          path = "/ready"
          port = 8080
        }
      }
    }
  }
}

ℹ️ A failed liveness probe restarts the container; a failed readiness probe removes it from the group's exposed endpoints until it recovers.

9 Β· Init container that runs first
module "with_init" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-container-group.git?ref=v1.0.0"

  name                = "aci-init-01"
  resource_group_name = "rg-app-eastus"
  location            = "eastus"
  os_type             = "Linux"

  init_containers = {
    migrate = {
      name     = "migrate"
      image    = "myregistry.azurecr.io/migrate:1.4.0"
      commands = ["/bin/sh", "-c", "./run-migrations.sh"]
      secure_environment_variables = {
        DB_CONNECTION = var.db_connection_string
      }
    }
  }

  containers = {
    app = {
      name   = "app"
      image  = "myregistry.azurecr.io/app:1.4.0"
      cpu    = 1.0
      memory = 2.0
    }
  }
}

ℹ️ Init containers run to completion, in key order, before any main container starts β€” use them for schema migrations or asset preparation.

10 Β· Confidential compute SKU
module "confidential" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-container-group.git?ref=v1.0.0"

  name                = "aci-cc-01"
  resource_group_name = "rg-secure-eastus"
  location            = "eastus"
  os_type             = "Linux"
  sku                 = "Confidential"

  containers = {
    app = {
      name   = "app"
      image  = "myregistry.azurecr.io/app:1.4.0"
      cpu    = 1.0
      memory = 2.0
    }
  }
}

πŸ”’ sku = "Confidential" runs the group in a hardware-based trusted execution environment. Availability is region- and image-dependent; changing the SKU is force-new.

11 Β· Spot-priority run-to-completion job
module "spot_job" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-container-group.git?ref=v1.0.0"

  name                = "aci-spot-01"
  resource_group_name = "rg-batch-eastus"
  location            = "eastus"
  os_type             = "Linux"

  sku            = "Dedicated"
  priority       = "Spot"
  restart_policy = "OnFailure"

  containers = {
    batch = {
      name     = "batch"
      image    = "myregistry.azurecr.io/batch:3.1.0"
      cpu      = 2.0
      memory   = 4.0
      commands = ["/bin/sh", "-c", "./process-queue.sh"]
    }
  }
}

⚠️ priority = "Spot" workloads can be evicted at any time. Use only for interruption-tolerant jobs and pair with restart_policy = "OnFailure" or "Never".

12 Β· Customer-managed-key encryption
module "cmk" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-container-group.git?ref=v1.0.0"

  name                = "aci-cmk-01"
  resource_group_name = "rg-secure-eastus"
  location            = "eastus"
  os_type             = "Linux"

  identity = {
    type         = "UserAssigned"
    identity_ids = [var.cmk_identity_id]
  }

  key_vault_key_id                    = var.kv_key_id
  key_vault_user_assigned_identity_id = var.cmk_identity_id

  containers = {
    app = {
      name   = "app"
      image  = "myregistry.azurecr.io/app:1.4.0"
      cpu    = 1.0
      memory = 2.0
    }
  }
}

πŸ”’ With a Key Vault key and an identity that can wrap/unwrap it, the group is encrypted at rest with a customer-managed key instead of the platform key. Omit both to stay on the platform key.

13 Β· Ship container logs to Log Analytics
module "observed" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-container-group.git?ref=v1.0.0"

  name                = "aci-observed-01"
  resource_group_name = "rg-app-eastus"
  location            = "eastus"
  os_type             = "Linux"

  diagnostics = {
    log_analytics = {
      workspace_id  = var.law_workspace_id
      workspace_key = var.law_workspace_key # provision out of band
      log_type      = "ContainerInsights"
    }
  }

  containers = {
    app = {
      name   = "app"
      image  = "myregistry.azurecr.io/app:1.4.0"
      cpu    = 1.0
      memory = 2.0
    }
  }
}

πŸ”’ workspace_key is a secret; the provider redacts it. Source it from a Key Vault reference and never commit it.

14 Β· A worker pool via for_each at scale
locals {
  workers = {
    for i in range(1, 4) : "worker-${i}" => {
      name   = "worker-${i}"
      image  = "myregistry.azurecr.io/worker:2.0.0"
      cpu    = 0.5
      memory = 1.0
    }
  }
}

module "worker_group" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-container-group.git?ref=v1.0.0"

  name                = "aci-workers-01"
  resource_group_name = "rg-batch-eastus"
  location            = "eastus"
  os_type             = "Linux"

  containers = local.workers
}

ℹ️ Because containers is a keyed map, adding or removing one worker never re-indexes the others β€” the diff stays surgical.

15 Β· πŸ—οΈ End-to-end composition

Wires a resource group, a private registry, a user-assigned identity (with AcrPull), and a Log Analytics workspace into a single identity-authenticated, observed container group.

provider "azurerm" {
  features {}
}

module "rg" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
  name     = "rg-platform-eastus"
  location = "eastus"
}

module "identity" {
  source              = "git::https://github.com/microsoftexpert/terraform-azurerm-user-assigned-identity.git?ref=v1.0.0"
  name                = "id-aci-pull"
  resource_group_name = module.rg.name
  location            = module.rg.location
}

module "acr" {
  source              = "git::https://github.com/microsoftexpert/terraform-azurerm-container-registry.git?ref=v1.0.0"
  name                = "acrplatformeastus"
  resource_group_name = module.rg.name
  location            = module.rg.location
}

module "law" {
  source              = "git::https://github.com/microsoftexpert/terraform-azurerm-log-analytics-workspace.git?ref=v1.0.0"
  name                = "law-platform-eastus"
  resource_group_name = module.rg.name
  location            = module.rg.location
}

# Grant the identity pull rights on the registry (role-assignments module).
module "acr_pull" {
  source               = "git::https://github.com/microsoftexpert/terraform-azurerm-role-assignments.git?ref=v1.0.0"
  scope                = module.acr.id
  role_assignments = {
    pull = {
      role_definition_name = "AcrPull"
      principal_id         = module.identity.principal_id
    }
  }
}

module "container_group" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-container-group.git?ref=v1.0.0"

  name                = "aci-platform-01"
  resource_group_name = module.rg.name
  location            = module.rg.location
  os_type             = "Linux"

  identity = {
    type         = "UserAssigned"
    identity_ids = [module.identity.id]
  }

  image_registry_credentials = {
    acr = {
      server                    = module.acr.login_server
      user_assigned_identity_id = module.identity.id
    }
  }

  containers = {
    api = {
      name   = "api"
      image  = "${module.acr.login_server}/api:2.0.0"
      cpu    = 1.0
      memory = 2.0
      ports  = { http = { port = 8080 } }

      liveness_probe = {
        initial_delay_seconds = 15
        http_get = { path = "/healthz", port = 8080 }
      }
    }
  }

  diagnostics = {
    log_analytics = {
      workspace_id  = module.law.workspace_id
      workspace_key = module.law.primary_shared_key
      log_type      = "ContainerInsights"
    }
  }

  tags = {
    environment = "prod"
    owner       = "platform-team"
  }

  depends_on = [module.acr_pull]
}

πŸ”’ The group pulls from a private registry with a managed identity (no password), ships logs to Log Analytics, and exposes no public IP. The depends_on ensures the AcrPull assignment lands before the first pull.


πŸ“₯ Inputs

Identity & placement (required)

Name Type Description
name string Container group name (force-new)
resource_group_name string Existing resource group (force-new)
location string Azure region (force-new)
os_type string Linux or Windows (force-new)
containers map(object) Keyed map of container definitions; at least one required

Networking & exposure (secure defaults)

Name Type Default Description
ip_address_type string "None" None / Private / Public
subnet_ids set(string) [] Required for Private
dns_name_label string null Public FQDN label (Public only)
dns_name_label_reuse_policy string null Reuse policy for the DNS label
exposed_ports map(object) null Explicit group-level ports (else derived)

Platform, encryption & extras

Name Type Default Description
sku string "Standard" Standard / Dedicated / Confidential
priority string null Regular / Spot
restart_policy string "Always" Always / Never / OnFailure
zones set(string) [] Availability zones
key_vault_key_id string null CMK key for encryption at rest
key_vault_user_assigned_identity_id string null Identity granted access to the CMK key
identity object null Managed identity
image_registry_credentials map(object) {} Private-registry pull credentials
init_containers map(object) {} Run-to-completion init containers
dns_config object null Custom resolver settings
diagnostics object null Log Analytics diagnostics
tags map(string) {} Resource tags
timeouts object null Create/read/update/delete timeouts
Full object() schemas
variable "containers" {
  type = map(object({
    name         = string
    image        = string
    cpu          = number
    memory       = number
    cpu_limit    = optional(number)
    memory_limit = optional(number)
    commands     = optional(list(string))

    environment_variables        = optional(map(string), {})
    secure_environment_variables = optional(map(string), {}) # redacted by the provider

    ports = optional(map(object({
      port     = number
      protocol = optional(string, "TCP")
    })), {})

    liveness_probe = optional(object({
      exec                  = optional(list(string))
      failure_threshold     = optional(number)
      initial_delay_seconds = optional(number)
      period_seconds        = optional(number)
      success_threshold     = optional(number)
      timeout_seconds       = optional(number)
      http_get = optional(object({
        path         = optional(string)
        port         = optional(number)
        scheme       = optional(string)
        http_headers = optional(map(string))
      }))
    }))

    readiness_probe = optional(object({
      exec                  = optional(list(string))
      failure_threshold     = optional(number)
      initial_delay_seconds = optional(number)
      period_seconds        = optional(number)
      success_threshold     = optional(number)
      timeout_seconds       = optional(number)
      http_get = optional(object({
        path         = optional(string)
        port         = optional(number)
        scheme       = optional(string)
        http_headers = optional(map(string))
      }))
    }))

    security = optional(object({
      privilege_enabled = optional(bool, false)
    }))

    volumes = optional(map(object({
      name                 = string
      mount_path           = string
      read_only            = optional(bool, false)
      empty_dir            = optional(bool)
      share_name           = optional(string)
      storage_account_name = optional(string)
      storage_account_key  = optional(string) # redacted by the provider
      secret               = optional(map(string)) # redacted by the provider
      git_repo = optional(object({
        url       = string
        directory = optional(string)
        revision  = optional(string)
      }))
    })), {})
  }))
}

variable "identity" {
  type = object({
    type         = string # SystemAssigned | UserAssigned | "SystemAssigned, UserAssigned"
    identity_ids = optional(list(string), [])
  })
  default = null
}

variable "image_registry_credentials" {
  type = map(object({
    server                    = string
    user_assigned_identity_id = optional(string)
    username                  = optional(string)
    password                  = optional(string) # redacted by the provider
  }))
  default = {}
}

variable "diagnostics" {
  type = object({
    log_analytics = object({
      workspace_id  = string
      workspace_key = string # redacted by the provider
      log_type      = optional(string)
      metadata      = optional(map(string))
    })
  })
  default = null
}

🧾 Outputs

Output Description Kind
id The Azure Resource ID of the container group Passthrough
name The name of the container group Passthrough
location Azure region the resource is deployed in, in the canonical form Azure uses Passthrough
ip_address The IP address allocated to the container group (empty when ip_address_type is "None") Passthrough
fqdn The public fully-qualified domain name of the container group (set only when a dns_name_label is used with a public IP) Passthrough
identity_principal_id Principal (object) ID of the container group's managed identity, or null when no identity is configured Derived
identity_tenant_id Tenant ID of the container group's managed identity, or null when no identity is configured Derived
dns_label_is_reusable_by_anyone True when this group publishes a DNS label AND leaves its reuse policy at Azure's default, which is Unsecure - the least restrictive of five values, meaning ANYONE in ANY tenant may claim the label once this group is deleted Derived
key_rotation_requires_replacing_the_group True when customer-managed-key encryption is configured, and it carries a consequence that differs from the sibling modules in this suite Derived
is_publicly_reachable True when the group holds a public IP address Derived
restart_policy_will_rerun_a_completed_job True when the group restarts its containers on any exit, including a successful one - which is the default, and the wrong choice for a run-to-completion job Derived
is_evictable_spot_capacity True when the group runs on Spot capacity, which is cheaper and can be evicted at any time with no error Terraform will report Derived
spot_and_ip_address_are_mutually_exclusive Always true, and the rule lives where a schema search will not find it Constant
name_is_not_pattern_checked_by_the_provider Always true Constant
network_profile_id_is_deliberately_not_exposed Always true, and it is a decision rather than an omission Constant

No plaintext secret is emitted. Registry passwords, storage keys, secret volumes, secure environment variables, and the diagnostics workspace key are never surfaced as outputs.


🧠 Architecture Notes

  • The group is a replace-in-place unit. Because name, resource_group_name, location, os_type, sku, zones, subnet_ids, restart_policy, priority, dns_name_label, and every container/init-container definition are force-new, most changes replace the whole group. Plan changes accordingly β€” there is no in-place mutation of a running container's image.
  • for_each key stability. containers, init_containers, image_registry_credentials, per-container ports, and per-container volumes are keyed maps rendered with dynamic blocks. Choose keys that describe the entry's role and keep them stable so a plan never re-creates unrelated entries.
  • Secret handling is attribute-level, not variable-level. secure_environment_variables, volumes[*].storage_account_key, volumes[*].secret, image_registry_credentials[*].password, and diagnostics.log_analytics.workspace_key live inside variables that feed for_each / dynamic blocks. Terraform forbids a sensitive value as a for_each source, so this module cannot mark those enclosing variables sensitive without breaking rendering. Instead it relies on the provider's own attribute-level sensitivity β€” the provider redacts these fields in plan output and state diffs β€” and asks you to provision the values out of band (a Key Vault reference, a CI secret) rather than committing them.
  • Exposure is closed by default. ip_address_type defaults to "None". Reaching a container requires either private VNet injection ("Private" + subnet_ids) or an explicit public IP ("Public"). dns_name_label only attaches to a public IP.
  • Privilege is off by default. A container's security.privilege_enabled defaults to false; grant elevated host privileges only for a workload that genuinely needs them.
  • features {} dependence. The provider will not initialize without a caller-side provider "azurerm" { features {} } block. That block is the caller's, never the module's.

🧱 Design Principles

Concern Secure default (empty call) Opt-out (caller must type it)
Inbound network exposure ip_address_type = "None" (no inbound IP) "Private" (+ subnet_ids) or "Public"
Public DNS record no dns_name_label set dns_name_label (Public only)
Registry authentication managed identity (user_assigned_identity_id) username / password
Container privilege security.privilege_enabled = false set to true
Secrets in environment secure_environment_variables (redacted) plaintext environment_variables
Encryption at rest platform-managed key supply key_vault_key_id for a customer-managed key
Identity prefer a managed identity {} over embedded credentials omit the identity block

πŸš€ Runbook

# From the module folder β€” offline, no backend, no cloud:
terraform init -backend=false
terraform validate
terraform fmt -check
  • Pin the module with ?ref=v1.0.0 β€” never a branch.
  • This module is plan-only during authoring; a human runs terraform plan / apply from CI against real credentials.
  • The caller supplies provider "azurerm" { features {} }, auth, and subscription.

πŸ§ͺ Testing

The offline proof gate is what this module ships against:

Gate Command Proves
Format terraform fmt -check Canonical formatting
Init terraform init -backend=false Provider resolves at the pinned version
Validate terraform validate The configuration is type-correct against the pinned provider schema β€” every object() schema and enum validation holds

What the gate does not cover: terraform validate never calls Azure. Region availability, subnet delegation, SKU/zone support, and image reachability are only exercised by terraform plan / apply against a real subscription, run by a human from CI.


πŸ’¬ Example Output

$ terraform output
id                    = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-platform-eastus/providers/Microsoft.ContainerInstance/containerGroups/aci-platform-01"
name                  = "aci-platform-01"
ip_address            = "10.20.1.4"
fqdn                  = ""
identity_principal_id = "11111111-1111-1111-1111-111111111111"
identity_tenant_id    = "22222222-2222-2222-2222-222222222222"

πŸ” Troubleshooting

Symptom Cause Fix
containers must contain at least one container containers map is empty Supply at least one container entry
Plan shows the group being replaced on a small change The changed field is force-new (image, cpu, ports, os_type, sku, …) Expected for Container Instances; schedule the replacement
subnet_ids set but injection fails Subnet lacks the Microsoft.ContainerInstance/containerGroups delegation Delegate the subnet, or use ip_address_type = "Public" / "None"
dns_name_label rejected ip_address_type is not "Public" A DNS label requires a public IP
ImageRegistryCredentialError / pull denied Identity lacks AcrPull, or the wrong server Grant AcrPull to the identity; confirm the login server matches
Diagnostics ship nothing Wrong workspace_id or workspace_key Use the workspace's workspace_id GUID and its shared key
Provider fails to initialize Caller has no features {} block Add provider "azurerm" { features {} } to the root module

πŸ”— Related Docs


πŸ’™ "Infrastructure as Code should be standardized, consistent, and secure."