Main featured image

AWS CDKを使ってS3とCloudFrontでWebサイトを配信する

AWS
CDK
S3
CloudFront

AWS CDK を使って S3 と CloudFront で Web サイトを配信します。

S3 にアップロードした HTML を CloudFront で配信し、CloudFront を経由してのみ HTML にアクセスできるように設定します。

また、今回 CDK で使用する言語は TypeScript です。

環境
  • macOS Big Sur 11.2.3
  • node 15.14.0
  • npm 7.7.6
  • yarn 1.22.4
  • cdk 1.98.0
プロジェクトの作成

作業ディレクトリとプロジェクトを作成します。

作業ディレクトリは cdk-s3-cloud-front-deploy という名前にしています。

mkdir cdk-s3-cloud-front-deploy && cd cdk-s3-cloud-front-deploy

cdk init を実行して雛形を作成します。

cdk init --language typescript

実行が完了すると以下雛形が作成されました。

├── README.md
├── bin
│   └── cdk-s3-cloud-front-deploy.ts
├── cdk.json
├── jest.config.js
├── lib
│   └── cdk-s3-cloud-front-deploy-stack.ts
├── node_modules
│   ├── @aws-cdk
│   ├── @babel
│   :
│   :
├── package-lock.json
├── package.json
├── test
│   └── cdk-s3-cloud-front-deploy.test.ts
└── tsconfig.json
AWS module の install

S3 と CloudFront のモジュールを追加します。

S3 と CloudFront に加え IAM も利用するのでインストールしておきます。

$ yarn add @aws-cdk/aws-iam @aws-cdk/aws-s3 @aws-cdk/aws-cloudfront
S3 の Bucket を作成する

最初に S3 の Bucket を新規作成してみたいと思います。

今回作成するバケット名は cdk.json に記述したいと思います。

今回は cdk-sample-bucket というバケット名にします。

プロジェクトルート直下にある cdk.json を開いて、context の中に以下の記述をします。

{
  "app": "npx ts-node --prefer-ts-exts bin/cdk-s3-cloud-front-deploy.ts",
  "context": {
    "s3": {
      "bucketName": "cdk-sample-bucket"
    }
  }
}

次に実際に S3 のバケットを作成します。

記述は先程作成した雛形の以下ファイルを編集していきます。

  • bin/cdk-s3-cloud-front-deploy.ts
import * as cdk from '@aws-cdk/core';
import * as s3 from '@aws-cdk/aws-s3';

export class CdkS3CloudFrontDeployStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const bucketName: string = this.node.tryGetContext('s3').bucketName;
    const originBucket = new s3.Bucket(this, 'S3Bucket', {
      bucketName: bucketName,
      // Bucketへの直接アクセスを禁止
      accessControl: s3.BucketAccessControl.PRIVATE,
      // CDK Stack削除時にBucketも削除する
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });
  }
}

this.node.tryGetContext('s3').bucketName; で先程 cdk.json の context に記述した要素が取得できます。

それでは以下のコマンドを実行して deploy してみましょう。

cdk deploy

実行して以下のログが流れれば成功です。

$ cdk deploy
CdkS3CloudFrontDeployStack: deploying...
CdkS3CloudFrontDeployStack: creating CloudFormation changeset...
[█████████████████████████████·····························] (1/2)

 ✅  CdkS3CloudFrontDeployStack

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:999216002524:stack/CdkS3CloudFrontDeployStack/cedbfd30-a24c-11eb-bd62-0ed47b91b1b7

実際にバケットが作成されたかコンソールを開いて確認します。

posted image

確認したら以下のコマンドでバケットを削除しておきます。

cdk destroy

以下ログが流れたらコンソールを確認して S3 のバケットが削除されているか確認しましょう。

$ cdk destroy
Are you sure you want to delete: CdkS3CloudFrontDeployStack (y/n)? y
CdkS3CloudFrontDeployStack: destroying...
12:05:59 | DELETE_IN_PROGRESS   | AWS::CloudFormation::Stack | CdkS3CloudFrontDeployStack
12:06:02 | DELETE_IN_PROGRESS   | AWS::CDK::Metadata | CDKMetadata/Default
12:06:03 | DELETE_IN_PROGRESS   | AWS::S3::Bucket    | S3Bucket

 ✅  CdkS3CloudFrontDeployStack: destroyed
