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 targetinghashicorp/azurerm ~> 4.0.
- π¦ 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
containerblocks from a keyed map, plus optionalinit_containerblocks 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
diagnosticsblock. - π§© Exposes per-container
ports,liveness_probe/readiness_probe,security, andvolume(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.
If this module saves you time, please consider supporting its continued development:
- β Star the repository β it helps others find the work.
- π€ Connect on LinkedIn: linkedin.com/in/microsoftexpert
- β Buy me a coffee: buymeacoffee.com/microsoftexpert
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
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
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 |
| 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
containersmap must be non-empty; this module fails at plan time with a clear message if it is. ip_address_typegates several fields."Private"requiressubnet_idsand rejects adns_name_label;"Public"is what adns_name_labeland a public FQDN attach to;"None"(the default) exposes no inbound IP.dns_name_label_reuse_policyonly applies when adns_name_labelis 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/containerGroupsdelegation).
- Create/manage the container group:
Contributoron the target resource group, or a custom role grantingMicrosoft.ContainerInstance/containerGroups/*scoped to that resource group. - Private VNet injection (
ip_address_type = "Private"):Network Contributor(orMicrosoft.Network/virtualNetworks/subnets/join/action) on the target subnet. - Identity-based image pulls: the referenced user-assigned identity needs
AcrPullon the source registry; the deploying principal needsManaged Identity Operatoron that identity to assign it. - Customer-managed-key encryption: the identity in
key_vault_user_assigned_identity_idneedsget/wrapKey/unwrapKeyon the Key Vault key (Key Vault Crypto Service Encryption User under RBAC authorization).
- An existing resource group in a supported US Azure region.
- The
Microsoft.ContainerInstanceresource 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
AcrPullon 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.
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
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_typeleft at its"None"default, this group has no inbound IP address. Outbound connectivity still works β ideal for batch/job workloads.
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 |
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_typedefaults 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 becomesmy-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/containerGroupsdelegation.
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
AcrPullon 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. adata.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, neverenvironment_variablesβ the latter is visible in state and API responses.storage_account_keyandsecretvolumes 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 withrestart_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_keyis 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
containersis 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_onensures theAcrPullassignment lands before the first pull.
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
}| 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.
- 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_eachkey stability.containers,init_containers,image_registry_credentials, per-containerports, and per-containervolumesare keyed maps rendered withdynamicblocks. 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, anddiagnostics.log_analytics.workspace_keylive inside variables that feedfor_each/dynamicblocks. Terraform forbids a sensitive value as afor_eachsource, so this module cannot mark those enclosing variablessensitivewithout 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_typedefaults to"None". Reaching a container requires either private VNet injection ("Private"+subnet_ids) or an explicit public IP ("Public").dns_name_labelonly attaches to a public IP. - Privilege is off by default. A container's
security.privilege_enableddefaults tofalse; grant elevated host privileges only for a workload that genuinely needs them. features {}dependence. The provider will not initialize without a caller-sideprovider "azurerm" { features {} }block. That block is the caller's, never the module's.
| 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 |
# 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/applyfrom CI against real credentials. - The caller supplies
provider "azurerm" { features {} }, auth, and subscription.
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.
$ 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"| 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 |
azurerm_container_groupresource- Azure Container Instances documentation
- Sibling modules:
terraform-azurerm-resource-group,terraform-azurerm-container-registry,terraform-azurerm-user-assigned-identity,terraform-azurerm-virtual-network,terraform-azurerm-log-analytics-workspace,terraform-azurerm-role-assignments - This module's
SCOPE.md
π "Infrastructure as Code should be standardized, consistent, and secure."