Integration & Automation

Read parameters across AWS Regions with AWS CloudFormation custom resources

A challenge in deploying multistack AWS Cloud Development Kit (AWS CDK) applications is sharing parameters among resources across AWS Regions. While you can use AWS CloudFormation outputs to read parameters across stacks in the same Region, you can’t do this across Regions. For example, you might want to serve a static website with Amazon CloudFront from one Region that reads an SSL certificate in another Region. The answer lies in using AWS CloudFormation custom resources.

In this post, we show how to deploy a CloudFront distribution that’s linked to a web application firewall and an SSL certificate in another Region. We store the Amazon Resource Numbers (ARNs) of the SSL certificate and web application firewall in Parameter Store, a capability of AWS Systems Manager. We create a custom resource so that CloudFront can read the ARNs stored in a different Region. We deploy the entire solution with a construct comprising three AWS CDK stacks.

About this blog post
Time to read ~10 min.
Time to complete ~30 min.
Cost to complete ~$1
Learning level Advanced (300)
AWS services AWS Cloud Development Kit (AWS CDK)
Amazon CloudFront
AWS WAF
AWS Systems Manager Parameter Store
AWS Certificate Manager
Amazon Simple Storage Service (Amazon S3)

Overview

Figure 1 shows the resources that you deploy in the walkthrough.

Architecture diagram for multi-Region web-application deployment

Figure 1. Architecture diagram for multi-Region web-application deployment

The solution deploys the following resources:

  • In the US East (N. Virginia) (us-east-1) Region:
  • In the Europe (Frankfurt) (eu-central-1) Region:
    • CloudFront to deliver static website content.
    • Parameter Store to store the ARNs of the SSL certificate and web application firewall.
    • An Amazon Simple Storage Service (Amazon S3) bucket to store static website content.

Prerequisites

Before getting started, ensure that you have the following:

Walkthrough

In the walkthrough, you perform the following steps:

  • Step 1: Initialize the AWS CDK app.
  • Step 2: Create a custom resource.
  • Step 3: Create the SSL certificate stack.
  • Step 4: Create the AWS WAF stack.
  • Step 5: Create the S3 stack.
  • Step 6: Create an S3 folder for site contents.
  • Step 7: Verify and test the solution.

Step 1: Initialize the AWS CDK app

Initialize the AWS CDK app using the AWS CDK app template and TypeScript.

cdk init app --language typescript

Step 2: Create a custom resource

To reference parameters from Parameter Store, create a custom resource using the AwsCustomResource construct. In the project’s lib folder, create and save a file named ssm-parameter-reader.ts containing the following code:

import { Arn, Stack } from 'aws-cdk-lib';
import * as CustomResource from 'aws-cdk-lib/custom-resources';
import { Construct } from 'constructs';

interface SSMParameterReaderProps {
  parameterName: string;
  region: string;
}

function removeLeadingSlash(value: string): string {
  return value.slice(0, 1) == '/' ? value.slice(1) : value;
}
export class SSMParameterReader extends CustomResource.AwsCustomResource {
  constructor(scope: Construct, name: string, props: SSMParameterReaderProps) {
    const { parameterName, region } = props;

    const ssmAwsSdkCall: CustomResource.AwsSdkCall = {
      service: 'SSM',
      action: 'getParameter',
      parameters: {
        Name: parameterName,
      },
      region,
      physicalResourceId: CustomResource.PhysicalResourceId.of(Date.now().toString()),

    };

    const ssmCrPolicy = CustomResource.AwsCustomResourcePolicy.fromSdkCalls({
      resources: [
        Arn.format(
          {
            service: 'ssm',
            region: props.region,
            resource: 'parameter',
            resourceName: removeLeadingSlash(parameterName),
          },
          Stack.of(scope),
        ),
      ],
    });

    super(scope, name, { onUpdate: ssmAwsSdkCall, policy: ssmCrPolicy });
  }

  public getParameterValue(): string {
    return this.getResponseField('Parameter.Value').toString();
  }
}

The code in ssm-parameter-reader.ts contains the following:

  • A custom resource to read parameters in Parameter Store (ssmAwsSdkCall).
  • A policy to allow the invocation of the custom resource during stack creation (ssmCrPolicy).

Step 3: Create the SSL certificate stack

Create the first stack. The stack deploys an SSL certificate for a custom domain name managed with ACM to US East (N. Virginia) (us-east-1). In the project’s lib folder, create and save a file named certificate-stack.ts containing the following code:

import * as route53 from 'aws-cdk-lib/aws-route53';
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
import { CfnOutput, Stack } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { StringParameter } from 'aws-cdk-lib/aws-ssm'

export interface StaticSiteProps {
    domainName: string;
    siteSubDomain: string;
}
export class CertificateStack 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;

        new CfnOutput(this, 'Site', { value: 'https://' + siteDomain });

        // TLS certificate
        const certificateArn = new acm.DnsValidatedCertificate(this, 'SiteCertificate', {
            domainName: siteDomain,
            hostedZone: zone,
            region: 'us-east-1', // Cloudfront only checks this region for certificates.
        }).certificateArn;
        new CfnOutput(this, 'Certificate', { value: certificateArn });

        new StringParameter(this, 'CertificateARN', {
            parameterName: "CERTIFICATE_ARN",
            description: 'Certificate ARN to be used with Cloudfront',
            stringValue: certificateArn
        });
    }
}

The code in certificate-stack.ts does the following:

  • Imports your Route 53 hosted zone (zone).
  • Creates a domain name (siteDomain) by concatenating parameters siteSubDomain and domainName.
  • Creates an SSL certificate associated with the domain name (Certificate).
  • Creates a string parameter containing the SSL certificate ARN (CertifiateArn).

Step 4: Create the AWS WAF stack

Create a stack that deploys a web application firewall to US East (N. Viriginia) (us-east-1). In the project’s lib folder, create and save a file named waf-stack.ts containing the following code:

import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as wafv2 from 'aws-cdk-lib/aws-wafv2';
import { StringParameter } from 'aws-cdk-lib/aws-ssm'
export class WafStack extends Construct {
    constructor(parent: Stack, name: string, props: StackProps) {
        super(parent, name);

        const webacl = new wafv2.CfnWebACL(this, "BasicWaf", {
            defaultAction: {
                allow: {}
            },
            scope: "CLOUDFRONT",
            visibilityConfig: {
                cloudWatchMetricsEnabled: false,
                metricName: "waf-country-block-acl",
                sampledRequestsEnabled: false,
            },
        });

        new StringParameter(this, 'WebAclID', {
            parameterName: "WEBACL_ID",
            description: 'Web ACL ID',
            stringValue: webacl.attrArn
        });

    }
}

The code in waf-stack.ts contains the following components:

  • 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

  1. In the Amazon CloudWatch console, choose the Europe (Frankfurt) (eu-central-1) Region from the top toolbar.
  2. Choose Stacks. Verify that the MyStaticSite and CDKToolkit stacks display in the list, as shown in Figure 2.
Stacks deployed to eu-central-1.

Figure 2. Stacks deployed to eu-central-1.

  1. Choose the US East (N. Virginia) (us-east-1) Region from the top toolbar.
  2. 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.

Stacks deployed to us-east-1.

  1. Choose MyWafStack.
  2. Choose Outputs.
  3. 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.