WebサイトをCDKでサクッと立ち上げる

2023/09/01に公開

これは何をするもの?

TypeScript ベースの AWS CDK を利用して静的コンテンツな Web サイトを構築するための一式です。

以前に紹介した「S3で更新したファイルを自動ですぐに反映する」を組み込んでいるので、後からHTMLや画像を差し替えもすぐ反映できるようにしています。

準備

Node.js

https://nodejs.org/ja

からダウンロードしてインストールするか、OS のパッケージツールで入れます。
本記事では Node.js 18.17.1 LTS を利用しています。

CDK Toolkit

CDK ツールキットをグローバルインストールします。本記事では 2.93.0 です。

npm -g i aws-cdk

始める

プロジェクトを作る

真っ新な cdk-sample ディレクトリを作り、その中で cdk init します。

mkdir cdk-sample
cd cdk-sample
cdk init app --language=typescript

サンプルのスタックやテストがありますが、あとで消します。

スタック作成

まずはメインとなるスタック部分です。
配布元となるコンテンツの格納場所として S3 にバケットを用意して、それをオリジンとした CloudFront のディストリビューションを作る構成です。

lib/website-stack.ts
import { RemovalPolicy, Stack, StackProps } from "aws-cdk-lib";
import {
  AllowedMethods, CachePolicy, CachedMethods, Distribution,
  OriginAccessIdentity, OriginRequestPolicy, PriceClass,
  ResponseHeadersPolicy, SecurityPolicyProtocol, ViewerProtocolPolicy,
} from "aws-cdk-lib/aws-cloudfront";
import { S3Origin } from "aws-cdk-lib/aws-cloudfront-origins";
import {
  CanonicalUserPrincipal, Effect, PolicyStatement
} from "aws-cdk-lib/aws-iam";
import {
  BlockPublicAccess, Bucket, BucketEncryption, HttpMethods,
} from "aws-cdk-lib/aws-s3";
import { Construct } from "constructs";
import { CacheInvalidationFunction } from "./cache-invalidation-function";

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

    // 配信するコンテンツ置き場
    const bucket = new Bucket(this, "Content", {
      removalPolicy: RemovalPolicy.RETAIN,
      blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
      encryption: BucketEncryption.S3_MANAGED,
      cors: [
        {
          allowedMethods: [HttpMethods.HEAD, HttpMethods.GET],
          allowedOrigins: ["*"],
          allowedHeaders: ["*"],
        },
      ],
    });

    // CloudFront の設定
    const oai = new OriginAccessIdentity(this, "OAI");
    const distribution = new Distribution(this, "Site", {
      enableIpv6: true,
      priceClass: PriceClass.PRICE_CLASS_200,
      minimumProtocolVersion: SecurityPolicyProtocol.TLS_V1_2_2018,
      defaultRootObject: "index.html",
      defaultBehavior: {
        allowedMethods: AllowedMethods.ALLOW_GET_HEAD_OPTIONS,
        cachedMethods: CachedMethods.CACHE_GET_HEAD_OPTIONS,
        cachePolicy: CachePolicy.CACHING_OPTIMIZED,
        originRequestPolicy: OriginRequestPolicy.CORS_S3_ORIGIN,
        responseHeadersPolicy: ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS_WITH_PREFLIGHT,
        viewerProtocolPolicy: ViewerProtocolPolicy.HTTPS_ONLY,
        // S3 バケットへ接続
        origin: new S3Origin(bucket, { originAccessIdentity: oai }),
      },
    });

    // S3 バケットに OAI を利用したアクセスを許可
    const bucketPolicy = new PolicyStatement({
      actions: ["s3:GetObject"],
      effect: Effect.ALLOW,
      principals: [new CanonicalUserPrincipal(oai.cloudFrontOriginAccessIdentityS3CanonicalUserId)],
      resources: [`${bucket.bucketArn}/*`],
    });
    bucket.addToResourcePolicy(bucketPolicy);

    // 更新されたコンテンツの再キャッシュを促す Lambda を連結
    new CacheInvalidationFunction(this, "CacheInvalidation", {
      bucket,
      distribution,
    });
  }
}

再キャッシュ用の Lambda 関数リソース

使い勝手が良いように Lambda Function リソース Function を継承する形で独自のリソースを作ります。

CDK リソース部分

lib/cache-invalidation-function/index.ts
import { join } from "path";

import { Duration } from "aws-cdk-lib";
import { IDistribution } from "aws-cdk-lib/aws-cloudfront";
import { Code, Function, FunctionProps, Runtime } from "aws-cdk-lib/aws-lambda";
import { EventType, IBucket } from "aws-cdk-lib/aws-s3";
import { LambdaDestination } from "aws-cdk-lib/aws-s3-notifications";
import { Construct } from "constructs";

export interface CacheInvalidationFunctionProps
  extends Omit<FunctionProps, "runtime" | "code" | "handler"> {
  bucket: IBucket;
  distribution: IDistribution;
}

