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 🙂