How to Set Up a Maintenance Page in AWS Using CloudFront, ALB, and Terraform

H

Hi, DevOps fans

Here’s a short but practical article on how to set up a maintenance page in AWS using CloudFront, ALB, and Terraform. I’ll assume you already know your way around Terraform, and understand the basics of Application Load Balancer and CloudFront. So let’s skip the “what is ALB” and dive into the real stuff!

Also known as a “Service Works Page” or “Downtime Page”, a maintenance page is a temporary screen shown to your users when your application is under maintenance or temporarily unavailable. It informs users politely, gives them confidence you’re in control, and avoids ugly errors. In AWS, many web apps sit behind an Application Load Balancer — and here’s the classic scenario with two pieces of news: one good and one bad. bad 🙂

The good news: ALB Ccn serve a fixed response. That’s right — you can configure ALB to return a static response directly. Here’s how to do it in Terraform:

resource "aws_lb_listener_rule" "maintenance_mode" {
  count        = var.maintenance_mode ? 1 : 0
  listener_arn = aws_lb_listener.app_443.arn
  priority     = 1
  action {
    type = "fixed-response"
    fixed_response {
      content_type = "text/html"
      message_body = "<html><body><h1>We'll be back soon!</h1></body></html>"
      status_code  = "503"
    }
  }
  condition {
    path_pattern {
      values = ["*"]
    }
  }
}

This rule intercepts all traffic (thanks to * path pattern) and shows a simple HTML message. The 503 status code indicates the site is temporarily unavailable. You can toggle this rule with a simple Terraform variable like:

 maintenance_mode = true.

But, here comes the pain. The message_body in fixed-response is limited to just 1024 bytes. That’s barely enough for a basic HTML snippet. Trying to inject a full web page will give you this error:

Validation error: FixedResponseAction messageBody must be less than or equal to 1024 bytes

Yes, message body length is restricted to 1Kb only, which irritates. So, reasonable question appears – how to bypass that limitation?

The Solution: Let CloudFront Do the Heavy Lifting. Most production apps already use CloudFront in front of ALB for performance. The good news? You can configure CloudFront to:

  • Catch 503 errors
  • Show a beautiful /maintenance.html page from S3
  • Keep everything clean and fast

Let me provide some sample code at firsts – and then explanations:

resource "aws_cloudfront_distribution" "maintenance_dist" {
  origin {
    domain_name = "${aws_s3_bucket.maintenance_page.bucket}.s3-website-us-east-1.amazonaws.com"
    origin_id   = "maintenance-s3-origin"

    custom_origin_config {
      origin_protocol_policy = "http-only"
      http_port              = 80
      https_port             = 443
      origin_ssl_protocols   = ["TLSv1.2"]
    }
  }

  enabled             = true
  default_root_object = "index.html"

  default_cache_behavior {
    allowed_methods  = ["GET", "HEAD"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = "maintenance-s3-origin"
    viewer_protocol_policy = "redirect-to-https"
    compress               = true

    cache_policy_id          = "4135ea2d-6df8-44a3-9df3-4b5a84be39ad" # AWS Managed CachingDisabled
    origin_request_policy_id = "88a5eaf4-2fd4-4709-b370-b4c650ea3fcf" # AWS Managed CORS-S3Origin
  }

  custom_error_response {
    error_code            = 503
    response_code         = 503
    response_page_path    = "/maintenance.html"
    error_caching_min_ttl = 60
  }

  viewer_certificate {
    cloudfront_default_certificate = true
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }
}

This CloudFront config is part of a setup where:

  • A custom error response maps 503 errors to /maintenance.html.
  • A static /maintenance.html file is stored in an S3 bucket.
  • CloudFront serves it automatically whenever a 503 error is returned (e.g., from ALB or origin failure).
  • There’s an ordered cache behavior to handle requests to /maintenance.html directly.

ordered_cache_behavior block defines a special behavior for requests matching /maintenance.html. It uses:

  • S3 cache and origin request policies to serve the file.
  • Limited allowed methods (GET, HEAD, OPTIONS).
  • A reference to the correct origin: maintenance-s3-origin.
  • AWS-Origin-Request-Managed-CORS-S3Origin-Policy – common AWS-managed Origin Request Policy

So: when someone accesses /maintenance.html directly, this behavior ensures it’s cached and served correctly from S3.

custom_error_response – This is the magic for redirecting error traffic to the maintenance page.

  • error_code = 503: Catches any 503 error from the origin (ALB, etc.).
  • response_page_path = “/maintenance.html”: Serves this page instead of the raw 503.
  • response_code = 503: Maintains the correct HTTP code for browsers/search engines.
  • error_caching_min_ttl = 60: Caches the error response for 60 seconds (so it doesn’t hit origin repeatedly).

How This All Works Together:

1. Normal operation: CloudFront proxies requests to your backend (ALB, S3, etc.).

2. Maintenance mode (e.g., ALB returns a 503 fixed-response):

  • CloudFront intercepts that 503.
  • Instead of passing it raw to the client, it responds with /maintenance.html.
  • This file is fetched from S3 via the cache behavior and served with correct headers.

3. The user sees a nice static maintenance page instead of a generic error.

You will also need to configure S3 bucket and policies related with that, e.g:

resource "aws_s3_bucket" "maintenance_page" {
  bucket = "my-maintenance-page"
}

resource "aws_s3_bucket_website_configuration" "maintenance" {
  bucket = aws_s3_bucket.maintenance_page.id

  index_document {
    suffix = "index.html"
  }

  error_document {
    key = "index.html"
  }
}

resource "aws_s3_bucket_policy" "public_access" {
  bucket = aws_s3_bucket.maintenance_page.id

  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [{
      Effect    = "Allow",
      Principal = "*",
      Action    = "s3:GetObject",
      Resource  = "${aws_s3_bucket.maintenance_page.arn}/*"
    }]
  })
}

resource "aws_s3_object" "maintenance_html" {
  bucket       = aws_s3_bucket.maintenance_page.id
  key          = "maintenance.html"
  source       = "maintenance.html" # path to your local file
  content_type = "text/html"
}

You may also put maintenance.html at terraform code directly and upload it at S3 after changes are made at content, using null_resource, e.g:

resource "null_resource" "upload_maintenance_page" {
  provisioner "local-exec" {
    command = "aws s3 cp ${path.module}/maintenance.html s3://${aws_s3_bucket.maintenance_page.id}/maintenance.html"
  }

  depends_on = [aws_s3_bucket_policy.public_access]
}

Thank you for your attentions. If you are interested in similar content, then sign up to my newsletter:

Or visit my courses 🙂

About the author

sergii-demianchuk

Software engineer with over 18 year’s experience. Everyday stack: PHP, Python, Java, Javascript, Symfony, Flask, Spring, Vue, Docker, AWS Cloud, Machine Learning, Ansible, Terraform, Jenkins, MariaDB, MySQL, Mongo, Redis, ElasticSeach

architecture AWS cluster cyber-security devops devops-basics docker elasticsearch flask geo high availability java machine learning opensearch php programming languages python recommendation systems search systems spring boot symfony

Privacy Overview
Sergii Demianchuk Blog

This website uses cookies so that we can provide you with the best user experience possible. Cookie information is stored in your browser and performs functions such as recognising you when you return to our website and helping our team to understand which sections of the website you find most interesting and useful.

Strictly Necessary Cookies

Strictly Necessary Cookie should be enabled at all times so that we can save your preferences for cookie settings.

3rd Party Cookies

This website uses Google Analytics to collect anonymous information such as the number of visitors to the site, and the most popular pages.

Keeping this cookie enabled helps us to improve our website.