Leveraging Lambda@Edge to seamlessly manage frontend maintenance window like a pro

Vimal Paliwal - Sep 10 - - Dev Community

Photo by Bermix Studio on Unsplash


In today’s time, zero-downtime deployment is preferred for introducing updates/patches for our applications but exceptions arise and we may need to put on maintenance window before a rollout in certain situations. While working on a project, one such situation occurred and that’s when we decided to implement a simple and quick mechanism to manage maintenance window for our frontend application to deal with such situation.

We decided to use Lambda@Edge rather than CloudFront functions to achieve the end goal. In case you are wondering why we did not opt for CloudFront functions, let’s hold that thought for sometime and come back to that at a later point.

Basically, we used 4 components to achieve this task:

  • S3: To host our frontend application files
  • CloudFront: To serve our frontend application
  • Lambda: To handle maintenance window logic
  • GitHub Action: To toggle maintenance window

Let’s go through each of the components and understand how we executed it.


S3 Bucket

Let’s setup an S3 bucket to store the files. To keep it simple, we will upload just two files:

  • home.html
  • maintenance.html

Fig 1. S3 Bucket
Fig 1. S3 Bucket

home.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Home Page</title>
    <style>
        /* Set height to 100% for both html and body to make centering work */
        html, body {
            height: 100%;
            margin: 0;
            font-family: Arial, sans-serif;
            display: flex;
            justify-content: center; /* Centers horizontally */
            align-items: center; /* Centers vertically */
            background-color: #f0f0f0;
        }

        .welcome-message {
            background-color: #ffffff;
            padding: 20px 40px;
            border-radius: 8px;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
            text-align: center;
        }

        h1 {
            margin: 0;
            font-size: 2rem;
            color: #333333;
        }
    </style>
</head>

<body>
    <div class="welcome-message">
        <h1>Welcome to My Home Page!</h1>
    </div>
</body>

</html>
Enter fullscreen mode Exit fullscreen mode

maintenance.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Maintenance Mode</title>
    <style>
        html, body {
            height: 100%;
            margin: 0;
            font-family: 'Arial', sans-serif;
            display: flex;
            justify-content: center; /* Center horizontally */
            align-items: center; /* Center vertically */
            background-color: #f8d7da; /* Light red background to indicate maintenance */
            color: #721c24; /* Dark red text color */
        }

        .maintenance-message {
            background-color: #ffffff;
            padding: 30px 50px;
            border-radius: 10px;
            box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
            text-align: center;
            max-width: 600px;
        }

        h1 {
            margin: 0;
            font-size: 2.5rem;
        }

        p {
            margin-top: 15px;
            font-size: 1.2rem;
        }
    </style>
</head>

<body>
    <div class="maintenance-message">
        <h1>We're Under Maintenance</h1>
        <p>Our website is currently undergoing scheduled maintenance. We should be back shortly. Thank you for your patience!</p>
    </div>
</body>

</html>
Enter fullscreen mode Exit fullscreen mode

Now, let’s setup the Lambda function that will contain the logic to show maintenance page when it is switched on.


Lambda Function

The logic we are using to display the maintenance page is quite straightforward. If the maintenance-on.html file is present in the bucket, read its content and return it as HTML body. Otherwise, return the original request block.

Before we create the Lambda function, we will need an IAM role that has required permissions to write to CloudWatch logs and read the files from S3 bucket and appropriate trust relationship policy to be associated to CloudFront distribution, so let’s create the role first.

trust-relationship-policy.json

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": [
                    "edgelambda.amazonaws.com",
                    "lambda.amazonaws.com"
                ]
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

lambda-iam-role-policy.json

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:s3:::BUCKET_NAME/*"
            ]
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

Note: Make sure to update the placeholder BUCKET_NAME in the above IAM policy with your own bucket name.

Important points to consider before creating the lambda function:

  • It must be in us-east-1 region
  • And, the architecture must be x86_64

It’s time to create the lambda function using the above IAM role and the below Python code.

import boto3

from botocore.exceptions import ClientError

s3 = boto3.resource('s3')

def lambda_handler(event, context):
    try:
        body = s3.Object('maintenance-mode-demo', 'maintenance-on.html').get()['Body'].read()
        print('maintenance mode is switched on')
        return {
            'status': '200',
            'statusDescription': 'ok',
            'headers': {
                'content-type': [
                    {
                        'key': 'Content-Type',
                        'value': 'text/html'
                    }
                ]
            },
            'body': body
        }
    except ClientError:
        print('maintenance mode is switched off')
        return event['Records'][0]['cf']['request']
Enter fullscreen mode Exit fullscreen mode

Once you have created the Lambda function, publish a version of it by navigating to Versions tab. We will need this while associating lambda function with the CloudFront distribution.

Fig 2. Lambda Function
Fig 2. Lambda Function

Next step is to setup a CloudFront distribution to serve the static pages stored in S3 bucket and associate the lambda function to it.


CloudFront

I’m assuming you’re aware about how to set up CloudFront distribution to service a static website, if not you can follow this AWS article. It’s a simple 5 min process.

Fig 3. CloudFront Distribution
Fig 3. CloudFront Distribution

Lambda function can be associated either during CloudFront creation time or after it is created. I decided to choose the later option so that you get to learn both the ways.

Associating Lambda Function:

  • Navigate to Behaviors tab
  • Select the listed behavior in the table and click the Edit button
  • Scroll down to the Function associations section
  • For viewer request, select Lambda@Edge from dropdown, provide published lambda function version ARN, ignore the Include body checkbox and save the changes

We selected viewer request so that as soon as the request is received by the CloudFront distribution, we run the maintenance window logic and take an appropriate action.

Fig 4. Lambda function association
Fig 4. Lambda function association

That’s it. We are ready to test our setup.

We won’t be creating GitHub actions workflow in this article because all it does is rename the maintenance.html file to maintenance-on.html to enable the maintenance window and vice-versa.

To begin testing, let’s navigate to CloudFront domain to confirm if we can load the home page. This will also confirm that maintenance mode is currently disabled.

Fig 5. Home Page
Fig 5. Home Page

Now, let’s head to the S3 bucket and rename maintenance.html file to maintenance-on.html to enable the maintenance window.

Fig 6. S3 Bucket
Fig 6. S3 Bucket

Now, refresh the CloudFront domain and you should see the maintenance page.

Fig 7. Maintenance Page
Fig 7. Maintenance Page

Congratulations! You have successfully implemented a mechanism to manage maintenance mode for your frontend application.

Before concluding, let me explain why we chose Lambda@Edge over CloudFront Functions. At the time of writing, CloudFront Functions does not support modifying the response body or importing the boto3 library, which is necessary for interacting with S3.

Lastly, don't forget to delete all the resources if you followed along, as leaving them active could result in additional costs.


Understanding the basics

What is Lambda@Edge?
Lambda@Edge is a Amazon CloudFront feature that enables you to run Lambda functions at edge locations closer to your customers, improving performance and reducing latency.

When to use CloudFront Functions vs Lambda@Edge?
Both CloudFront Functions and Lambda@Edge runs your code at edge location to improve performance but CloudFront Functions can be used only simple operations like modifying headers, redirecting requests, etc using Javascript whereas Lambda@Edge is used to more complex operations where you need to interact with other AWS or third-party services and can be written in either NodeJS or Python.

Does Lambda@Edge have cold start?
Yes, Lambda@Edge is prone to cold start and the size of your Lambda function also plays a vital role in the cold start duration.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .