Terraform
Define a sandbox group as code and create an Ubuntu sandbox with one Terraform configuration.
- An Azure subscription with permission to create resource groups and role assignments.
- Terraform 1.11 or later.
- Git, required to download the pinned Sandbox Group module.
- Azure CLI (
az) installed and signed in with a user account. - Bash with
curlandunzipon Linux or macOS, or PowerShell on Windows. - Permission to create resource groups and role assignments.
Azure Container Apps Terraform provider is in public preview. It is distributed as a GitHub prerelease, not through the Terraform Registry.
What you can define
The sandbox_groups module provides the same group-level controls as Bicep:
- Set default CPU, memory, and disk, a maximum sandbox count, and a default timeout.
- Attach a system- or user-assigned managed identity.
- Connect the group to a delegated VNet subnet.
- Grant data-plane access with the Container Apps SandboxGroup Data Owner role.
The native Azure/aca provider also creates individual sandboxes and private
disk images through the regional Sandbox data plane. The end-to-end example
creates an Ubuntu sandbox with 1 vCPU, 2 GiB of memory, a 20 GiB disk, denied
outbound traffic, and memory-preserving auto-suspend after 10 minutes.
Core objects
| Object | Purpose | Terraform implementation |
|---|---|---|
| Resource group | Container for the quickstart resources. | azurerm_resource_group |
Microsoft.App/sandboxGroups | Holds sandboxes, snapshots, disk images, connections, and egress policies. | Pinned sandbox_groups module using AzAPI |
| Data-plane role assignment | Grants permission to create and manage sandboxes. | data_plane_operators in the sandbox_groups module |
| Public Ubuntu image | Resolves the service-provided Ubuntu disk image. | aca_sandbox_public_disk_image |
| Individual sandbox | Defines resources, lifecycle, egress, labels, and startup command. | aca_sandbox |
Sandbox group examples
These patterns match the group-level examples on the Bicep quickstart. Apply
them to the module "sandbox_group" block in main.tf.
Example 1 - Minimal sandbox group
Omit the rich preview settings to use the service defaults:
module "sandbox_group" {
source = "git::https://github.com/Azure/terraform-provider-aca.git//modules/sandbox_groups?ref=debaefd1c08d8156a3291c48432dd4ca63e0fbed"
name = var.sandbox_group_name
resource_group_id = azurerm_resource_group.this.id
location = azurerm_resource_group.this.location
}
Example 2 - Resource defaults and tags
Set CPU, memory, disk, maximum Sandbox count, timeout, and tags. These values apply when a data-plane client does not override them.
module "sandbox_group" {
# Keep the source, name, resource_group_id, and location arguments.
api_profile = "rich_preview"
default_cpu = "1"
default_memory = "2Gi"
default_disk = "20Gi"
max_sandbox_count = 50
default_timeout_seconds = 3600
tags = {
environment = "production"
team = "platform"
}
}
Example 3 - System-assigned managed identity
Add this argument to the module:
identity = {
type = "SystemAssigned"
}
Example 4 - User-assigned managed identity
Create an identity and attach it to the group:
resource "azurerm_user_assigned_identity" "sandbox" {
name = "aca-sandbox-identity"
resource_group_name = azurerm_resource_group.this.name
location = azurerm_resource_group.this.location
}
module "sandbox_group" {
# Keep the source, name, resource_group_id, location, and rich preview settings.
identity = {
type = "UserAssigned"
identity_ids = [azurerm_user_assigned_identity.sandbox.id]
}
}
Example 5 - Sandbox group with a VNet connection
Create a delegated subnet, then pass it to vnet_connections. One subnet maps
to one sandbox group, and the subnet ID cannot be changed after deployment.
resource "azurerm_virtual_network" "sandbox" {
name = "aca-sandbox-vnet"
resource_group_name = azurerm_resource_group.this.name
location = azurerm_resource_group.this.location
address_space = ["10.0.0.0/16"]
}
resource "azurerm_subnet" "sandbox" {
name = "sandbox-subnet"
resource_group_name = azurerm_resource_group.this.name
virtual_network_name = azurerm_virtual_network.sandbox.name
address_prefixes = ["10.0.0.0/23"]
delegation {
name = "sandbox-delegation"
service_delegation {
name = "Microsoft.App/environments"
actions = ["Microsoft.Network/virtualNetworks/subnets/action"]
}
}
lifecycle {
ignore_changes = [delegation]
}
}
module "sandbox_group" {
# Keep the source, name, resource_group_id, location, and rich preview settings.
vnet_connections = {
default = {
subnet_id = azurerm_subnet.sandbox.id
}
}
}
The subnet ID is immutable once set. Size the subnet for peak sandbox concurrency before you deploy.
End-to-end example - Sandbox group and sandbox
Create an empty directory and save the following configuration as main.tf.
The Sandbox Group module is pinned to the same commit used to publish the
provider release.
terraform {
required_version = ">= 1.11.0"
required_providers {
aca = {
source = "Azure/aca"
version = "= 0.5.0-preview"
}
azapi = {
source = "Azure/azapi"
version = ">= 2.12.0, < 3.0.0"
}
azurerm = {
source = "hashicorp/azurerm"
version = ">= 4.81.0, < 5.0.0"
}
}
}
provider "azurerm" {
subscription_id = var.subscription_id
features {}
}
provider "azapi" {
subscription_id = var.subscription_id
}
provider "aca" {
use_azure_cli = true
}
variable "subscription_id" {
description = "Azure subscription used by this quickstart."
type = string
}
variable "location" {
description = "Azure region that supports ACA Sandboxes."
type = string
default = "westus2"
}
variable "resource_group_name" {
description = "Resource group created by this quickstart."
type = string
default = "aca-sandbox-terraform"
}
variable "sandbox_group_name" {
description = "Sandbox group created by this quickstart."
type = string
default = "aca-sandbox-terraform"
}
variable "sandbox_name" {
description = "Logical Terraform name for the sandbox."
type = string
default = "ubuntu"
}
data "azurerm_client_config" "current" {}
resource "azurerm_resource_group" "this" {
name = var.resource_group_name
location = var.location
tags = {
environment = "dev"
managed_by = "terraform"
}
}
module "sandbox_group" {
source = "git::https://github.com/Azure/terraform-provider-aca.git//modules/sandbox_groups?ref=debaefd1c08d8156a3291c48432dd4ca63e0fbed"
name = var.sandbox_group_name
resource_group_id = azurerm_resource_group.this.id
location = azurerm_resource_group.this.location
api_profile = "rich_preview"
default_cpu = "1"
default_memory = "2Gi"
default_disk = "20Gi"
max_sandbox_count = 5
default_timeout_seconds = 3600
data_plane_operators = {
terraform_caller = {
principal_id = data.azurerm_client_config.current.object_id
principal_type = "User"
}
}
tags = {
environment = "dev"
managed_by = "terraform"
}
}
data "aca_sandbox_public_disk_image" "ubuntu" {
sandbox_group_id = module.sandbox_group.id
location = module.sandbox_group.location
name = "ubuntu"
depends_on = [module.sandbox_group]
}
resource "aca_sandbox" "ubuntu" {
sandbox_group_id = module.sandbox_group.id
location = module.sandbox_group.location
name = var.sandbox_name
source = {
public_disk_image = data.aca_sandbox_public_disk_image.ubuntu.name
}
resources = {
cpu = "1000m"
memory = "2048Mi"
disk = "20Gi"
}
entrypoint = ["/bin/sh", "-c"]
command = ["while true; do sleep 3600; done"]
auto_suspend = {
enabled = true
interval_seconds = 600
mode = "Memory"
}
egress_policy = {
default_action = "Deny"
}
labels = {
quickstart = "sandbox"
}
deletion_policy = "Delete"
timeouts = {
create = "20m"
delete = "10m"
}
}
output "sandbox_group_id" {
value = module.sandbox_group.id
}
output "sandbox_id" {
value = aca_sandbox.ubuntu.id
}
output "sandbox_state" {
value = aca_sandbox.ubuntu.state
}
output "sandbox_resource_url" {
value = aca_sandbox.ubuntu.resource_url
}
Install the preview provider
The canonical installers are maintained in the ACA Terraform provider
repository and pinned here to a specific commit. The installer detects the
platform used by your Terraform executable, downloads the matching release
archive and checksum manifest, verifies the SHA-256 hash, and writes
terraform.rc. The provider is stored under .terraform-provider-mirror in
the current directory.
- Bash
- PowerShell
INSTALLER_COMMIT="${ACA_TF_INSTALLER_COMMIT:-c4e104628fce8c975935a790c364a9ea9692c85c}"
INSTALLER_URL="https://raw.githubusercontent.com/Azure/terraform-provider-aca/${INSTALLER_COMMIT}/scripts/install-provider.sh"
TEMP_INSTALLER="$(mktemp)"
trap 'rm -f "$TEMP_INSTALLER"' EXIT
curl -fsSL "$INSTALLER_URL" -o "$TEMP_INSTALLER"
bash "$TEMP_INSTALLER"
$InstallerCommit = if ($env:ACA_TF_INSTALLER_COMMIT) {
$env:ACA_TF_INSTALLER_COMMIT
} else {
'c4e104628fce8c975935a790c364a9ea9692c85c'
}
$InstallerUrl = "https://raw.githubusercontent.com/Azure/terraform-provider-aca/$InstallerCommit/scripts/install-provider.ps1"
$TempInstaller = Join-Path ([IO.Path]::GetTempPath()) "$([guid]::NewGuid()).ps1"
try {
Invoke-WebRequest -Uri $InstallerUrl -OutFile $TempInstaller
& $TempInstaller
} finally {
Remove-Item -Path $TempInstaller -Force -ErrorAction SilentlyContinue
}
Deploy the sandbox
The configuration grants your signed-in user the Container Apps SandboxGroup Data Owner role. Role assignment propagation can take up to one minute; the provider retries initial data-plane reads during that interval.
- Bash
- PowerShell
if ! az account show --only-show-errors >/dev/null 2>&1; then
az login
fi
ACCOUNT_TYPE="$(az account show --query user.type -o tsv)"
if [[ "$(printf '%s' "$ACCOUNT_TYPE" | tr '[:upper:]' '[:lower:]')" != "user" ]]; then
echo "This quickstart requires Azure CLI authentication with a user account." >&2
exit 1
fi
SUBSCRIPTION_ID="${ACA_TF_SUBSCRIPTION_ID:-$(az account show --query id -o tsv)}"
az account set --subscription "$SUBSCRIPTION_ID"
export ARM_SUBSCRIPTION_ID="$SUBSCRIPTION_ID"
export TF_CLI_CONFIG_FILE="$PWD/terraform.rc"
terraform init
terraform apply -var="subscription_id=$ARM_SUBSCRIPTION_ID"
terraform output
az account show --only-show-errors *> $null
if ($LASTEXITCODE -ne 0) {
az login
}
$AccountType = az account show --query user.type -o tsv
if ($LASTEXITCODE -ne 0 -or $AccountType.Trim().ToLowerInvariant() -ne 'user') {
throw 'This quickstart requires Azure CLI authentication with a user account.'
}
$SubscriptionId = if ($env:ACA_TF_SUBSCRIPTION_ID) {
$env:ACA_TF_SUBSCRIPTION_ID
} else {
az account show --query id -o tsv
}
az account set --subscription $SubscriptionId
if ($LASTEXITCODE -ne 0) {
throw "Unable to select Azure subscription $SubscriptionId."
}
$env:ARM_SUBSCRIPTION_ID = $SubscriptionId
$env:TF_CLI_CONFIG_FILE = Join-Path $PWD 'terraform.rc'
terraform init
if ($LASTEXITCODE -ne 0) {
throw 'terraform init failed.'
}
terraform apply -var="subscription_id=$env:ARM_SUBSCRIPTION_ID"
if ($LASTEXITCODE -ne 0) {
throw 'terraform apply failed.'
}
terraform output
if ($LASTEXITCODE -ne 0) {
throw 'terraform output failed.'
}
After the apply finishes, sandbox_state should be Running. After 10 idle
minutes, the service applies the configured memory-preserving auto-suspend
policy.
Grant data-plane access
Creating the group does not grant access to the regional Sandbox APIs. The complete configuration assigns the signed-in user the Container Apps SandboxGroup Data Owner role through the module:
data_plane_operators = {
terraform_caller = {
principal_id = data.azurerm_client_config.current.object_id
principal_type = "User"
}
}
This quickstart rejects non-user Azure CLI sessions so the role assignment is
not created with the wrong principal type. For automation, replace
terraform_caller with the service principal object ID and set
principal_type = "ServicePrincipal".
Clean up
The quickstart sets deletion_policy = "Delete", so Terraform deletes the
data-plane sandbox before deleting the sandbox group and resource group.
- Bash
- PowerShell
SUBSCRIPTION_ID="${ACA_TF_SUBSCRIPTION_ID:-$(az account show --query id -o tsv)}"
az account set --subscription "$SUBSCRIPTION_ID"
export ARM_SUBSCRIPTION_ID="$SUBSCRIPTION_ID"
export TF_CLI_CONFIG_FILE="$PWD/terraform.rc"
terraform destroy -var="subscription_id=$ARM_SUBSCRIPTION_ID"
$SubscriptionId = if ($env:ACA_TF_SUBSCRIPTION_ID) {
$env:ACA_TF_SUBSCRIPTION_ID
} else {
az account show --query id -o tsv
}
az account set --subscription $SubscriptionId
if ($LASTEXITCODE -ne 0) {
throw "Unable to select Azure subscription $SubscriptionId."
}
$env:ARM_SUBSCRIPTION_ID = $SubscriptionId
$env:TF_CLI_CONFIG_FILE = Join-Path $PWD 'terraform.rc'
terraform destroy -var="subscription_id=$env:ARM_SUBSCRIPTION_ID"
if ($LASTEXITCODE -ne 0) {
throw 'terraform destroy failed.'
}
Learn more
- Sandbox groups - concepts behind Sandbox Groups.
- Identity - how sandboxes use managed identities.
- VNet - using VNet connections with Sandboxes.