Skip to content

Container Instances

See below for an example of an Azure Container Instance deployment using the container_group variable. The subnet used for the container group must be delegated to Microsoft.ContainerInstance/containerGroups.

Basic Example

The example includes some additional optional configuration including a user assigned managed identity that can be used to grant the container access to other Azure resources and some Azure File Shares that are mounted as volumes within the container for persistent storage.

See the azurerm_container_group for more information.

storage_accounts = {
    "diagsaphdev" = {
        resource_group = "hsw"
        public_network_access_enabled = true
        shares = {
            "ansible-files" = {
                quota = "10"
            }
        }
    }   
}

networks = {
    "hsw" = {
        resource_group = "hsw"
        address_space = ["10.40.44.0/22"]
        subnets = {
            hsw = {
                network_security_group = "hsw"
                address_prefixes = ["10.40.44.0/25"]
                nat_gateway = "hsw"
            }
            container = {
                network_security_group = "hsw"
                address_prefixes = ["10.40.44.128/25"]
                nat_gateway = "hsw"
                delegation = {
                    name = "Microsoft.ContainerInstance/containerGroups"
                    service_delegation = {
                        name = "Microsoft.ContainerInstance/containerGroups"
                        actions = ["Microsoft.Network/virtualNetworks/subnets/action"]
                    }
                }
            }
            bastion = {
                name = "AzureBastionSubnet"
                network_security_group = "bastion"
                address_prefixes = ["10.40.45.0/24"]
            }
        }
    }
}

user_assigned_identity = {
    ansible_container = {
        resource_group = "hsw"
    }
}

container_group = {
    ansible = {
        resource_group = "hsw"
        ip_address_type = "Private"
        subnets = ["hsw.container"]
        container = {
            ansible = {
                image = "ghcr.io/sapphire-health/ansible-epic:azure"
                cpu = 2
                memory = 4
                ports = [
                    {
                        port = 22
                        protocol = "TCP"
                    }
                ]
                volume = {
                    ansible-files = {
                        mount_path = "/files"
                        storage_account = "diagsaphdev"
                        share = "ansible-files"
                    }
                }
                environment_variables = {
                    TUNNEL_NAME = "ansible-az-container"
                }
            }
        }
        identity = {
            type = "UserAssigned"
            user_assigned_identities = ["ansible_container"]
        }
    }
}

Secure environment variables from secrets

Containers that need a secret (passwords, API keys, client secrets) must not receive it through environment_variables in tfvars. Instead, list the env var names in secure_environment_variable_secrets:

container = {
    postfix = {
        image = "ghcr.io/sapphire-health/postfix:1.11.0"
        environment_variables = {
            SMTP_USERNAME = "smtp-forwarder"
        }
        secure_environment_variable_secrets = [
            "SMTP_PASSWORD"
        ]
    }
}

The list contains env var names only — no secret key or value. For each entry the container_group module injects local.secrets[lower(<env var name>)] as a secure_environment_variables entry on the container, so SMTP_PASSWORD is always backed by the root variable smtp_password. To make a new secret available:

  1. Declare a sensitive = true variable named lower(<env var name>) in src/variables.secrets.tf (value supplied via TF_VAR_<name> at runtime).
  2. Add it to local.secrets in src/locals.tf under the same name.

If the lowercase name is missing from local.secrets, terraform plan fails with a key-not-found error rather than deploying an empty value.

secure_environment_variables (a plain map) still exists for non-secret values that should merely be hidden from the Azure portal; secret-backed entries are merged over it. See SMTP Forwarder for a complete worked example.

Advanced Example

The next example shows how to deploy a Caddy reverse proxy in front of a container to separate

storage_accounts = {
    "diagsaphdev" = {
        resource_group = "hsw"
        public_network_access_enabled = true
        shares = {
            "iot-hsm-softhsm2" = {
                quota = "10"
            }
            "iot-hsm-state" = {
                quota = "10"
            }
        }
    }   
}