CloudFormation の Distribution を作成する

先程のファイルに CloudFormation の distribution を作成する実装を追記します。

ソースコード全文はこちらになります。

  • bin/cdk-s3-cloud-front-deploy.ts
import * as cdk from '@aws-cdk/core';
import * as s3 from '@aws-cdk/aws-s3';
import * as cloudfront from '@aws-cdk/aws-cloudfront';
import * as iam from '@aws-cdk/aws-iam';

export class CdkS3CloudFrontDeployStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // cdk.jsonからbucketNameを取得
    const bucketName: string = this.node.tryGetContext('s3').bucketName;
    // bucketを新規作成
    const bucket = new s3.Bucket(this, 'S3Bucket', {
      bucketName: bucketName,
      // Bucketへの直接アクセスを禁止
      accessControl: s3.BucketAccessControl.PRIVATE,
      // CDK Stack削除時にBucketも削除する
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      websiteIndexDocument: 'index.html',
      websiteErrorDocument: 'index.html',
    });

    // S3 を公開状態にすることなく、S3 へのアクセスを CloudFront からのリクエストに絞る為の仕組み
    const identity = new cloudfront.OriginAccessIdentity(this, 'OriginAccessIdentity', {
      comment: `${bucket.bucketName} access identity`,
    });

    // principalsに設定したアクセス元からのみに S3 バケットのGetObject権限を渡す
    // ポリシーを設定することで、S3 バケットのオブジェクトは CloudFront を介してのみアクセスできる
    const bucketPolicyStatement = new iam.PolicyStatement({
      actions: ['s3:GetObject'],
      effect: iam.Effect.ALLOW,
      principals: [identity.grantPrincipal],
      resources: [`${bucket.bucketArn}/*`],
    });
    // bucketにポリシーをアタッチ
    bucket.addToResourcePolicy(bucketPolicyStatement);
    // CloudFrontのdistribution作成
    new cloudfront.CloudFrontWebDistribution(this, 'WebDistribution', {
      enableIpV6: true,
      httpVersion: cloudfront.HttpVersion.HTTP2,
      viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
      originConfigs: [
        {
          s3OriginSource: {
            s3BucketSource: bucket,
            originAccessIdentity: identity,
          },
          behaviors: [
            {
              isDefaultBehavior: true,
              allowedMethods: cloudfront.CloudFrontAllowedMethods.GET_HEAD,
              cachedMethods: cloudfront.CloudFrontAllowedCachedMethods.GET_HEAD,
              forwardedValues: {
                queryString: false,
              },
            },
          ],
        },
      ],
      // 403/404エラーはindex.htmlを表示
      errorConfigurations: [
        {
          errorCode: 403,
          responseCode: 200,
          errorCachingMinTtl: 0,
          responsePagePath: '/index.html',
        },
        {
          errorCode: 404,
          responseCode: 200,
          errorCachingMinTtl: 0,
          responsePagePath: '/index.html',
        },
      ],
      priceClass: cloudfront.PriceClass.PRICE_CLASS_200,
    });
  }
}
Deploy する

それでは以下のコマンドを実行して deploy してみましょう。

cdk deploy

途中で Do you wish to deploy these changes (y/n)? と効かれるので y を入力します。

以下のログが流れれば deploy 成功です。

$ cdk deploy
This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening).
Please confirm you intend to make the following modifications:

