πŸš€ 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?

  1. Zero downtime – traffic shifts gradually without impacting users.

  2. Quick rollback – if issues occur in Green, simply shift traffic back to Blue.

  3. Seamless upgrades – deploy new versions with confidence.

  4. Better testing – Green can be validated in real-time before full cutover.


How It Works in Our Project

  1. 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, and green.thirucloud.xyz.

    • Route 53 DNS records pointing to the ALB.

  2. By default, all traffic (100%) goes to Blue.

  3. When deploying a new version, update the weights in terraform.tfvars:

     blue_weight  = 50
     green_weight = 50
    

    Apply Terraform β†’ traffic shifts evenly.

  4. Once Green is stable, shift 100% traffic to Green.

  5. If issues arise, rollback is as easy as setting Blue weight back to 100.


Deployment Flow

  1. Initial State: Blue serves all traffic.

  2. Deploy New Version: Green environment is launched.

  3. Weighted Routing: Gradually increase Green weight, monitor metrics (CloudWatch, logs, health checks).

  4. Full Cutover: Switch 100% traffic to Green.

  5. 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:

  1. 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).

  2. terraform init && terraform apply (in us-east-1 by default).

  3. Visit:

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.

0
Subscribe to my newsletter

Read articles from SRINIVAS TIRUNAHARI directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

SRINIVAS TIRUNAHARI
SRINIVAS TIRUNAHARI