Handling a maintenance page with Amazon CloudFront, AWS WAF and CDK
Nobody really wants it, but sometimes you need it – even if it's just for a short period of time: A maintenance page. In this blog post I want to describe how you can setup a maintenance page when working with CloudFront in AWS. Architecture CloudFront does not have a built-in functionality to set a web application into maintenance mode. A common way to achieve this is by integrating a Web Application Firewall (WAF) before CloudFront. With WAF and its Web ACLs (Web Access Control Lists) you can block all requests against your CloudFront domain and instead respond with a maintenance page. What at first seems like a detour actually brings with it several advantages. More on that below. Challenge: multi-region WAF has an important requirement if it is used together with CloudFront: WAF resources must be deployed in us-east-1. Because CloudFront is a global resource, WAF must also be global (-> technically that means us-east-1). This has to be considered if your web application runs in another region. In the following code example we assume such a case. Code example CDK The example application consists of two stacks: A SecurityStack including our WAF and Web ACLs which will be deployed in us-east-1 and an AppStack which will be deployed in the default region (e.g. eu-central-1). const app = new cdk.App(); new SecurityStack(app, "SecurityStack"); new AppStack(app, "AppStack"); SecurityStack The SecurityStack uses an environment variable MAINTENANCE_MODE to control whether requests should be blocked or not. As mentioned above, our example assumes that the application runs in a different region than us-east-1. As it is not possible to share CloudFormation Exports between regions, we have to write the ARN of the created WAF to the AWS Systems Manager Parameter Store. This allows us to retrieve the ARN via an API call inside the AppStack and configure CloudFront to use the WAF (see: AppStack). export class SecurityStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, { ...props, env: { region: "us-east-1", }, }); const { MAINTENANCE_MODE } = process.env; const maintenancePageHtmlContent = readFileSync( resolve(__dirname, "html", "maintenance.html"), "utf8" ); const webACL = new aws_wafv2.CfnWebACL(this, "WebACL", { scope: "CLOUDFRONT", defaultAction: { ...(MAINTENANCE_MODE === "true" ? { block: { customResponse: { responseCode: 200, customResponseBodyKey: "maintenance", }, }, } : { allow: {}, }), }, customResponseBodies: { maintenance: { content: maintenancePageHtmlContent, contentType: "TEXT_HTML", }, }, }); // Write ARN of Web ACL into ParameterStore to make it available for Stacks in other regions new cdk.aws_ssm.StringParameter(this, "WebACLParameter", { parameterName: "/yourApplicationName/webACLArn", stringValue: webACL.attrArn, }); } } AppStack To retrieve the ARN of the WAF we can use an Custom Resource and perform an API call against the Parameter Store (SSM). export class AppStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); // Fetch ARN for webACL from ParameterStore in us-east-1 const webACLArnParameter = new custom_resources.AwsCustomResource( this, "WebACLArnParameter", { onUpdate: { service: "SSM", action: "GetParameter", parameters: { Name: "/yourApplicationName/webACLArn", }, region: "us-east-1", physicalResourceId: custom_resources.PhysicalResourceId.of( Date.now().toString() ), }, policy: custom_resources.AwsCustomResourcePolicy.fromSdkCalls({ resources: custom_resources.AwsCustomResourcePolicy.ANY_RESOURCE, }), } ); // Create CloudFront Distribution new aws_cloudfront.Distribution(this, "Distribution", { webAclId: webACLArnParameter.getResponseField("Parameter.Value"), }); } } Advantages Amazon's Web Application Firewall (WAF) is a very flexible service which brings a lot more functionality than used above. Developer access during maintenance When your application is in maintenance mode it might be useful to grant access for example to you as a developer (team). Therefore we can configure additional rules in our Web ACL. On top of the default action which blocks all access an additional rule can grant access for specific IP addresses (See: Docs) Security layer If WAF and Web ACL is already deployed, we can also increase security by adding AWS managed rule

