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
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>
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>
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"
}
]
}
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/*"
]
}
]
}
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']
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.
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
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
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.
Now, let’s head to the S3 bucket and rename maintenance.html file to maintenance-on.html to enable the maintenance window.
Now, refresh the CloudFront domain and you should see the 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.