🎉
CDKで別のリージョンのリソースを使用する方法
はじめに
CDKを使用していると、時々異なるリージョンにまたがるリソースを利用したい場合があります。例えば、CloudFrontディストリビューションで使用する証明書は必ず us-east-1リージョンである必要があります。このような場合、CDKでどのように対処すればよいかを調べました。
この記事の対象読者
- CDKで構成管理しているエンジニア
この記事に書くこと
- カスタムリソースの使用例
この記事に書かないこと
- CDKについての説明
- カスタムリソースとは何か?
方法
こちらのIssueが参考になりました。
[lambda] Lambda@Edge support · Issue #1575 · aws/aws-cdk (github.com)
- 一方の region でリソースを作成し、ARNを格納する SSM Parameter リソースを作成する
- もう一方の region で SSM Parameter への権限付与と取得のためのカスタムリソースを作成する
- 作成したカスタムリソースを介して、別の region のリソースのARNを取得して、
fromCertificateArn
などで使用する
具体的なコード例を紹介します。
サンプルコード
リージョンごとにスタックを作成します。
bin/cdk.ts
new CertificateStack(app, 'CertificateStack', {
env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: 'us-east-1' },
});
new CloudFrontStack(app, 'CloudFrontStack', {
env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: 'ap-northeast-2' },
});
一方のリージョンでリソース(ここでは ACM証明書)を作成し、SSM Parameter に含めます。
lib/certificate-stack.ts
// 以下のリソースは us-east-1 に作成される
const certificate = new acm.Certificate(this, "Certificate", {
domainName: "takeyuweb.co.jp",
subjectAlternativeNames: ["*.takeyuweb.co.jp"],
validation: acm.CertificateValidation.fromDns(publicHostedZone),
});
new ssm.StringParameter(this, 'CertificateArnParameter', {
dataType: ssm.ParameterDataType.TEXT,
description: 'Certifcate ARN for CloudFront',
parameterName: '/rails-takeyuwebinc/certificate_arn',
stringValue: certificate.certificateArn,
});
もう一方のリージョンで、先ほどの証明書を使用するCloudFrontディストリビューションを作成します。その際、カスタムリソース SsmParameterReader
を作成して使用します。
lib/cloud-front-stack.ts
// us-east-1 リージョンの証明書を使用する
const certificateArnReader = new SsmParameterReader(this, 'CertificateArnParameter', {
parameterName: '/rails-takeyuwebinc/certificate_arn',
region: 'us-east-1',
});
const certificate = acm.Certificate.fromCertificateArn(this, 'Certificate', certificateArnReader.getParameterValue());
const distribution = new cloudfront.Distribution(this, 'Distribution', {
domainNames: ['takeyuweb.co.jp', 'www.takeyuweb.co.jp'],
certificate: certificate, // 証明書を指定
comment: 'takeyuweb.co.jp',
defaultBehavior: {
origin: httpOrigin,
},
});
カスタムリソース SsmParameterReader
を作成します。
SsmParameterReader
は次のことを行います。
- SSMパラメータの取得に必要なポリシーの作成
- AWSのAPIを呼び出してSSMパラメータの値を取得
- 取得した値を返す
lib/ssm-parameter-reader.ts
// https://github.com/aws/aws-cdk/issues/1575#issuecomment-674767075
// https://stackoverflow.com/a/59774628/6058505
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as cr from 'aws-cdk-lib/custom-resources';
import * as iam from 'aws-cdk-lib/aws-iam';
interface SsmParameterReaderProps {
parameterName: string;
region: string;
}
export class SsmParameterReader extends Construct {
private reader: cr.AwsCustomResource;
get stringValue(): string {
return this.getParameterValue();
}
constructor(scope: Construct, name: string, props: SsmParameterReaderProps) {
super(scope, name);
const { parameterName, region } = props;
const customResource = new cr.AwsCustomResource(scope, `${name}CustomResource`, {
policy: cr.AwsCustomResourcePolicy.fromStatements([
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['ssm:GetParameter*'],
resources: [
cdk.Stack.of(this).formatArn({
service: 'ssm',
region,
resource: 'parameter',
resourceName: parameterName.replace(/^\/+/, ''), // remove leading '/', since formatArn() will add one
}),
],
}),
]),
onUpdate: {
service: 'SSM',
action: 'getParameter',
parameters: {
Name: parameterName,
},
region,
physicalResourceId: cr.PhysicalResourceId.of(Date.now().toString()), // Update physical id to always fetch the latest version
},
});
this.reader = customResource;
}
getParameterValue(): string {
return this.reader.getResponseField('Parameter.Value');
}
}
サンプルコード全体
bin/cdk.ts
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { CertificateStack } from '../lib/certificate-stack';
import { CloudFrontStack } from '../lib/cloud-front-stack';
const app = new cdk.App();
new CertificateStack(app, 'CertificateStack', {
env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: 'us-east-1' },
});
new CloudFrontStack(app, 'CloudFrontStack', {
env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: 'ap-northeast-2' },
});
app.synth();
lib/certificate-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ssm from 'aws-cdk-lib/aws-ssm';
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
import * as route53 from 'aws-cdk-lib/aws-route53';
export class CertificateStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const publicHostedZone = route53.PublicHostedZone.fromLookup(
this,
"PublicHostedZone",
{
domainName: "takeyuweb.co.jp",
}
);
const certificate = new acm.Certificate(this, "Certificate", {
domainName: "takeyuweb.co.jp",
subjectAlternativeNames: ["*.takeyuweb.co.jp"],
validation: acm.CertificateValidation.fromDns(publicHostedZone),
});
new route53.CaaRecord(this, 'CaaRecord', {
zone: publicHostedZone,
values: [
{
flag: 0,
tag: route53.CaaTag.ISSUE,
value: "amazontrust.com"
},
{
flag: 0,
tag: route53.CaaTag.ISSUEWILD,
value: "amazontrust.com"
}
]
});
new ssm.StringParameter(this, 'CertificateArnParameter', {
dataType: ssm.ParameterDataType.TEXT,
description: 'Certifcate ARN for CloudFront',
parameterName: '/rails-takeyuwebinc/certificate_arn',
stringValue: certificate.certificateArn,
});
}
}
lib/cloud-front-stack.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ssm from 'aws-cdk-lib/aws-ssm';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as cloudfront_origins from 'aws-cdk-lib/aws-cloudfront-origins';
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
import * as route53 from 'aws-cdk-lib/aws-route53';
import * as route53_targets from 'aws-cdk-lib/aws-route53-targets';
import { SsmParameterReader } from './ssm-parameter-reader';
export class CloudFrontStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// CloudFront Distributionを作成する
// カスタムオリジンは パラメータストアから取得する(パラメータ名 /rails-takeyuwebinc/origin)
const origin = ssm.StringParameter.valueForStringParameter(this, '/rails-takeyuwebinc/origin');
const httpOrigin = new cloudfront_origins.HttpOrigin(origin, {
protocolPolicy: cloudfront.OriginProtocolPolicy.HTTP_ONLY,
});
const publicHostedZone = route53.PublicHostedZone.fromLookup(
this,
"PublicHostedZone",
{
domainName: "takeyuweb.co.jp",
}
);
// us-east-1 リージョンの証明書を使用する
const certificateArnReader = new SsmParameterReader(this, 'CertificateArnParameter', {
parameterName: '/rails-takeyuwebinc/certificate_arn',
region: 'us-east-1',
});
const certificate = acm.Certificate.fromCertificateArn(this, 'Certificate', certificateArnReader.getParameterValue());
const distribution = new cloudfront.Distribution(this, 'Distribution', {
domainNames: ['takeyuweb.co.jp', 'www.takeyuweb.co.jp'],
certificate: certificate, // 証明書を指定
comment: 'takeyuweb.co.jp',
defaultBehavior: {
origin: httpOrigin,
},
});
new route53.ARecord(this, 'ARecord', {
zone: publicHostedZone,
target: route53.RecordTarget.fromAlias(new route53_targets.CloudFrontTarget(distribution)),
recordName: 'takeyuweb.co.jp',
});
new route53.CnameRecord(this, 'CnameRecord', {
zone: publicHostedZone,
domainName: 'takeyuweb.co.jp.',
recordName: 'www',
ttl: cdk.Duration.minutes(5),
});
}
}
まとめ
カスタムリソースを利用することで、CDK実行中にAPIを使ってリージョンをまたぐ操作ができました。このように、CDKではCloudFormationによる設定と異なりプログラミングできるのが大きな魅力です。
Discussion