🎉

CDKで別のリージョンのリソースを使用する方法

2024/04/08に公開

はじめに

CDKを使用していると、時々異なるリージョンにまたがるリソースを利用したい場合があります。例えば、CloudFrontディストリビューションで使用する証明書は必ず us-east-1リージョンである必要があります。このような場合、CDKでどのように対処すればよいかを調べました。

この記事の対象読者

  • CDKで構成管理しているエンジニア

この記事に書くこと

  • カスタムリソースの使用例

この記事に書かないこと

  • CDKについての説明
  • カスタムリソースとは何か?

方法

こちらのIssueが参考になりました。
[lambda] Lambda@Edge support · Issue #1575 · aws/aws-cdk (github.com)

  1. 一方の region でリソースを作成し、ARNを格納する SSM Parameter リソースを作成する
  2. もう一方の region で SSM Parameter への権限付与と取得のためのカスタムリソースを作成する
  3. 作成したカスタムリソースを介して、別の 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