LoginSignup
5
3

【AWS CDK】CloudFront経由でS3のファイルを取得するインフラ構築を--no-executeのデプロイで

Last updated at Posted at 2023-05-28

やりたいこと

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による参照で作成可能です。

コード

package.json
{
  ...,
  "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"
  }
}
lib/virginia-acm-stack.ts
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),
    });
  }
}
lib/tokyo-stack.ts
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)),
    });
  }
}
bin/aws-cdk.ts
#!/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を使うしかなさそうです。(⭐︎の数があまり多くないので、できれば使いたくないですが。。)

コード

package.json
{
  ...,
  "scripts": {
    "deploy": "cdk deploy --all --require-approval never --no-execute"
  },
  "dependencies": {
    "cdk-remote-stack": "^2.0.11"
  }
}
lib/virginia-acm-stack.ts
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`,
    });
  }
}
lib/tokyo-stack.ts
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)),
    });
  }
}
bin/aws-cdk.ts
#!/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で、変更を確認してから反映ボタンを押せばリソースが作成されます。

その他参考

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3