export class CacheInvalidationFunction extends Function {
  constructor(scope: Construct, id: string, props: CacheInvalidationFunctionProps) {
    const { bucket, distribution, ...innerProps } = props;

    super(scope, id, {
      runtime: Runtime.NODEJS_18_X,
      code: Code.fromAsset(join(__dirname, "lambda")),
      handler: "index.handler",
      timeout: Duration.seconds(30),
      memorySize: 128,
      environment: {
        DISTRIBUTION_ID: distribution.distributionId,
      },
      ...innerProps,
    });

    bucket.addEventNotification(EventType.OBJECT_CREATED, new LambdaDestination(this));
    bucket.addEventNotification(EventType.OBJECT_REMOVED, new LambdaDestination(this));

    // CloudFront のキャッシュ削除権限を付与
    distribution.grantCreateInvalidation(this);
  }
}

Lambda ハンドラー

S3 バケットのイベント通知を処理するハンドラー部分です。

S3で更新したファイルを自動ですぐに反映する」のソースまんまです。

CacheInvalidationFunction リソースの指定から受け取った環境変数 DISTRIBUTION_ID を参照して該当の CloudFront ディストリビューションに対してキャッシュクリアを要求します。

lib/cache-invalidation-function/lambda/index.ts
import { CloudFront } from "@aws-sdk/client-cloudfront";

export const handler = async (event) => {
  const s3keys = event.Records.map(
    (record) => encodeURI(`/${record.s3.object.key}`)
  );

  const client = new CloudFront();
  await client.createInvalidation({
    DistributionId: process.env.DISTRIBUTION_ID,
    InvalidationBatch: {
      CallerReference: new Date().toISOString(),
      Paths: {
        Quantity: s3keys.length,
        Items: s3keys,
      },
    },
  });
};

CDK bin

スタックのとりまとめをする bin 部分と cdk の設定を調整します。

bin/main.ts
import "source-map-support/register";
import * as cdk from "aws-cdk-lib";
import { WebSiteStack } from "../lib/website-stack";

const app = new cdk.App();
new WebSiteStack(app, "WebSite", {
  env: {
    account: process.env.CDK_DEFAULT_ACCOUNT,
    region: process.env.CDK_DEFAULT_REGION,
  },
});

cdk.json は以下の内容で置き換えます。

cdk.json
{
  "app": "npx ts-node --prefer-ts-exts bin/main.ts",
  "versionReporting": false,
  "requireApproval": "never"
}
  • requireApproval は IAM ロールやポリシーなど権限に変化がある場合にデプロイ前確認を取るかの設定です。never 指定で常に許可(確認不要)としています。

その他

サンプル生成されていた以下のファイルは削除しておきます。

  • bin/cdk-sample.ts
  • lib/cdk-sample-stack.ts

いざデプロイ

cdk deploy

うまく通れば以下のようにデプロイメッセージが流れてくると思います。

✨  Synthesis time: 5.03s

WebSite:  start: Building fc8be9f040e2c1391a213ed4346994a031c1a4d60da357f111d84edc7f361819:012345678912-ap-northeast-1
WebSite:  success: Built fc8be9f040e2c1391a213ed4346994a031c1a4d60da357f111d84edc7f361819:012345678912-ap-northeast-1
WebSite:  start: Publishing fc8be9f040e2c1391a213ed4346994a031c1a4d60da357f111d84edc7f361819:012345678912-ap-northeast-1WebSite:  success: Published fc8be9f040e2c1391a213ed4346994a031c1a4d60da357f111d84edc7f361819:012345678912-ap-northeast-1
WebSite: deploying... [1/1]
WebSite: creating CloudFormation changeset...
WebSite |  0/11 | 23:34:05 | UPDATE_IN_PROGRESS   | AWS::CloudFormation::Stack                      | WebSite User Initiated
WebSite |  0/11 | 23:34:09 | CREATE_IN_PROGRESS   | AWS::S3::Bucket                                 | Content (Content12345678)
~省略~
WebSite |  9/11 | 23:38:45 | DELETE_COMPLETE      | AWS::Lambda::Permission                         | BucketAllowBucketNotificationsToWebSiteCacheInvalidation1234567890ABCDEF
WebSite |  9/11 | 23:38:46 | DELETE_SKIPPED       | AWS::S3::Bucket                                 | Bucket1234567890
WebSite | 10/11 | 23:38:46 | UPDATE_COMPLETE      | AWS::CloudFormation::Stack                      | WebSite
	
 ✅  WebSite

✨  Deployment time: 295.89s

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:012345678912:stack/WebSite/6b8ff9e0-4808-11ee-8eac-0af275d570a3

✨  Total time: 300.91s

コンテンツを作る

index.html

おなじみのハローワールドをでっちあげます。

<!doctype>
<html>
<head>
    <meta charset="utf-8">
    <title>ハローワールド</title>
</head>
<body>
    <div class="container">
        <h1>ハローワールド</h1>
        <p>こんにちは</p>
    </div>
</html>

アップロード

CDK デプロイで作られた S3 バケットを開いてアップロードします。

こんな感じになればOKです。

確認

CloudFront のディストリビューションに記載されている「ディストリビューションドメイン名」がデフォルトのURLになります。

いざアクセス!

ちゃんと表示できました!
また、index.html を修正・再アップロードしたら1分程度で反映を確認できると思います。

おわりに

イベントサイトなど、CMSを導入するまでもないシンプル&期間限定なサイトをサクッと準備したい時にすぐに使えるツールセットを想定しました。
お試しアップして再修正を繰り返す事が想定されると、キャッシュ活用との両立が AWS コンソールでポチポチだけでは微妙に出来ない。。。という悩み解決にこの CDK がお役に立てればと思います。

それではまた!

コラボスタイル Developers

Discussion