IAM Statement Changes
┌───┬─────────────────────────────────────┬────────┬──────────────┬──────────────────────────────────────┬───────────┐
│   │ Resource                            │ Effect │ Action       │ Principal                            │ Condition │
├───┼─────────────────────────────────────┼────────┼──────────────┼──────────────────────────────────────┼───────────┤
│ + │ ${S3Bucket.Arn}/*                   │ Allow  │ s3:GetObject │ CanonicalUser:${OriginAccessIdentity │           │
│   │                                     │        │              │ .S3CanonicalUserId}                  │           │
└───┴─────────────────────────────────────┴────────┴──────────────┴──────────────────────────────────────┴───────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)

Do you wish to deploy these changes (y/n)? y
CdkS3CloudFrontDeployStack: deploying...
CdkS3CloudFrontDeployStack: creating CloudFormation changeset...
[██████████████████████████████████████████████████████████] (6/6)

 ✅  CdkS3CloudFrontDeployStack

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:999216002524:stack/CdkS3CloudFrontDeployStack/cd1f9570-a273-11eb-80c3-06420aaf754f
動作確認

動作確認の為、ローカルの HTML ファイルを S3 にアップロードします。

mkdir ./html
echo '<html><head><title>CDK deploy test</title></head><body><h1>Hello! CDK!</h1></body></html>' > ./html/index.html

上記のように index.html を作成して S3 にファイルをアップロードしておきます。

posted image

次に CloudFront コンソールにアクセスして CloudFront の distribution が作成されていることを確認します。

Domain Name が作成された CloudFront のドメインです。

posted image

CloudFront のドメインにアクセスして先程作成した HTML が表示されれば成功です。

posted image

次に S3 のドメインにアクセスしてみてください。

S3 のドメインは CloudFront の Origin に記載されている ドメイン です。

アクセスすると以下のように Access Denied になるはずです。

posted image

また、CloudFront のドメインの後に適当なパスを入力してみてください。

404 エラーは index.html を表示するように実装しているので、先程の Hello! CDK! が表示されるはずです。

Deploy した環境を削除する

作成した環境を削除したい場合は cdk destroy コマンドを実行してください。

cdk destroy

途中で Are you sure you want to delete: {StackName} (y/n)? と聞かれますので y を入力します。

ここで注意が必要なのが S3 にファイルが残っていると以下のエラーが発生します。

以下ログが流れたら S3 にあるファイルを全て削除して再度 cdk destroy を実行します。

 ❌  CdkS3CloudFrontDeployStack: destroy failed Error: The stack named CdkS3CloudFrontDeployStack is in a failed state. You may need to delete it from the AWS console : DELETE_FAILED (The following resource(s) failed to delete: [S3Bucket07682993]. )
    at Object.waitForStackDelete (/usr/local/lib/node_modules/aws-cdk/lib/api/util/cloudformation.ts:277:11)
    at processTicksAndRejections (node:internal/process/task_queues:94:5)
    at Object.destroyStack (/usr/local/lib/node_modules/aws-cdk/lib/api/deploy-stack.ts:395:28)
    at CdkToolkit.destroy (/usr/local/lib/node_modules/aws-cdk/lib/cdk-toolkit.ts:253:9)
    at initCommandLine (/usr/local/lib/node_modules/aws-cdk/bin/cdk.ts:208:9)
The stack named CdkS3CloudFrontDeployStack is in a failed state. You may need to delete it from the AWS console : DELETE_FAILED (The following resource(s) failed to delete: [S3Bucket07682993]. )

以下のログが流れれば削除成功です。

$ cdk destroy
Are you sure you want to delete: CdkS3CloudFrontDeployStack (y/n)? y
CdkS3CloudFrontDeployStack: destroying...
17:21:45 | DELETE_IN_PROGRESS   | AWS::CloudFormation::Stack                      | CdkS3CloudFrontDeployStack

 ✅  CdkS3CloudFrontDeployStack: destroyed

S3 と CloudFront のコンソールにアクセスして、Bucket や Distribution が削除されていることを確認してください。

おわりに

簡単ですが S3 と CloudFront で Web サイトを配信する方法でした。

筆者は CDK 初心者の為、内容が誤っていたり、もっとこういうやり方があるよ!という方はぜひ Twitter で DM していただくか Contact で教えて頂けると助かります。

更に調べてみるとローカルのソースを CDK で S3 にデプロイしたり、CloudFront のキャッシュ削除の自動化、独自ドメインを CloudFront に割り当てるなど色々出来そうです。

これらも CDK で実装できたら記事にしていきます。

最後に、ここまでのソースは Github にもありますので参照ください。

Written by ZUMA a.k.a. Kazuma. Web/Mobile App developer.  My profile.
Tags
Archives
2021-042021-032021-022021-01
Recent Posts