In the previous article in this series, I covered path-based routing using httpRouteConfigs. That approach routes traffic to multiple backend services through a single entry point. I also mentioned that traffic_weight targets support revision labels, which opens the door to Container Apps blue-green Terraform deployments.
In this article, I walk through a full Azure Container Apps blue-green deployment using Terraform. Everything is declarative — no portal clicks, no scripts. You can find the complete code in my azure-way/terraform-container-apps repository.
What Is Blue-Green Deployment in Azure Container Apps?
Blue-green deployment in Azure Container Apps is a zero-downtime release strategy. Two versions of the same app run as concurrent revisions inside a single Container App. One serves live traffic (blue), while the other holds the new version under test (green).
Traffic shifts between them using percentage-based weights. When the new version passes validation, a single config change promotes it to production. If something goes wrong, rollback is instant. The old revision still runs at 0% traffic, so it takes over with one terraform apply.
This differs from blue-green on Azure App Service, which requires dedicated deployment slots. In contrast, Azure Container Apps shares the same resource and environment for both versions. As a result, it is cheaper and simpler to manage.
Why Container Apps Blue-Green Terraform Deployments Beat Single-Revision
By default, deploying a new image to a Container App replaces the running revision right away. That model works during development. However, in production it creates real risk:
| Challenge | Single Revision | Blue-Green Deployment |
|---|---|---|
| Zero-downtime deployment | ✗ Brief gap during revision switch | ✔ Old version stays live until swap |
| Instant rollback | ✗ Requires re-deploy of previous image | ✔ Flip traffic weight back in seconds |
| Pre-production validation | ✗ Impossible without a separate environment | ✔ New version testable via stable label URL |
| Traffic control | ✗ All-or-nothing | ✔ Granular percentage-based split |
| IaC-driven | ✔ Standard Terraform | ✔ Fully declarative with revision labels |
Because of these gaps, I needed a safer release process once I started running production workloads. Container Apps blue-green Terraform deployments with revision labels solve all of these problems. Moreover, they do it without extra infrastructure or a separate environment.
How Azure Container Apps Blue-Green Deployment Works
Azure Container Apps supports running multiple concurrent revisions of the same app. The blue-green mechanism relies on three platform primitives:
Multiple Revision Mode
First, you set revision_mode = “Multiple” in the azurerm_container_app resource. This allows more than one revision to be active at the same time. Without it, every new deployment shuts down the previous revision immediately. With Multiple mode, both stay alive until you change the traffic split.
Revision Labels
Second, you assign named labels (blue and green) to specific revisions. Each label gets its own stable URL:
https://<app-name>---blue.<env-domain>.azurecontainerapps.io
https://<app-name>---green.<env-domain>.azurecontainerapps.io
As a result, you can test the new version on the green URL before it gets any live traffic. The production FQDN stays untouched.
Traffic Weights
Third, the traffic_weight blocks inside the ingress control what percentage of requests goes to each labeled revision. In a standard flow, you start at blue = 100 / green = 0. Then you deploy the new image to green, validate it, and swap to blue = 0 / green = 100. Rolling back just means flipping the numbers.
Because of this design, the entire lifecycle is fully declarative. Every stage is driven by changing Terraform variables and running terraform apply.
Container Apps Blue-Green Terraform: The First-Deployment Problem
There is a critical nuance that is easy to miss. Terraform can only deploy one revision per apply. On the first run, only one revision exists. However, if the config references both blue and green in traffic_weight blocks and httpRouteConfigs rules, Azure rejects it. The second revision simply does not exist yet.
To solve this, I use an enable_blue_green boolean variable. It controls whether the second revision’s traffic rules are included. As a result, the Container Apps blue-green Terraform deployment becomes a two-step process:
- First apply (enable_blue_green = false) — deploys one revision only, with a single traffic_weight block. There are no references to the missing second revision.
- Second apply (enable_blue_green = true) — both revisions now exist, so Terraform safely creates both traffic_weight blocks and the httpRouteConfigs module.
Once both revisions are live, enable_blue_green stays true permanently. In other words, this two-step bootstrap is a one-time operation.
Container Apps Blue-Green Terraform Configuration
The project follows the same module structure as the rest of this series: azurerm provider, VNet + ACR modules, Consumption environment, and a managed identity with AcrPull access. If those pieces are new to you, Part 1 covers them in detail. Here I focus only on what makes the Container Apps blue-green Terraform setup unique.
Container Apps Blue-Green Terraform Input Variables
These variables form the control plane for the blue-green lifecycle. Changing them drives every deployment stage:
variable "production_label" {
description = "Which revision label receives 100% of production traffic: 'blue' or 'green'."
type = string
default = "blue"
validation {
condition = contains(["blue", "green"], var.production_label)
error_message = "production_label must be either 'blue' or 'green'."
}
}
variable "enable_blue_green" {
description = <<-EOT
Enable blue-green traffic splitting. Set to false for first deployment
(single revision), then set to true after both revisions exist.
EOT
type = bool
default = false
}
variable "blue_revision_suffix" {
description = "Deterministic revision suffix for the blue environment (e.g. commit hash or build ID)."
type = string
default = "blue-v1"
}
variable "green_revision_suffix" {
description = "Deterministic revision suffix for the green environment (e.g. commit hash or build ID)."
type = string
default = "green-v1"
}
variable "blue_image" {
description = "Container image tag deployed to the blue revision."
type = string
}
variable "green_image" {
description = "Container image tag deployed to the green revision."
type = string
default = ""
}
Key design decision: I use revision_suffix as a stable, deterministic identifier instead of relying on Azure’s auto-generated names. Terraform needs to reference revisions consistently across applies. A suffix like blue-v1 or a short commit hash (e.g. blue-a3f9c1) provides that anchor. If Azure auto-generates the name, Terraform loses track of which revision is which, and the label mapping breaks.
Why green_image has a default: During the initial deployment (enable_blue_green = false), only the blue revision is created. Since the green image is not needed yet, making it optional avoids forcing a placeholder value on the first apply.
Container Apps Blue-Green Terraform: The azurerm_container_app Resource
This is where the Container Apps blue-green Terraform logic lives. Several properties are deliberately different from a standard single-revision Container App:
resource "azurerm_container_app" "app" {
name = "${local.prefix}-app"
container_app_environment_id = azurerm_container_app_environment.app_env.id
resource_group_name = azurerm_resource_group.rg.name
revision_mode = "Multiple" # Required for concurrent revisions
workload_profile_name = "Consumption"
identity {
type = "SystemAssigned, UserAssigned"
identity_ids = [azurerm_user_assigned_identity.ca_identity.id]
}
registry {
identity = azurerm_user_assigned_identity.ca_identity.id
server = module.container_registry.url
}
# The template always targets the currently active production slot.
template {
revision_suffix = var.production_label == "blue" ? var.blue_revision_suffix : var.green_revision_suffix
container {
name = "sample-app"
image = "${module.container_registry.url}/${var.production_label == "blue" ? var.blue_image : var.green_image}"
cpu = 0.25
memory = "0.5Gi"
env {
name = "REVISION_LABEL"
value = var.production_label
}
}
min_replicas = 1
max_replicas = 5
}
ingress {
allow_insecure_connections = false
external_enabled = true
target_port = 8080
# Blue — present when production_label is "blue" OR blue-green is enabled
dynamic "traffic_weight" {
for_each = (var.enable_blue_green || var.production_label == "blue") ? [1] : []
content {
revision_suffix = var.blue_revision_suffix
label = "blue"
percentage = var.production_label == "blue" ? 100 : 0
}
}
# Green — present when production_label is "green" OR blue-green is enabled
dynamic "traffic_weight" {
for_each = (var.enable_blue_green || var.production_label == "green") ? [1] : []
content {
revision_suffix = var.green_revision_suffix
label = "green"
percentage = var.production_label == "green" ? 100 : 0
}
}
}
depends_on = [null_resource.acr_import]
}
How the Key Properties Work Together
revision_mode = “Multiple” is the non-negotiable requirement for blue-green on Azure Container Apps. Without it, every terraform apply shuts down the previous revision. That defeats the whole purpose.
Conditional template block — the template describes the revision Terraform creates right now. It always points at the active production slot. So when you set production_label = “green”, only the green revision updates. Blue stays untouched.
Dynamic traffic_weight blocks — this is how I solve the first-deployment problem. Instead of two static blocks that always reference both revisions, each block uses for_each to decide if it should exist. On the initial deploy (enable_blue_green = false, production_label = “blue”), only the blue block appears. The green block is skipped because neither condition is true. Once enable_blue_green = true, both blocks are always present.
percentage = var.production_label == “blue” ? 100 : 0 — this ternary drives the traffic swap. Changing production_label flips both weight values in a single apply.
Container Apps Blue-Green Terraform Outputs for CI/CD
I expose the label URLs so that CI/CD pipelines can run health checks against the non-production revision before promoting it:
output "app_url" {
value = "https://${azurerm_container_app.app.ingress[0].fqdn}"
}
output "blue_label_url" {
description = "Direct URL to the blue revision (via label)"
value = (var.production_label == "blue" || var.enable_blue_green) ? "https://${azurerm_container_app.app.name}---blue.${azurerm_container_app_environment.app_env.default_domain}" : null
}
output "green_label_url" {
description = "Direct URL to the green revision (via label)"
value = (var.production_label == "green" || var.enable_blue_green) ? "https://${azurerm_container_app.app.name}---green.${azurerm_container_app_environment.app_env.default_domain}" : null
}
These outputs are conditional. For example, blue_label_url returns null when the blue revision does not exist yet. Similarly, green_label_url returns null during the initial deploy. This prevents pipelines from running health checks against URLs that do not resolve.
Note the URL pattern: Azure builds the label FQDN using three dashes: <app-name>—<label>.<env-domain>. This differs from the main app FQDN format and is easy to miss in the official documentation. Because of this, I surface both URLs as Terraform outputs to remove guesswork from CI/CD pipelines.
Container Apps Blue-Green Terraform Lifecycle in Practice
The entire release flow is driven by editing terraform.tfvars and running terraform apply. There are no Azure CLI commands, no portal clicks, and no separate scripts. The Container Apps blue-green Terraform lifecycle has two phases: a one-time bootstrap, followed by the ongoing blue-green cycle.
Step 1 — Initial Deploy (Bootstrap)
On the very first deployment, only one revision can exist. Therefore, set enable_blue_green = false so Terraform does not reference the missing second revision:
production_label = "blue"
enable_blue_green = false
blue_revision_suffix = "blue-v1"
blue_image = "containerapps-helloworld:1.0.0"
After terraform apply, the blue revision receives 100% of traffic. No green revision exists yet, and no httpRouteConfigs are created.
Step 2 — Enable Blue-Green (Deploy Second Revision)
Now that the blue revision is running, you can enable blue-green mode. Update the tfvars to deploy the green revision:
production_label = "green"
enable_blue_green = true
blue_revision_suffix = "blue-v1"
green_revision_suffix = "green-v1"
blue_image = "containerapps-helloworld:1.0.0"
green_image = "containerapps-helloworld:2.0.0"
After terraform apply, both traffic_weight blocks are present. Green now receives 100% of traffic, while blue receives 0%. In addition, the httpRouteConfigs routing module is created. From this point on, enable_blue_green stays true.
Step 3 — Promote Blue Back to Production
production_label = "blue" # single change — all production traffic shifts to blue
One terraform apply moves 100% of traffic to blue. Meanwhile, green remains running at 0%, warm and ready.
Step 4 — Rollback (if needed)
production_label = "green" # one line — production instantly reverts to green
No re-build, no re-deploy, no image push. The old revision is still running and takes over within seconds.
Summary Table
| Step | enable_blue_green | production_label | Blue Traffic | Green Traffic | Blue Image | Green Image |
|---|---|---|---|---|---|---|
| Initial deploy (bootstrap) | false | blue | 100% | — | v1.0 | — |
| Enable blue-green | true | green | 0% | 100% | v1.0 | v2.0 |
| Swap to blue | true | blue | 100% | 0% | v1.0 | v2.0 |
| Rollback to green | true | green | 0% | 100% | v1.0 | v2.0 |
| Next release cycle | true | blue | 100% | 0% | v3.0 | v2.0 |
How to Deploy Container Apps Blue-Green Terraform
Prerequisites
- Azure CLI installed and logged in
- Terraform >= 1.1.0
- An Azure subscription with a Service Principal
Deployment Steps
-
Clone the repository:
git clone https://github.com/azure-way/terraform-container-apps.git cd terraform-container-apps/container_apps_blue_green -
Create a terraform.tfvars file for the initial deployment:
subscription-id = "<your-subscription-id>" spn-client-id = "<your-spn-client-id>" spn-client-secret = "<your-spn-client-secret>" spn-tenant-id = "<your-tenant-id>" production_label = "blue" enable_blue_green = false blue_revision_suffix = "blue-v1" blue_image = "containerapps-helloworld:latest" -
Initialize and apply:
terraform init terraform plan terraform apply -
Then enable blue-green by updating terraform.tfvars:
production_label = "green" enable_blue_green = true blue_revision_suffix = "blue-v1" green_revision_suffix = "green-v1" blue_image = "containerapps-helloworld:latest" green_image = "containerapps-helloworld:latest"terraform apply -
Next, verify the deployment. Check the outputs for both the main FQDN and the label URLs. Hit blue_label_url and green_label_url to confirm both revisions respond.
-
Finally, simulate a promotion. Update production_label to “blue” and run terraform apply again. You can observe the traffic split in the Azure portal under Revisions and replicas.
Container Apps Blue-Green Terraform: Key Takeaways
Here are the most important lessons from running Container Apps blue-green Terraform deployments in production:
- revision_mode = “Multiple” is the single most important setting. Without it, every Terraform apply is a destructive, in-place replacement. Therefore, always enable Multiple mode for blue-green.
- Revision labels create stable, testable URLs for each environment. Always validate the new version on the label URL before promoting it.
- A single variable (production_label) drives the entire lifecycle. Deploy, promote, and rollback all reduce to changing one value and running terraform apply.
- The first deployment needs a two-step bootstrap. Because Terraform can only create one revision per apply, referencing a missing revision in traffic_weight blocks fails. The enable_blue_green variable solves this by including the second revision’s rules only after both revisions exist. Once both are live, enable_blue_green stays true.
- Dynamic traffic_weight blocks make the bootstrap possible. Using for_each, Terraform emits only the blocks that reference existing revisions. As a result, the first apply avoids Azure API errors.
- Deterministic revision_suffix values are mandatory. Auto-generated revision names break Terraform’s ability to track label mappings across applies. Instead, treat the suffix as a build ID.
- Rollback is instant and costs nothing extra. The standby revision runs at 0% traffic on the Consumption profile, so it scales to zero. Yet it is available right away without any re-deployment.
- The label URL format (<app>—<label>.<domain>) is not well documented. Because of this, outputting both URLs from Terraform is the best way to avoid guesswork in CI/CD pipelines.
Frequently Asked Questions About Container Apps Blue-Green Terraform
Does Container Apps blue-green Terraform deployment require a slot or separate environment?
No. Unlike Azure App Service deployment slots, blue-green on Container Apps runs both versions as revisions within the same Container App and environment. There is no extra cost for the standby revision. On the Consumption profile, it scales to zero when it gets no traffic.
What is revision_mode = “Multiple” in Terraform for Azure Container Apps?
This property lets more than one revision stay active at the same time. It is the requirement for any traffic-splitting strategy, including blue-green and canary deployments. When set to “Single” (the default), each new image shuts down the previous revision. As a result, staged rollouts become impossible.
How do I rollback a Container Apps blue-green Terraform deployment?
Rollback is a one-line change in terraform.tfvars. Simply set production_label back to the previous value and run terraform apply. Because the old revision keeps running at 0% traffic, the rollback takes effect within seconds.
Why does the Container Apps blue-green Terraform bootstrap need two apply runs?
Terraform can only create one revision per apply. On the first run, only one revision exists. If the config always references both blue and green revisions, Azure rejects the request. To fix this, the enable_blue_green variable controls whether the second revision’s rules are included. After the first apply, the second run (with enable_blue_green = true) creates the other revision and enables full traffic splitting.
What is the URL format for Azure Container Apps revision labels?
Azure uses the pattern https://<app-name>—<label>.<environment-default-domain>, with three dashes between the app name and label. For example: https://myapp—green.happysand-12345678.westeurope.azurecontainerapps.io. This URL is stable across deployments. You can use it for health checks and smoke tests before promoting a revision.
Can I use Container Apps blue-green Terraform with the Consumption workload profile?
Yes. Container Apps blue-green Terraform works on both Consumption and Dedicated profiles. The Consumption profile is ideal because the standby revision scales to zero automatically. As a result, there is no extra cost while the previous version sits on standby.
What is the difference between blue-green and canary deployment on Azure Container Apps?
Blue-green switches traffic in one atomic step. All 100% moves from blue to green at once. In contrast, canary gradually shifts traffic in increments (for example, 10%, then 30%, then 100%) while monitoring error rates. Both strategies use the same primitives: revision_mode = “Multiple”, labels, and traffic_weight percentages. The only difference is whether you flip the percentage in one step or several.
What’s Next for Container Apps Blue-Green Terraform
This pattern is the foundation for more advanced release strategies. The next step is canary deployments. Instead of flipping 100% at once, you gradually shift traffic: blue = 90 / green = 10, then 70 / 30, and so on. At each stage, you monitor error rates and latency before continuing the rollout.
In addition, you can combine this with path-based routing to scope the blue-green split to a specific route. For example, you could run the traffic split only on /api/v2 while /api/v1 stays on the stable revision.
The full source code is available in my azure-way/terraform-container-apps repository. Clone it, adapt the tfvars to your images and suffixes, and let me know how it works for you.
