π Zero-Downtime Blue/Green Deployments on AWS with Route 53 Weighted Routing and Terraform


Introduction
In modern DevOps practices, zero downtime deployment is a crucial requirement. Traditional deployments often involve replacing servers or updating load balancer targets, which may cause short outages or unexpected errors. To solve this, we use a Blue/Green Deployment Strategy.
Blue/Green deployment creates two separate but identical environments:
Blue β the currently running production environment.
Green β the new version of your application.
With AWS Route 53 and Application Load Balancer (ALB), we can shift traffic gradually and safely between environments using Weighted Routing Policies. Terraform helps us automate this process end-to-end.
Why Blue/Green Deployments?
Zero downtime β traffic shifts gradually without impacting users.
Quick rollback β if issues occur in Green, simply shift traffic back to Blue.
Seamless upgrades β deploy new versions with confidence.
Better testing β Green can be validated in real-time before full cutover.
How It Works in Our Project
We use Terraform to provision:
A VPC, Subnets, Security Groups.
Two EC2 instances (Blue & Green) running Nginx.
An Application Load Balancer (ALB) with:
Target Group for Blue.
Target Group for Green.
HTTPS Listener with Weighted Forwarding Rules.
An ACM SSL Certificate for
app.thirucloud.xyz
,blue.thirucloud.xyz
, andgreen.thirucloud.xyz
.Route 53 DNS records pointing to the ALB.
By default, all traffic (100%) goes to Blue.
When deploying a new version, update the weights in
terraform.tfvars
:blue_weight = 50 green_weight = 50
Apply Terraform β traffic shifts evenly.
Once Green is stable, shift 100% traffic to Green.
If issues arise, rollback is as easy as setting Blue weight back to 100.
Deployment Flow
Initial State: Blue serves all traffic.
Deploy New Version: Green environment is launched.
Weighted Routing: Gradually increase Green weight, monitor metrics (CloudWatch, logs, health checks).
Full Cutover: Switch 100% traffic to Green.
Rollback (if needed): Shift traffic back to Blue instantly.
Benefits of Route 53 Weighted Routing
Fine-grained control over traffic percentages.
Supports canary releases (e.g., 10% users on Green, 90% on Blue).
Native health checks β Route 53 wonβt send traffic to unhealthy endpoints.
Fully automated via Terraform IaC.
Terraform project (split into the six files you asked for) in the canvas:
main.tf β VPC, subnets, SGs, EC2 (blue & green), Route 53 zone lookup
alb.tf β ALB, target groups, attachments, HTTPβHTTPS redirect, HTTPS listener with weighted forward, host rules for blue/green, Route 53 alias records
acm.tf β ACM certificate (DNS-validated via Route 53) covering app/blue/green, plus validation resources
variables.tf β sensible defaults for thirucloud.xyz
terraform.tfvars β ready-to-run values
outputs.tf β URLs and weights
How to use:
Purcahse domain name on godaddy, Create publichosted zone under route53, copy NS records from route53 and add them to godaddy. Ensure your public hosted zone
thirucloud.xyz
exists in Route 53 (same AWS account).terraform init && terraform apply
(in us-east-1 by default).Visit:
https://app.thirucloud.xyz β weighted split (starts 100% Blue)
https://blue.thirucloud.xyz β 100% Blue TG (host rule)
https://green.thirucloud.xyz β 100% Green TG (host rule)
To shift traffic, just change blue_weight
/ green_weight
in terraform.tfvars
and apply.
Letβs get into action:
main.tf
# Terraform: HTTPS ALB fronting Blue & Green EC2 (Nginx) with host-based routing and weighted default
# Domain: thirucloud.xyz (app, blue, green subdomains)
# Files: main.tf, variables.tf, terraform.tfvars, alb.tf, acm.tf, outputs.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
}
provider "aws" {
region = var.region
}
########################
# Networking (VPC/Public)
########################
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = { Name = "bluegreen-vpc" }
}
data "aws_availability_zones" "available" {}
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.main.id
tags = { Name = "bluegreen-igw" }
}
resource "aws_subnet" "public_a" {
vpc_id = aws_vpc.main.id
cidr_block = var.public_subnet_a_cidr
availability_zone = data.aws_availability_zones.available.names[0]
map_public_ip_on_launch = true
tags = { Name = "public-a" }
}
resource "aws_subnet" "public_b" {
vpc_id = aws_vpc.main.id
cidr_block = var.public_subnet_b_cidr
availability_zone = data.aws_availability_zones.available.names[1]
map_public_ip_on_launch = true
tags = { Name = "public-b" }
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
tags = { Name = "public-rt" }
}
resource "aws_route" "default_inet" {
route_table_id = aws_route_table.public.id
destination_cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw.id
}
resource "aws_route_table_association" "a" {
route_table_id = aws_route_table.public.id
subnet_id = aws_subnet.public_a.id
}
resource "aws_route_table_association" "b" {
route_table_id = aws_route_table.public.id
subnet_id = aws_subnet.public_b.id
}
########################
# Security Groups
########################
# ALB SG: allow 80/443 from Internet
resource "aws_security_group" "alb" {
name = "alb-sg"
description = "Allow web"
vpc_id = aws_vpc.main.id
ingress {
description = "HTTP"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "HTTPS"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = { Name = "alb-sg" }
}
# EC2 SG: allow HTTP from ALB SG, SSH from your CIDR
resource "aws_security_group" "ec2" {
name = "ec2-sg"
description = "Allow web from ALB and SSH"
vpc_id = aws_vpc.main.id
ingress {
description = "HTTP from ALB"
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = [aws_security_group.alb.id]
}
ingress {
description = "SSH"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [var.allowed_ssh_cidr]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = { Name = "ec2-sg" }
}
########################
# EC2 instances (Blue & Green)
########################
# Latest Ubuntu 22.04 for region (optionally replace with your preferred AMI)
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"] # Canonical
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
resource "aws_instance" "blue" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
subnet_id = aws_subnet.public_a.id
vpc_security_group_ids = [aws_security_group.ec2.id]
key_name = var.key_name
associate_public_ip_address = true
user_data = <<-EOF
#!/bin/bash
apt update -y
apt install -y nginx
echo "<h1>Blue Environment</h1>" > /var/www/html/index.html
systemctl enable nginx && systemctl restart nginx
EOF
tags = { Name = "blue-nginx" }
}
resource "aws_instance" "green" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
subnet_id = aws_subnet.public_b.id
vpc_security_group_ids = [aws_security_group.ec2.id]
key_name = var.key_name
associate_public_ip_address = true
user_data = <<-EOF
#!/bin/bash
apt update -y
apt install -y nginx
echo "<h1>Green Environment</h1>" > /var/www/html/index.html
systemctl enable nginx && systemctl restart nginx
EOF
tags = { Name = "green-nginx" }
}
########################
# Route 53 Hosted Zone
########################
data "aws_route53_zone" "this" {
name = var.zone_name
private_zone = false
}
# Records are created in alb.tf after ALB is created (alias to ALB)
alb.tf
# Application Load Balancer with HTTPS
resource "aws_lb" "this" {
name = "bluegreen-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = [aws_subnet.public_a.id, aws_subnet.public_b.id]
idle_timeout = 60
enable_deletion_protection = false
tags = { Name = "bluegreen-alb" }
}
# Target groups (Blue & Green)
resource "aws_lb_target_group" "blue" {
name = "tg-blue"
port = 80
protocol = "HTTP"
vpc_id = aws_vpc.main.id
target_type = "instance"
health_check {
healthy_threshold = 2
unhealthy_threshold = 2
timeout = 5
interval = 15
path = "/"
matcher = "200-399"
}
}
resource "aws_lb_target_group" "green" {
name = "tg-green"
port = 80
protocol = "HTTP"
vpc_id = aws_vpc.main.id
target_type = "instance"
health_check {
healthy_threshold = 2
unhealthy_threshold = 2
timeout = 5
interval = 15
path = "/"
matcher = "200-399"
}
}
# Attach instances to respective TGs
resource "aws_lb_target_group_attachment" "blue_attach" {
target_group_arn = aws_lb_target_group.blue.arn
target_id = aws_instance.blue.id
port = 80
}
resource "aws_lb_target_group_attachment" "green_attach" {
target_group_arn = aws_lb_target_group.green.arn
target_id = aws_instance.green.id
port = 80
}
# HTTP listener β redirect to HTTPS
resource "aws_lb_listener" "http" {
load_balancer_arn = aws_lb.this.arn
port = 80
protocol = "HTTP"
default_action {
type = "redirect"
redirect {
port = "443"
protocol = "HTTPS"
status_code = "HTTP_301"
}
}
}
# HTTPS listener with default WEIGHTED forward (for gradual blue/green)
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.this.arn
port = 443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-2016-08"
certificate_arn = aws_acm_certificate_validation.cert.certificate_arn
default_action {
type = "forward"
forward {
target_group {
arn = aws_lb_target_group.blue.arn
weight = var.blue_weight
}
target_group {
arn = aws_lb_target_group.green.arn
weight = var.green_weight
}
stickiness {
enabled = true
duration = 60
}
}
}
}
# Host-based rules to force 100% to specific envs
resource "aws_lb_listener_rule" "host_blue" {
listener_arn = aws_lb_listener.https.arn
priority = 10
condition {
host_header {
values = ["${var.blue_subdomain}.${trim(var.zone_name, ".")}"]
}
}
action {
type = "forward"
target_group_arn = aws_lb_target_group.blue.arn
}
}
resource "aws_lb_listener_rule" "host_green" {
listener_arn = aws_lb_listener.https.arn
priority = 20
condition {
host_header {
values = ["${var.green_subdomain}.${trim(var.zone_name, ".")}"]
}
}
action {
type = "forward"
target_group_arn = aws_lb_target_group.green.arn
}
}
# Route 53 alias records β ALB
resource "aws_route53_record" "blue" {
zone_id = data.aws_route53_zone.this.zone_id
name = "${var.blue_subdomain}.${trim(var.zone_name, ".")}"
type = "A"
alias {
name = aws_lb.this.dns_name
zone_id = aws_lb.this.zone_id
evaluate_target_health = true
}
}
resource "aws_route53_record" "green" {
zone_id = data.aws_route53_zone.this.zone_id
name = "${var.green_subdomain}.${trim(var.zone_name, ".")}"
type = "A"
alias {
name = aws_lb.this.dns_name
zone_id = aws_lb.this.zone_id
evaluate_target_health = true
}
}
resource "aws_route53_record" "app" {
zone_id = data.aws_route53_zone.this.zone_id
name = "${var.app_subdomain}.${trim(var.zone_name, ".")}"
type = "A"
alias {
name = aws_lb.this.dns_name
zone_id = aws_lb.this.zone_id
evaluate_target_health = true
}
}
acm.tf
# Request a single cert for app, blue, green (SANs) using DNS validation
locals {
root_domain = trim(var.zone_name, ".")
san_domains = [
"${var.app_subdomain}.${local.root_domain}",
"${var.blue_subdomain}.${local.root_domain}",
"${var.green_subdomain}.${local.root_domain}"
]
}
resource "aws_acm_certificate" "cert" {
domain_name = local.root_domain
subject_alternative_names = local.san_domains
validation_method = "DNS"
}
# Create DNS validation records for each domain in cert
resource "aws_route53_record" "cert_validation" {
for_each = {
for dvo in aws_acm_certificate.cert.domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
}
allow_overwrite = true
name = each.value.name
records = [each.value.record]
ttl = 60
type = each.value.type
zone_id = data.aws_route53_zone.this.zone_id
}
resource "aws_acm_certificate_validation" "cert" {
certificate_arn = aws_acm_certificate.cert.arn
validation_record_fqdns = [for r in aws_route53_record.cert_validation : r.fqdn]
}
var.tf
variable "region" {
type = string
default = "us-east-1"
}
variable "zone_name" {
description = "Public hosted zone name with trailing dot (e.g., thirucloud.xyz.)"
type = string
default = "thirucloud.xyz."
}
variable "vpc_cidr" {
type = string
default = "10.0.0.0/16"
}
variable "public_subnet_a_cidr" {
type = string
default = "10.0.1.0/24"
}
variable "public_subnet_b_cidr" {
type = string
default = "10.0.2.0/24"
}
variable "instance_type" {
type = string
default = "t3.micro"
}
variable "key_name" {
type = string
default = null
}
variable "allowed_ssh_cidr" {
description = "CIDR allowed to SSH to EC2 (restrict to your IP)"
type = string
default = "0.0.0.0/0"
}
# Subdomain labels
variable "app_subdomain" {
type = string
default = "app"
}
variable "blue_subdomain" {
type = string
default = "blue"
}
variable "green_subdomain" {
type = string
default = "green"
}
# Weighted forwarding on ALB default action
variable "blue_weight" {
type = number
default = 100
}
variable "green_weight" {
type = number
default = 0
}
terraform.tfvars
# Region & Hosted Zone
region = "us-east-1"
zone_name = "thirucloud.xyz."
# Networking
vpc_cidr = "10.0.0.0/16"
public_subnet_a_cidr = "10.0.1.0/24"
public_subnet_b_cidr = "10.0.2.0/24"
# EC2 + access
instance_type = "t3.micro"
key_name = null # or your existing key pair name
allowed_ssh_cidr = "0.0.0.0/0" # CHANGE to your IP/CIDR for security
# Subdomains
app_subdomain = "app"
blue_subdomain = "blue"
green_subdomain = "green"
# ALB default weighted split (for app.thirucloud.xyz)
blue_weight = 100
green_weight = 0
Conclusion
With this setup, you achieve a robust, zero-downtime deployment pipeline using AWS Route 53, ALB, ACM, and Terraform. This architecture can be extended with CI/CD pipelines (GitHub Actions, Jenkins, or CodePipeline) for automated deployments.
By combining Infrastructure as Code with Blue/Green strategy, your deployments become faster, safer, and rollback-ready.
π₯ Next Step: Enhance this by adding auto-scaling groups for Blue/Green environments, or integrate with GitHub Actions for fully automated blue/green rollouts.
Subscribe to my newsletter
Read articles from SRINIVAS TIRUNAHARI directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
