Deploying Cross-Region Certificates with AWS Cloudfront via CDK
AWS Certificate Manager (ACM) can be used to deploy TLS certificates for use with things like Amazon Cloudfront. However, ACM certificates must be deployed in us-east-1
. In order to deploy an ACM certificate and associate with a resource outside of us-east-1
, a multiple stack deployment must be used. The repo here: https://github.com/schuettc/cdk-cross-region-cloudfront will show an example of this.
Certificate Stack
The first stack to be deployed will be the CertificateStack
. This stack will create a Certificate
in the us-east-1
region that can be used by a Cloudfront Distribution in another region.
interface CertificateStackProps extends StackProps { siteSubdomain: string; domain: string; hostedZoneId: string; } export class CertificateStack extends Stack { siteCertificate: Certificate; constructor(scope: Construct, id: string, props: CertificateStackProps) { super(scope, id, props); const certificateResources = new CertificateResources(this, 'certificate', { siteSubdomain: props.siteSubdomain, domain: props.domain, hostedZoneId: props.hostedZoneId, }); this.siteCertificate = certificateResources.siteCertificate; } } const app = new App(); const certEnv = { account: process.env.CDK_DEFAULT_ACCOUNT, region: 'us-east-1', }; const certProps = { siteSubdomain: process.env.SITE_SUBDOMAIN || '', domain: process.env.DOMAIN || '', hostedZoneId: process.env.HOSTED_ZONE_ID || '', }; const certStack = new CertificateStack(app, 'CertificateStack', { ...certProps, env: certEnv, crossRegionReferences: true, }); app.synth();
Here we are explicitly declaring us-east-1
as the region to deploy this Stack
to because ACM certificates used with CloudFront must use us-east-1
. We are passing in to the Stack
props from a .env
file that will be used to create the Certificate
. This Certificate
will be used as input to the next Stack
. Note that the Stack
must take the input crossRegionReferences: true
to let the next Stack
use this Certificate
.
Site Stack
interface SiteStackProps extends StackProps { siteCertificate: Certificate; siteSubdomain: string; domain: string; hostedZoneId: string; } export class SiteStack extends Stack { constructor(scope: Construct, id: string, props: SiteStackProps) { super(scope, id, props); const site = new SiteResources(this, 'site', { siteCertificate: props.siteCertificate, siteSubdomain: props.siteSubdomain, domain: props.domain, hostedZoneId: props.hostedZoneId, }); new CfnOutput(this, 'distributionURL', { value: site.distribution.domainName, }); new CfnOutput(this, 'siteUrl', { value: `https://${props.siteSubdomain}.${props.domain}`, }); } } const app = new App(); const siteEnv = { account: process.env.CDK_DEFAULT_ACCOUNT, region: 'eu-central-1', }; const siteProps = { siteCertificate: certStack.siteCertificate, siteSubdomain: process.env.SITE_SUBDOMAIN || '', domain: process.env.DOMAIN || '', hostedZoneId: process.env.HOSTED_ZONE_ID || '', }; new SiteStack(app, 'SiteStack', { ...siteProps, env: siteEnv, crossRegionReferences: true, }); app.synth();
Here we are creating a second Stack
as part of this deployment. This Stack
will deploy the actual website using the Certificate
from the previous Stack
. The props used for this Stack
are the same as the previous Stack
with the addition of the Certificate
created in the first Stack
. This Stack
uses a typical Single-Page Application (SPA) deployment of S3 + CloudFront with a specific Certificate
and ARecord
assigned.
this.siteBucket = new Bucket(this, 'websiteBucket', { publicReadAccess: false, removalPolicy: RemovalPolicy.DESTROY, autoDeleteObjects: true, }); const hostedZone = HostedZone.fromHostedZoneAttributes(this, 'hostedZone', { zoneName: props.domain, hostedZoneId: props.hostedZoneId, }); this.distribution = new Distribution(this, 'CloudfrontDistribution', { enableLogging: true, domainNames: [props.siteSubdomain + '.' + props.domain], certificate: props.siteCertificate, minimumProtocolVersion: SecurityPolicyProtocol.TLS_V1_2_2021, defaultBehavior: { origin: new S3Origin(this.siteBucket), viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, cachePolicy: CachePolicy.CACHING_DISABLED, }, defaultRootObject: 'index.html', }); new ARecord(this, 'clientSiteARecord', { zone: hostedZone, target: RecordTarget.fromAlias(new CloudFrontTarget(this.distribution)), recordName: props.siteSubdomain + '.' + props.domain, });
The result will be an SPA deployed to S3 with a CloudFront Distribution that uses a specific URL from Route53 and a Certificate provided by ACM. This site can be deployed in any Region while the Certificate will be deployed in us-east-1
as required by CloudFront.
.env File
To deploy this demo, a .env
file must be used. This .env
file will contain the following:
SITE_SUBDOMAIN = 'cross-region-site' DOMAIN = 'example.com' HOSTED_ZONE_ID = 'Z00000000000000000000'
Result
The result is an SPA deployed to a non us-east-1
Region while still using an ACM Certificate used by CloudFront all deployed by CDK. Information on cross-region certificates is available on the AWS CDK Reference Documentation.