やりたいこと
S3バケットtest-cloud-front-{accout}-ap-northeast-1
の配下に配置されたtest.json
ファイルに対し、https://config.example.com/test.json
でアクセスされたら、CloudFront経由でファイルを取得できるようにする
必要なリソース
AWS Certificate Manager
ドメイン名: config.example.com
の証明書
us-east-1で発行
CNAMEレコードによるバリデーションを行う
※CloudFrontの代替ドメインで必要になる証明書はus-east-1で発行されたものでなければならない
Route53
※example.comゾーンは作成済とする
AliasのAレコード(Record name: config.example.com
、Value: 今回作成するCloudFrontのDomain name)
S3
Bucket Name: test-cloud-front-{accout}-ap-northeast-1
Bucket policy: CloudFrontからのアクセスを許可(マネジメントコンソールからボタン1つで作成されるポリシー)
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::{accout}:role/app-stack-CustomS3AutoDeleteObjectsCustomResource~"
},
"Action": [
"s3:DeleteObject*",
"s3:GetBucket*",
"s3:List*"
],
"Resource": [
"arn:aws:s3:::test-cloud-front-{accout}-ap-northeast-1",
"arn:aws:s3:::test-cloud-front-{accout}-ap-northeast-1/*"
]
},
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity E278HEESTXZKCR"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::test-cloud-front-{accout}-ap-northeast-1/*"
},
{
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::test-cloud-front-{accout}-ap-northeast-1/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::{accout}:distribution/{distributionId}"
}
}
}
]
}
Cross-origin resource sharing (CORS):
[
{
"AllowedHeaders": [],
"AllowedMethods": [
"GET"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": []
}
]
※test.jsonファイルは手動でアップロード
CloudFront
Alternate domain names: config.example.com
Custom SSL certificate: config.example.com
Origin domain: test-cloud-front-{account}-ap-northeast-1.s3.ap-northeast-1.amazonaws.com
AWS CDKで構築
ACMだけ別のリージョンに作成しないといけないせいで、CDKで構築する場合、結構手間がかかります。
us-east-1とap-northeast-1両方にbootstrap
CDKでは2.50.0から簡単にクロスリージョンのデプロイができるようになりました (PR)。以下のコードは、東京リージョンからヴァージニアリージョンのリソースを参照する例です。クロスリージョンのリソースの受け渡し (スタック間参照) が実現できています。
通常のデプロイして即リソース反映でよければ、crossRegionReferences: true
による参照で作成可能です。
コード
{
...,
"scripts": {
"deploy": "cdk deploy --all --require-approval never"
},
"devDependencies": {
"@types/node": "20.1.0",
"aws-cdk": "2.79.1",
"ts-node": "^10.9.1",
"typescript": "~5.0.4"
},
"dependencies": {
"aws-cdk-lib": "2.79.1",
"constructs": "^10.0.0"
}
}
import * as cdk from 'aws-cdk-lib';
import { Stack, StackProps } from 'aws-cdk-lib';
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
import * as route53 from 'aws-cdk-lib/aws-route53';
import { Construct } from 'constructs';
export class VirginiaAcmStack extends Stack {
public readonly certificate: acm.Certificate;
constructor(scope: Construct, id: string, props: StackProps) {
super(scope, id, props);
// 既にRoute53にHostedZone作っている場合、fromLookup
const hostedZone = route53.HostedZone.fromLookup(this, 'TestHostedZone', {
domainName: `{YOUR_DOMAIN_NAME}`, // 今回はexample.com
});
this.certificate = new acm.Certificate(this, 'TestCertificate', {
domainName: `{YOUR_DOMAIN_NAME}`, // 今回はconfig.example.com
validation: acm.CertificateValidation.fromDns(hostedZone),
});
}
}
import { RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront'
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins'
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as iam from 'aws-cdk-lib/aws-iam'
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
import * as route53 from 'aws-cdk-lib/aws-route53'
import * as route53Targets from 'aws-cdk-lib/aws-route53-targets';
import { Construct } from 'constructs';
interface TokyoStackProps extends StackProps {
uSCertificate: acm.Certificate;
}
export class TokyoStack extends Stack {
constructor(scope: Construct, id: string, props: TokyoStackProps) {
super(scope, id, props);
const myBucket = new s3.Bucket(this, 'TestBucket', {
bucketName: `test-cloud-front-${props?.env?.account}-ap-northeast-1`
});
myBucket.addCorsRule({
allowedMethods: [s3.HttpMethods.GET],
allowedOrigins: ['*'],
});
const myCloudfront = new cloudfront.Distribution(this, 'TesCloudfront', {
defaultBehavior: { origin: new origins.S3Origin(myBucket) },
domainNames: [`{YOUR_DOMAIN_NAME}`], // 今回はconfig.example.com
certificate: props.uSCertificate,
});
myBucket.addToResourcePolicy(new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
principals: [new iam.ServicePrincipal('cloudfront.amazonaws.com')],
actions: ['s3:GetObject'],
resources: [`${myBucket.bucketArn}/*`],
conditions: {
StringEquals:{
"AWS:SourceArn": `arn:aws:cloudfront::${props?.env?.account}:distribution/${myCloudfront.distributionId}`,
}
}
}));
const myHostedZone = route53.HostedZone.fromLookup(this, 'TestHostedZone', { domainName: `{YOUR_DOMAIN_NAME}` }); // 今回はexample.com
new route53.ARecord(this, 'TestARecord', {
recordName: `{YOUR_RECORD_NAME}`, // 今回はconfig
zone: myHostedZone,
target: route53.RecordTarget.fromAlias(new route53Targets.CloudFrontTarget(myCloudfront)),
});
}
}
#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { TokyoStack } from '../lib/tokyo-stack';
import { VirginiaAcmStack } from '../lib/virginia-acm-stack';
const app = new cdk.App();
const account = `{YOUR_ACCOUNT}`;
const envUS = {
region: 'us-east-1',
account,
};
const envJP = {
region: 'ap-northeast-1',
account,
};
const stackUS = new VirginiaAcmStack(app, 'virginia-acm-stack', {
env: envUS,
crossRegionReferences: true,
});
new TokyoStack(app, 'tokyo-stack', {
env: envJP,
crossRegionReferences: true,
certificate: stackUS.certificate
});
--no-executeでのデプロイ
デプロイしてからマネジメントコンソールの画面上でCFnでChange setsを確認し、適用ボタンを押して初めてリソースに反映する方法です。
従来CDKでクロスリージョン参照といえば、cdk-remote-stack を使う方法が主流でした (更にその前は同じような機能を各自で実装していました)。ググって出てくる記事もこの方法が多いと思います。私もよくお世話になったライブラリです。
2023年では1の方法が利用できるため、この方法をあえて使う理由は無くなったと考えて良いでしょう。ただしこちらは弱い参照 (後述)なので、人によってはもうしばらく出番があるかもしれません。
--no-executeでのデプロイの場合、us-east-1のACMのリソース参照は弱い参照になるので、2023/5/28現時点ではcdk-remote-stack
を使うしかなさそうです。(⭐︎の数があまり多くないので、できれば使いたくないですが。。)
コード
{
...,
"scripts": {
"deploy": "cdk deploy --all --require-approval never --no-execute"
},
"dependencies": {
"cdk-remote-stack": "^2.0.11"
}
}
import * as cdk from 'aws-cdk-lib';
import { Stack, StackProps } from 'aws-cdk-lib';
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
import * as route53 from 'aws-cdk-lib/aws-route53';
import { Construct } from 'constructs';
export class VirginiaAcmStack extends Stack {
constructor(scope: Construct, id: string, props: StackProps) {
super(scope, id, props);
const hostedZone = route53.HostedZone.fromLookup(this, 'TestHostedZone', {
domainName: `{YOUR_DOMAIN_NAME}`, // 今回はexample.com
});
const certificate = new acm.Certificate(this, 'TestCertificate', {
domainName: `{YOUR_DOMAIN_NAME}`, // 今回はconfig.example.com
validation: acm.CertificateValidation.fromDns(hostedZone),
});
new cdk.CfnOutput(this, 'TestCertificateArn', {
value: certificate.certificateArn,
exportName: `${this.stackName}:CertificateArn`,
});
}
}
import { RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront'
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins'
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as iam from 'aws-cdk-lib/aws-iam'
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
import * as route53 from 'aws-cdk-lib/aws-route53'
import * as route53Targets from 'aws-cdk-lib/aws-route53-targets';
import { Construct } from 'constructs';
import { VirginiaAcmStack } from './virginia-acm-stack';
import { RemoteOutputs } from 'cdk-remote-stack';
interface TokyoStackProps extends StackProps {
stackUS: VirginiaAcmStack;
}
export class TokyoStack extends Stack {
constructor(scope: Construct, id: string, props: TokyoStackProps) {
super(scope, id, props);
const outputs = new RemoteOutputs(this, 'Outputs', { stack: props.stackUS })
const importedCertificateArn = outputs.get('TestCertificateArn')
const myBucket = new s3.Bucket(this, 'TestBucket', {
bucketName: `test-cloud-front-${props?.env?.account}-ap-northeast-1`
});
myBucket.addCorsRule({
allowedMethods: [s3.HttpMethods.GET],
allowedOrigins: ['*'],
});
const myCloudfront = new cloudfront.Distribution(this, 'TesCloudfront', {
defaultBehavior: { origin: new origins.S3Origin(myBucket) },
domainNames: [`{YOUR_DOMAIN_NAME}`], // 今回はconfig.example.com
certificate: acm.Certificate.fromCertificateArn(this, 'ImportedCertificate', importedCertificateArn),
});
myBucket.addToResourcePolicy(new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
principals: [new iam.ServicePrincipal('cloudfront.amazonaws.com')],
actions: ['s3:GetObject'],
resources: [`${myBucket.bucketArn}/*`],
conditions: {
StringEquals:{
"AWS:SourceArn": `arn:aws:cloudfront::${props?.env?.account}:distribution/${myCloudfront.distributionId}`,
}
}
}));
const myHostedZone = route53.HostedZone.fromLookup(this, 'TestHostedZone', { domainName: `{YOUR_DOMAIN_NAME}` }); // 今回はexample.com
new route53.ARecord(this, 'TestARecord', {
recordName: `{YOUR_RECORD_NAME}`, // 今回はconfig
zone: myHostedZone,
target: route53.RecordTarget.fromAlias(new route53Targets.CloudFrontTarget(myCloudfront)),
});
}
}
#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { TokyoStack } from '../lib/tokyo-stack';
import { VirginiaAcmStack } from '../lib/virginia-acm-stack';
const app = new cdk.App();
const account = `{YOUR_ACCOUNT}`;
const envUS = {
region: 'us-east-1',
account,
};
const envJP = {
region: 'ap-northeast-1',
account,
};
const stackUS = new VirginiaAcmStack(app, 'virginia-acm-stack', {
env: envUS
});
const stackJP = new TokyoStack(app, 'tokyo-stack', {
env: envJP,
stackUS
});
stackJP.addDependency(stackUS)
デプロイしたらCFnからchange setsで、変更を確認してから反映ボタンを押せばリソースが作成されます。
その他参考