Nobody really wants it, but sometimes you need it – even if it's just for a short period of time: A maintenance page.
In this blog post I want to describe how you can setup a maintenance page when working with CloudFront in AWS.
Architecture
CloudFront does not have a built-in functionality to set a web application into maintenance mode. A common way to achieve this is by integrating a Web Application Firewall (WAF) before CloudFront. With WAF and its Web ACLs (Web Access Control Lists) you can block all requests against your CloudFront domain and instead respond with a maintenance page. What at first seems like a detour actually brings with it several advantages. More on that below.
Challenge: multi-region
WAF has an important requirement if it is used together with CloudFront: WAF resources must be deployed in us-east-1
. Because CloudFront is a global resource, WAF must also be global (-> technically that means us-east-1
). This has to be considered if your web application runs in another region. In the following code example we assume such a case.
Code example
CDK
The example application consists of two stacks: A SecurityStack
including our WAF and Web ACLs which will be deployed in us-east-1
and an AppStack
which will be deployed in the default region (e.g. eu-central-1
).
const app = new cdk.App();
new SecurityStack(app, "SecurityStack");
new AppStack(app, "AppStack");
SecurityStack
The SecurityStack
uses an environment variable MAINTENANCE_MODE
to control whether requests should be blocked or not.
As mentioned above, our example assumes that the application runs in a different region than us-east-1
. As it is not possible to share CloudFormation Exports between regions, we have to write the ARN of the created WAF to the AWS Systems Manager Parameter Store. This allows us to retrieve the ARN via an API call inside the AppStack
and configure CloudFront to use the WAF (see: AppStack).
export class SecurityStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, {
...props,
env: {
region: "us-east-1",
},
});
const { MAINTENANCE_MODE } = process.env;
const maintenancePageHtmlContent = readFileSync(
resolve(__dirname, "html", "maintenance.html"),
"utf8"
);
const webACL = new aws_wafv2.CfnWebACL(this, "WebACL", {
scope: "CLOUDFRONT",
defaultAction: {
...(MAINTENANCE_MODE === "true"
? {
block: {
customResponse: {
responseCode: 200,
customResponseBodyKey: "maintenance",
},
},
}
: {
allow: {},
}),
},
customResponseBodies: {
maintenance: {
content: maintenancePageHtmlContent,
contentType: "TEXT_HTML",
},
},
});
// Write ARN of Web ACL into ParameterStore to make it available for Stacks in other regions
new cdk.aws_ssm.StringParameter(this, "WebACLParameter", {
parameterName: "/yourApplicationName/webACLArn",
stringValue: webACL.attrArn,
});
}
}
AppStack
To retrieve the ARN of the WAF we can use an Custom Resource and perform an API call against the Parameter Store (SSM).
export class AppStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
// Fetch ARN for webACL from ParameterStore in us-east-1
const webACLArnParameter = new custom_resources.AwsCustomResource(
this,
"WebACLArnParameter",
{
onUpdate: {
service: "SSM",
action: "GetParameter",
parameters: {
Name: "/yourApplicationName/webACLArn",
},
region: "us-east-1",
physicalResourceId: custom_resources.PhysicalResourceId.of(
Date.now().toString()
),
},
policy: custom_resources.AwsCustomResourcePolicy.fromSdkCalls({
resources: custom_resources.AwsCustomResourcePolicy.ANY_RESOURCE,
}),
}
);
// Create CloudFront Distribution
new aws_cloudfront.Distribution(this, "Distribution", {
webAclId: webACLArnParameter.getResponseField("Parameter.Value"),
});
}
}
Advantages
Amazon's Web Application Firewall (WAF) is a very flexible service which brings a lot more functionality than used above.
Developer access during maintenance
When your application is in maintenance mode it might be useful to grant access for example to you as a developer (team). Therefore we can configure additional rules in our Web ACL. On top of the default action which blocks all access an additional rule can grant access for specific IP addresses (See: Docs)
Security layer
If WAF and Web ACL is already deployed, we can also increase security by adding AWS managed rules. AWS provides a variety of managed rules to protect e.g. against DDOS attacks etc. (See: Docs)
Full example
You can find the full code example on GitHub
Suggestions or feedback
If you have any kind of feedback, suggestions or ideas - feel free and write a comment below this article. There is always space for improvement!