container_group = {
    iothsm = {
        resource_group = "hsw"
        ip_address_type = "Private"
        subnets = ["hsw.container"]
        container = {
            caddy = {
                image = "ghcr.io/caddybuilds/caddy-cloudflare:latest"
                cpu = 0.5
                memory = 1
                ports = [
                    {
                        port = 80
                        protocol = "TCP"
                    },
                    {
                        port = 443
                        protocol = "TCP"
                    }
                ]
                environment_variables = {
                    CADDY_EMAIL = "[email protected]"
                    CADDY_ADDITIONAL_OPTIONS = "-agree=true"
                    CADDY_LOG_LEVEL = "ERROR"
                    CADDY_CERT = <<-EOT
                    -----BEGIN CERTIFICATE-----
                    MIICEjCCAbmgAwIBAgIUSW8QDVQ7oo4AT+5ois8pYd9OBQgwCgYIKoZIzj0EAwIw
                    RDEPMA0GA1UEAwwGaW90aHNtMQswCQYDVQQGEwJVUzESMBAGA1UECAwJTG91aXNp
                    YW5hMRAwDgYDVQQHDAdTbGlkZWxsMB4XDTI1MTIyNjE4NTQ0OVoXDTQ1MTIyMTE4
                    NTQ0OVowRDEPMA0GA1UEAwwGaW90aHNtMQswCQYDVQQGEwJVUzESMBAGA1UECAwJ
                    TG91aXNpYW5hMRAwDgYDVQQHDAdTbGlkZWxsMFkwEwYHKoZIzj0CAQYIKoZIzj0D
                    AQcDQgAEwOKCDmllUpTLI495CTKCDTVPxGYWJqEATRZ03LZ0LIgGG7gDydNk0ImH
                    SDwG5EMw4hJzu0BruNfsNx0iI4NFKqOBiDCBhTAdBgNVHQ4EFgQUNCEeULIG017r
                    CReJL/xca7hVdQcwHwYDVR0jBBgwFoAUNCEeULIG017rCReJL/xca7hVdQcwDgYD
                    VR0PAQH/BAQDAgWgMCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAR
                    BgNVHREECjAIggZpb3Roc20wCgYIKoZIzj0EAwIDRwAwRAIgNEV9aY7KKf+ZbuM3
                    I008Q6+GZ+ammyYS83y/GMp6z2ACIAX561haog+Sj0zdusnzC0M58D7XJgnYnO1/
                    A+ReZvz0
                    -----END CERTIFICATE-----
                    EOT
                    CADDY_KEY = <<-EOT
                    -----BEGIN PRIVATE KEY-----
                    MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgDO1DXcPFVxoH/5qx
                    9l2RAfGt4cg13Yxf+rfUyCj7sryhRANCAATA4oIOaWVSlMsjj3kJMoINNU/EZhYm
                    oQBNFnTctnQsiAYbuAPJ02TQiYdIPAbkQzDiEnO7QGu41+w3HSIjg0Uq
                    -----END PRIVATE KEY-----
                    EOT
                    CADDY_CONFIG = <<-EOT
                    :443 {
                        bind 0.0.0.0
                        tls /tmp/cert.pem /tmp/cert.key
                        basic_auth /* {
                            admin $2a$14$6Nv.IUjjsUcXXL62kuHvaezp7A2zWguxY4DwI7DVrMtHY6jAdMKIK
                        }
                        reverse_proxy http://localhost:3000
                    }
                    EOT
                }
                commands = [
                    "/bin/sh",
                    "-c",
                    "printf \"%s\" \"$CADDY_CERT\" > /tmp/cert.pem && printf \"%s\" \"$CADDY_KEY\" > /tmp/cert.key && printf \"%s\" \"$CADDY_CONFIG\" > Caddyfile && caddy run --config Caddyfile --adapter caddyfile"
                ]
            }
            iothsm = {
                image = "ghcr.io/lspiehler/iot-hsm:latest"
                cpu = 1
                memory = 2
                environment_variables = {
                    LISTENIP = "0.0.0.0"
                }
                volume = {
                    iot-hsm-softhsm2 = {
                        mount_path = "/var/lib/softhsm/tokens"
                        storage_account = "diagsaphdev"
                        share = "iot-hsm-softhsm2"
                    }
                    iot-hsm-state = {
                        mount_path = "/var/node/iot-hsm/state"
                        storage_account = "diagsaphdev"
                        share = "iot-hsm-state"
                    }
                }
            }
        }
        exposed_port = [
            {
                port = 80
                protocol = "TCP"
            },
            {
                port = 443
                protocol = "TCP"
            }
        ]
        identity = {
            type = "UserAssigned"
            user_assigned_identities = ["ansible_container"]
        }
    }
}