S3 に格納したリソースを CloudFront で配信する構成をCDKで作る

S3 に格納したリソースを CloudFront を介してのみアクセスできるようにする鉄板構成をCDKで構築します。
2020.08.12

やりたいこと

S3 に置いてある JSON ファイルを CloudFront で配信し、CloudFrontを経由してのみアクセスできるように設定します。

使うもの

  • Typescript
  • AWS CDK

AWS リソースを作成するのに AWS CDK を使い言語はTypescript で記述します。

本記事で利用した環境は以下の通りです。

tsc -v
Version 3.7.4

cdk version
1.57.0 (build 2ccfc50)

AWS リソースの作成

早速 AWS CDK を使って AWS リソースを作成しましょう。必要なリソースは S3 バケットと CloudFront です。

雛形の作成

からっぽのディレクトリを作成して、そこに CDK の雛形を作成します。

$ mkdir cdk
$ cd cdk
$ cdk init --language typescript

モジュールの追加

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

$ npm install @aws-cdk/aws-cloudfront @aws-cdk/aws-s3 @aws-cdk/aws-iam

Watch Mode を ON にする

Typescript は Javascript へトランスパイルしてからデプロイする必要があるので Typescript コンパイラの Watch Mode を ON にしておくことをお勧めします。

シンタックスエラーがある場合リアルタイムで教えてくれるので便利です。

$ tsc -w

AWS リソースの定義

lib/cdk-stack.tsにリソースを定義します。

S3 Bucket & Policy

バケットと CloudFrontOriginAccessIdentity を定義してそのあとに Policy を設定します。

cdk-stack.ts

    // Create Bucket
    const myBucket = new s3.Bucket(this, "my-bucket");

    // Create OriginAccessIdentity
    const oai = new cloudfront.OriginAccessIdentity(this, "my-oai");

    // Create Policy and attach to mybucket
    const myBucketPolicy = new iam.PolicyStatement({
      effect: iam.Effect.ALLOW,
      actions: ["s3:GetObject"],
      principals: [
        new iam.CanonicalUserPrincipal(
          oai.cloudFrontOriginAccessIdentityS3CanonicalUserId
        ),
      ],
      resources: [myBucket.bucketArn + "/*"],
    });
    myBucket.addToResourcePolicy(myBucketPolicy);

principalsに設定したアクセス元からのみに S3 バケットのGetObject権限を渡しています。上記のポリシーを設定することで、S3 バケットのオブジェクトは CloudFront を介してのみアクセスできるようになります。

ポリシーの中にOriginという単語がよく出てきますが、Origin = CloudFrontを介してアクセスしたいリソースです。

参考:オリジンアクセスアイデンティティを使用して Amazon S3 コンテンツへのアクセスを制限する

CloudFront WebDistribution

次にCloudFrontのWebDistributionの設定をします。

cdk-stack.ts

    // Create CloudFront WebDistribution
    new cloudfront.CloudFrontWebDistribution(this, "WebsiteDistribution", {
      viewerCertificate: {
        aliases: [],
        props: {
          cloudFrontDefaultCertificate: true,
        },
      },
      priceClass: cloudfront.PriceClass.PRICE_CLASS_ALL,
      originConfigs: [
        {
          s3OriginSource: {
            s3BucketSource: myBucket,
            originAccessIdentity: oai,
          },
          behaviors: [
            {
              isDefaultBehavior: true,
              minTtl: cdk.Duration.seconds(0),
              maxTtl: cdk.Duration.days(365),
              defaultTtl: cdk.Duration.days(1),
              pathPattern: "my-contents/*",
            },
          ],
        },
      ],
      errorConfigurations: [
        {
          errorCode: 403,
          responsePagePath: "/index.html",
          responseCode: 200,
          errorCachingMinTtl: 0,
        },
        {
          errorCode: 404,
          responsePagePath: "/index.html",
          responseCode: 200,
          errorCachingMinTtl: 0,
        },
      ],
    });

originConfigs

  • s3OriginSource: アクセスしたいリソースの格納先にS3を利用する場合はバケットを指定します
  • behaviors: pathPatternmy-contents/*を指定しています。これでバケットのmy-contents/ディレクトリ下の全てのリソースへアクセスを許可する設定になります

errorConfigurations

許可したパス以外へアクセスがあった際のルーティング設定です。今回はindex.htmlへリダイレクトされるよう設定します。

CDK 全文

lib/cdk-stack.ts

cdk-stack.ts

import * as cloudfront from "@aws-cdk/aws-cloudfront";
import * as iam from "@aws-cdk/aws-iam";
import * as s3 from "@aws-cdk/aws-s3";
import * as cdk from "@aws-cdk/core";

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

    // Create Bucket
    const myBucket = new s3.Bucket(this, "my-bucket");

    // Create OriginAccessIdentity
    const oai = new cloudfront.OriginAccessIdentity(this, "my-oai");

    // Create Policy and attach to mybucket
    const myBucketPolicy = new iam.PolicyStatement({
      effect: iam.Effect.ALLOW,
      actions: ["s3:GetObject"],
      principals: [
        new iam.CanonicalUserPrincipal(
          oai.cloudFrontOriginAccessIdentityS3CanonicalUserId
        ),
      ],
      resources: [myBucket.bucketArn + "/*"],
    });
    myBucket.addToResourcePolicy(myBucketPolicy);

    // Create CloudFront WebDistribution
    new cloudfront.CloudFrontWebDistribution(this, "WebsiteDistribution", {
      viewerCertificate: {
        aliases: [],
        props: {
          cloudFrontDefaultCertificate: true,
        },
      },
      priceClass: cloudfront.PriceClass.PRICE_CLASS_ALL,
      originConfigs: [
        {
          s3OriginSource: {
            s3BucketSource: myBucket,
            originAccessIdentity: oai,
          },
          behaviors: [
            {
              isDefaultBehavior: true,
              minTtl: cdk.Duration.seconds(0),
              maxTtl: cdk.Duration.days(365),
              defaultTtl: cdk.Duration.days(1),
              pathPattern: "my-contents/*",
            },
          ],
        },
      ],
      errorConfigurations: [
        {
          errorCode: 403,
          responsePagePath: "/index.html",
          responseCode: 200,
          errorCachingMinTtl: 0,
        },
        {
          errorCode: 404,
          responsePagePath: "/index.html",
          responseCode: 200,
          errorCachingMinTtl: 0,
        },
      ],
    });

}
}

上記をデプロイして動作確認をしてみましょう。

cdk deploy

動作確認

CloudFormationのリソースからS3バケットが作成されているのが確認できたらpathPatternとS3の構成を合わせるためにS3バケットのCreate Folderからmy-contentsというディレクトリを作成します。

以下のファイルを作成したパスへ置いて、CloudFrontからアクセスできるかを確認します。 URLはCloudFrontのマネジメントコンソールから見ることができます。

questions.json

questions.json

{
  "survey1": {
    "question": "すきなどうぶつをおしえてね",
    "options": [
      "うさぎさん",
      "ぞうさん",
      "きりんさん",
      "いるかさん",
      "ねこさん"
    ]
  },
  "survey2": {
    "question": "すきなフルーツをおしえてね",
    "options": ["もも", "いちご", "すいか", "パイナップル", "ぶどう"]
  }
}

閲覧することができました。

バケットへ直接アクセスするとどうなるでしょう

アクセスできないようになっています。

指定していないパスへアクセスするとどうなるでしょうか

index.htmlへリダイレクトされているのが確認できます。(index.htmlが存在しないのでNoSuchKeyになっています)

References