WebサイトをCDKでサクッと立ち上げる
これは何をするもの?
TypeScript ベースの AWS CDK を利用して静的コンテンツな Web サイトを構築するための一式です。
以前に紹介した「S3で更新したファイルを自動ですぐに反映する」を組み込んでいるので、後からHTMLや画像を差し替えもすぐ反映できるようにしています。
準備
Node.js
からダウンロードしてインストールするか、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 のディストリビューションを作る構成です。
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 リソース部分
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 ディストリビューションに対してキャッシュクリアを要求します。
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 の設定を調整します。
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
は以下の内容で置き換えます。
{
"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 がお役に立てればと思います。
それではまた!
Discussion