- A basic web application firewall (
webacl
) with CloudFront as the scope.
- Parameter Store string parameter (
WEBACL_ID
) containing the ARN of the web application firewall.
Step 5: Create the S3 stack
Create a stack to deploy an S3 bucket. In the project’s lib
folder, create and save a file named s3-stack.ts
containing the following code:
import * as route53 from 'aws-cdk-lib/aws-route53';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as s3deploy from 'aws-cdk-lib/aws-s3-deployment';
import * as targets from 'aws-cdk-lib/aws-route53-targets';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as cloudwatch from "aws-cdk-lib/aws-cloudwatch";
import * as iam from 'aws-cdk-lib/aws-iam';
import { Aws, CfnOutput, RemovalPolicy, Stack } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { SSMParameterReader } from './ssm-parameter-reader';
export interface StaticSiteProps {
domainName: string;
siteSubDomain: string;
}
export class S3Stack extends Construct {
constructor(parent: Stack, name: string, props: StaticSiteProps) {
super(parent, name);
const zone = route53.HostedZone.fromLookup(this, 'Zone', { domainName: props.domainName });
const siteDomain = props.siteSubDomain + '.' + props.domainName;
const cloudfrontOAI = new cloudfront.OriginAccessIdentity(this, 'cloudfront-OAI', {
comment: `OAI for ${name}`
});
new CfnOutput(this, 'Site', { value: 'https://' + siteDomain });
// Content bucket
const siteBucket = new s3.Bucket(this, 'SiteBucket', {
bucketName: siteDomain,
websiteIndexDocument: 'index.html',
websiteErrorDocument: 'error.html',
publicReadAccess: false,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
removalPolicy: RemovalPolicy.RETAIN,
autoDeleteObjects: false,
});
// Grant access to cloudfront
siteBucket.addToResourcePolicy(new iam.PolicyStatement({
actions: ['s3:GetObject'],
resources: [siteBucket.arnForObjects('*')],
principals: [new iam.CanonicalUserPrincipal(cloudfrontOAI.cloudFrontOriginAccessIdentityS3CanonicalUserId)]
}));
new CfnOutput(this, 'Bucket', { value: siteBucket.bucketName });
const certificateArnReader = new SSMParameterReader(this, 'CertificateARNReader', {
parameterName: "CERTIFICATE_ARN",
region: 'us-east-1'
});
const webAclIdReader = new SSMParameterReader(this, 'WebAclIdReader', {
parameterName: "WEBACL_ID",
region: 'us-east-1'
});
new CfnOutput(this, 'Certificate', { value: certificateArnReader.getParameterValue() });
const viewerCertificate = cloudfront.ViewerCertificate.fromAcmCertificate({
certificateArn: certificateArnReader.getParameterValue(),
env: {
region: Aws.REGION,
account: Aws.ACCOUNT_ID
},
node: this.node,
stack: parent,
metricDaysToExpiry: () => new cloudwatch.Metric({
namespace: "TLS Viewer Certificate Validity",
metricName: "TLS Viewer Certificate Expired",
}),
applyRemovalPolicy: function (policy: RemovalPolicy): void {
throw new Error('Function not implemented.');
}
},
{
sslMethod: cloudfront.SSLMethod.SNI,
securityPolicy: cloudfront.SecurityPolicyProtocol.TLS_V1_1_2016,
aliases: [siteDomain]
})
// CloudFront distribution
const distribution = new cloudfront.CloudFrontWebDistribution(this, 'SiteDistribution', {
viewerCertificate,
originConfigs: [
{
s3OriginSource: {
s3BucketSource: siteBucket,
originAccessIdentity: cloudfrontOAI
},
behaviors: [{
isDefaultBehavior: true,
compress: true,
allowedMethods: cloudfront.CloudFrontAllowedMethods.GET_HEAD_OPTIONS,
}],
}
],
webACLId: webAclIdReader.getParameterValue()
});
new CfnOutput(this, 'DistributionId', { value: distribution.distributionId });
// Route53 alias record for the CloudFront distribution
new route53.ARecord(this, 'SiteAliasRecord', {
recordName: siteDomain,
target: route53.RecordTarget.fromAlias(new targets.CloudFrontTarget(distribution)),
zone
});
// Deploy site contents to S3 bucket
new s3deploy.BucketDeployment(this, 'DeployWithInvalidation', {
sources: [s3deploy.Source.asset('./site-contents')],
destinationBucket: siteBucket,
distribution,
distributionPaths: ['/*'],
});
}
}
The code in s3-stack.ts
contains the following components:
- An S3 bucket to store static website content.
- Two readers to obtain the following values from Parameter Store:
- SSL certificate ARN from parameter
CertificateArn
.
- Web application firewall ARN from parameter
WEBACL_ID
.
- A CloudFront viewer certificate based on the imported
CertificateArn
.
- A CloudFront distribution with the viewer certificate and web application firewall ARN, to link the distribution to the firewall.
BucketDeployment
construct to load static website content into the S3 bucket.
Step 6: Create an S3 folder for static website contents
Next, create a subfolder named ./site-contents
. In this folder, create and save a file named index.html
containing the following code. This is the content that the solution delivers from Amazon S3.
<!doctype html>
<html>
<head>
<title>This is the title of the webpage!</title>
</head>
<body>
<p>This is an example paragraph. Anything in the <strong>body</strong> tag will appear on the page, just like this <strong>p</strong> tag and its contents.</p>
</body>
</html>
Step 7: Deploy the AWS CDK app
To deploy the solution, first wrap the Amazon S3, web application firewall, and SSL certificate stacks into one application. In the project’s bin
folder, create and save a file named app.ts
containing the following code. The code contains the AWS CDK synth
command, which translates the resources defined in the three stacks into a CloudFormation template. In the code, replace every instance of ACCOUNT_ID
with your AWS account ID.
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { S3Stack } from '../lib/s3-stack';
import { CertificateStack } from '../lib/certificate-stack';
import { WafStack } from '../lib/waf-stack';
class MyStaticSiteStack extends cdk.Stack {
constructor(parent: cdk.App, name: string, props: cdk.StackProps) {
super(parent, name, props);
new S3Stack(this, 'StaticSite', {
domainName: "DOMAIN_NAME",
siteSubDomain: "www",
});
}
}
class MyCertificateStack extends cdk.Stack {
constructor(parent: cdk.App, name: string, props: cdk.StackProps) {
super(parent, name, props);
new CertificateStack(this, 'CertificateStack', {
domainName: "DOMAIN_NAME",
siteSubDomain: "www",
});
}
}
class MyWafStack extends cdk.Stack {
constructor(parent: cdk.App, name: string, props: cdk.StackProps) {
super(parent, name, props);
new WafStack(this, 'WafStack', {});
}
}
const app = new cdk.App();
async function main() {
const certificateStack = new MyCertificateStack(app, 'MyCertificateStack', {
env: {
account: "ACCOUNT_ID",
region: 'us-east-1',
}
});
const wafStack = new MyWafStack(app, 'MyWafStack', {
env: {
account: "ACCOUNT_ID",
region: 'us-east-1',
}
});
const mySiteStack = new MyStaticSiteStack(app, 'MyStaticSite', {
env: {
account: "ACCOUNT_ID",
region: 'eu-central-1',
}
})
mySiteStack.addDependency(certificateStack);
mySiteStack.addDependency(wafStack);
app.synth();
}
main();
Then run the following commands to deploy the application.
cdk bootstrap
cdk deploy --all
Step 8: Verify and test the solution
- In the Amazon CloudWatch console, choose the Europe (Frankfurt) (
eu-central-1
) Region from the top toolbar.
- Choose Stacks. Verify that the
MyStaticSite
and CDKToolkit
stacks display in the list, as shown in Figure 2.
Figure 2. Stacks deployed to eu-central-1
.
- Choose the US East (N. Virginia) (
us-east-1
) Region from the top toolbar.
- Choose Stacks. Verify that the
MyWafStack
web application, MyCertificateStack
SSL certificate, and CDKToolkit
stacks deployed display in the list, as shown in Figure 3.
Stacks deployed to us-east-1.
- Choose
MyWafStack
.
- Choose Outputs.
- Choose the URL in the Value column for the
StackSite
key. Verify that the content from ./site-contents/index.html
displays in your browser.
Cleanup
When you’re finished testing the deployment, run the following command to delete the stacks.
cdk destroy
Alternatively, to delete stacks using the console, see Deleting a stack on the AWS CloudFormation console.
Note: Keeping the AWL CLI installed does not incur future costs. For more information, see Installing, updating, and uninstalling the AWS CLI version 2.
Conclusion
In this post, we deployed a web application across multiple Regions using AWS CDK. We created an AWS CDK construct to deploy resources in multiple stacks and a custom resource to read parameter values across Regions.
Customize the solution used in this post to create constructs and custom resources for other cross-Regional deployments in the AWS Cloud. For example, you could build a construct to deploy an AWS Lambda function with access to an Amazon DynamoDB table in another Region. You can use a custom resource to share table and customer managed key information across Regions. For more information, check out the aws/constructs library on GitHub, and learn about the Construct Hub online registry of CDK libraries.
Let us know how you use AWS CDK and custom resources to deploy cross-Regional solutions. Use the Comments section for questions and comments.
About the authors
Jagdeep Singh Soni
Based in the Netherlands, Jagdeep is a senior partner solutions architect at AWS. He uses his passion for DevOps and builder tools to help both system integrators and technology partners. Jagdeep applies his application development and architecture background to drive innovation within his team and promote new technologies.
Michael Fraedrich
Michael is a senior partner solutions architect at AWS, based in Germany. Michael supports AWS software and technology partners with a focus on security and infrastructure as code (IaC). Outside of work, Michael enjoys craft activities and Internet of Things (IoT) home automation.