[AWS CDK] 一撃でCloudFrontとS3を使ったWebサイトを構築してみた
パッと静的Webサイトを用意したい
こんにちは、のんピ(@non____97)です。
皆さんはパッと静的Webサイトを用意したいなと思ったことはありますか? 私はあります。
AWS上で静的Webサイトを構築するとなると思いつくのは「CloudFront + S3」の構成です。しかし、OACの設定をしたりアクセスログの設定をしたりと意外と設定する項目が多く大変です。そのため、検証目的で用意する際には手間がかかります。
毎回都度用意するのも面倒なので、AWS CDKを使って一撃で構築できるようにしてみました。(Route 53 Public Hosted Zoneを作成する場合は二撃です)
AWS CDKのコードの紹介
やっていること
AWS CDKのコードは以下リポジトリに保存しています。
やっていることは以下のとおりです。
- Route 53 Public Hosted Zoneの作成 または インポート (Optional)
- ACM証明書の作成 または インポート (Optional)
- S3サーバーアクセスログ用S3バケットの作成 (Optional)
- CloudFrontのアクセスログ用S3バケットの作成 (Optional)
- Webサイトのコンテンツを保存するS3バケットの作成
- ディレクトリインデックス用のCloudFront Functionsの作成 (Optional)
- ディレクトリインデックス用のLambda@Edgeの作成 (Optional)
- CloudFront ディストリビューションの作成
- CloudFront OACの設定
- Route 53 Public Hosted ZoneにCloudFront ディストリビューションのALIASレコードを作成 (Optional)
- Webサイトのコンテンツを保存するS3バケットにコンテンツをアップロード (Optional)
ディレクトリインデックス用の仕組みはCloudFront FunctionsとLambda@Edgeの2種類を用意しました。CloudFront Functionsの方がシンプルではあるのですが、CloudFront Functionsのリクエストに対するコストが気になる人もいるかと思います。キャッシュのTTLが長いなどキャッシュヒット率も高いであれば、オリジンリクエストで実行が可能なLambda@Edgeの方がコストが安くなるケースがあります。詳細は以下記事をご覧ください。
「そもそもディレクトリインデックスとは?」という方は以下AWS Blogをご覧ください。
S3バケットへのコンテンツのアップロードはaws_s3_deployment.BucketDeploymentを使用しています。指定したディレクトリパスをzipで固めてからアップロードされます。指定したディレクトリ内のファイルを少しでも追加すると、全てのファイルがアップロードされ直されます。あまりに大量のコンテンツがある場合はエラーになるかもしれないので注意してください。
AWS WAFの設定はデプロイ後にお好みでどうぞ。今だとマネジメントコンソールから簡単にAWS WAFのWebACLの作成とディストリビューションへのアタッチができます。
CloudFrontディストリビューションの設定
CloudFrontディストリビューション周りの設定は以下のとおりです。
this.distribution = new cdk.aws_cloudfront.Distribution(this, "Default", {
defaultRootObject: "index.html",
errorResponses: [
{
ttl: cdk.Duration.minutes(1),
httpStatus: 403,
responseHttpStatus: 403,
responsePagePath: "/error.html",
},
{
ttl: cdk.Duration.minutes(1),
httpStatus: 404,
responseHttpStatus: 404,
responsePagePath: "/error.html",
},
],
defaultBehavior: {
origin: new cdk.aws_cloudfront_origins.S3Origin(
props.websiteBucketConstruct.bucket
),
allowedMethods: cdk.aws_cloudfront.AllowedMethods.ALLOW_GET_HEAD,
cachedMethods: cdk.aws_cloudfront.CachedMethods.CACHE_GET_HEAD,
cachePolicy: cdk.aws_cloudfront.CachePolicy.CACHING_OPTIMIZED,
viewerProtocolPolicy:
cdk.aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
responseHeadersPolicy:
cdk.aws_cloudfront.ResponseHeadersPolicy.SECURITY_HEADERS,
functionAssociations: directoryIndexCF2
? [
{
function: directoryIndexCF2,
eventType: cdk.aws_cloudfront.FunctionEventType.VIEWER_REQUEST,
},
]
: undefined,
edgeLambdas: directoryIndexLambdaEdge
? [
{
functionVersion: directoryIndexLambdaEdge.currentVersion,
eventType:
cdk.aws_cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST,
},
]
: undefined,
},
httpVersion: cdk.aws_cloudfront.HttpVersion.HTTP2_AND_3,
priceClass: cdk.aws_cloudfront.PriceClass.PRICE_CLASS_ALL,
domainNames: props.domainName ? [props.domainName] : undefined,
certificate: props.domainName
? props.certificateConstruct?.certificate
: undefined,
logBucket: props.cloudFrontAccessLogBucketConstruct?.bucket,
logFilePrefix: props.logFilePrefix,
});
現在はキャッシュポリシーとレスポンスヘッダーポリシーのどちらもマネージドのものを使用しています。
キャッシュの設定を変更したい場合は、以下AWS公式ドキュメントを参考にしてキャッシュポリシーを作成ください。
また、レスポンスヘッダーからserver
を削除したい場合は、以下記事を参考に設定してください。
各種パラメーター
各種パラメーターの設定は以下ファイルで行います。
import * as cdk from "aws-cdk-lib";
import * as path from "path";
export interface LifecycleRule {
prefix?: string; // ライフサイクルルールを適用するオブジェクトのプレフィックス
expirationDays: number; // オブジェクトの保持期間
ruleNameSuffix?: string; // ライフサイクルルールに付与するサフィックス
abortIncompleteMultipartUploadAfter?: cdk.Duration; // 不完全なマルチパートアップロードを削除するまでの期間
}
export interface AccessLog {
enableAccessLog?: boolean; // アクセスログを有効化するか
logFilePrefix?: string; // 出力するアクセスログのプレフィックス
lifecycleRules?: LifecycleRule[]; // 適用するライフサイクルルール
}
export interface HostZoneProperty {
zoneName?: string; // Public Hosted Zoneのゾーン名
hostedZoneId?: string; // 既存のPublic Hosted ZoneのID
}
export interface CertificateProperty {
certificateArn?: string; // 既存のACM証明書のID
certificateDomainName?: string; // ACM証明書のドメイン名
}
export interface ContentsDeliveryProperty {
domainName?: string; // CloudFrontディストリビューションに設定するドメイン名
contentsPath?: string; // Webサイト用のS3バケットにPUTするコンテンツのローカルパス
enableDirectoryIndex?: "cf2" | "lambdaEdge" | false; // ディレクトリインデックス機能の実装方法
enableS3ListBucket?: boolean; // CloudFrontディストリビューションからS3バケットに対して s3:ListBucket を許可するか 存在しないオブジェクトにアクセスした場合に404で返したい場合は有効化
}
export interface WebsiteProperty {
hostedZone?: HostZoneProperty;
certificate?: CertificateProperty;
contentsDelivery?: ContentsDeliveryProperty;
allowDeleteBucketAndObjects?: boolean; // S3バケットの削除 および S3バケット内のオブジェクトを削除するか
s3ServerAccessLog?: AccessLog;
cloudFrontAccessLog?: AccessLog;
}
export interface WebsiteStackProperty {
env?: cdk.Environment;
props: WebsiteProperty;
}
export const websiteStackProperty: WebsiteStackProperty = {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION,
},
props: {
hostedZone: {
zoneName: "www.non-97.net",
},
certificate: {
certificateDomainName: "www.non-97.net",
},
contentsDelivery: {
domainName: "www.non-97.net",
contentsPath: path.join(__dirname, "../lib/src/contents"),
enableDirectoryIndex: "cf2",
enableS3ListBucket: true,
},
allowDeleteBucketAndObjects: true,
s3ServerAccessLog: {
enableAccessLog: true,
lifecycleRules: [{ expirationDays: 365 }],
},
cloudFrontAccessLog: {
enableAccessLog: true,
lifecycleRules: [{ expirationDays: 365 }],
},
},
};
デプロイ (Ver. CloudFront Functions)
デプロイ
実際にデプロイして試してみます。
設定は以下のようにしています。
export const websiteStackProperty: WebsiteStackProperty = {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION,
},
props: {
hostedZone: {
zoneName: "www.non-97.net",
},
certificate: {
certificateDomainName: "www.non-97.net",
},
contentsDelivery: {
domainName: "www.non-97.net",
contentsPath: path.join(__dirname, "../lib/src/contents"),
enableDirectoryIndex: "cf2",
enableS3ListBucket: true,
},
allowDeleteBucketAndObjects: true,
s3ServerAccessLog: {
enableAccessLog: true,
lifecycleRules: [{ expirationDays: 365 }],
},
cloudFrontAccessLog: {
enableAccessLog: true,
lifecycleRules: [{ expirationDays: 365 }],
},
},
};
デプロイが走ると、Route 53 Public Hosted Zoneが作成されます。NSレコードを上位のゾーン(私の場合はnon-97.net
)に登録しましょう。登録が完了してしばらくすると、ACMでの証明書の発行など後続の処理が完了します。
全体で8分ほどでデプロイが完了しました。
動作確認
実際にアクセスしてみましょう。
$ curl https://www.non-97.net -IL
HTTP/2 200
content-type: text/html
content-length: 12
date: Thu, 28 Mar 2024 07:07:52 GMT
last-modified: Thu, 28 Mar 2024 06:45:06 GMT
etag: "56aec8b7843df637b3fb2ec0b027e5b6"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Miss from cloudfront
via: 1.1 d1fa9409a9380374423ca786990631ba.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT57-P2
alt-svc: h3=":443"; ma=86400
x-amz-cf-id: oPZk8a0JVb2HagQLZyIjmmYNEql7pU7Dt6pd6fPbZ9BVZEOtBTSB7Q==
x-xss-protection: 1; mode=block
x-frame-options: SAMEORIGIN
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
strict-transport-security: max-age=31536000
$ curl https://www.non-97.net -IL
HTTP/2 200
content-type: text/html
content-length: 12
date: Thu, 28 Mar 2024 07:07:52 GMT
last-modified: Thu, 28 Mar 2024 06:45:06 GMT
etag: "56aec8b7843df637b3fb2ec0b027e5b6"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Hit from cloudfront
via: 1.1 180bb14f3969a5383ec3b52ad1ce5ad6.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT57-P2
alt-svc: h3=":443"; ma=86400
x-amz-cf-id: mOimDjaGRT4Jsml8gthlfSl2TCYuO1qYFSKR-XNbBdqizL_2HDytWQ==
age: 9
x-xss-protection: 1; mode=block
x-frame-options: SAMEORIGIN
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
strict-transport-security: max-age=31536000
$ curl https://www.non-97.net/index.html -IL
HTTP/2 200
content-type: text/html
content-length: 12
date: Thu, 28 Mar 2024 07:07:52 GMT
last-modified: Thu, 28 Mar 2024 06:45:06 GMT
etag: "56aec8b7843df637b3fb2ec0b027e5b6"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Hit from cloudfront
via: 1.1 6a4098eaf995c1e965d6434534971664.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT57-P2
alt-svc: h3=":443"; ma=86400
x-amz-cf-id: Aer0EiYUTtwi0u3cFT2qXVAUc18N22GIS0YornuBzbjvjmHGmU41cg==
age: 16
x-xss-protection: 1; mode=block
x-frame-options: SAMEORIGIN
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
strict-transport-security: max-age=31536000
CloudFrontのキャッシュが効いていそうです。HSTSのヘッダーも追加されていますね。
HTTPでアクセスした場合にHTTPSにリダイレクトするようにもしています。
$ curl http://www.non-97.net/test.html -IL
HTTP/1.1 301 Moved Permanently
Server: CloudFront
Date: Thu, 28 Mar 2024 07:10:14 GMT
Content-Type: text/html
Content-Length: 167
Connection: close
Location: https://www.non-97.net/test.html
X-Cache: Redirect from cloudfront
Via: 1.1 b93822242d240fe957b16155421ce866.cloudfront.net (CloudFront)
X-Amz-Cf-Pop: NRT57-P2
Alt-Svc: h3=":443"; ma=86400
X-Amz-Cf-Id: UuGHMYX2UCWQJswF-E7YwhQMWiHjHmGZBAeehOv3EsP7xdDklBuUWw==
X-XSS-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN
Referrer-Policy: strict-origin-when-cross-origin
X-Content-Type-Options: nosniff
HTTP/2 200
content-type: text/html
content-length: 11
date: Thu, 28 Mar 2024 07:10:15 GMT
last-modified: Thu, 28 Mar 2024 06:45:06 GMT
etag: "aa83444f341b53601faa67868d57abd6"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Miss from cloudfront
via: 1.1 aaaa38f6638fefc2221f20ff18eceef2.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT57-P2
alt-svc: h3=":443"; ma=86400
x-amz-cf-id: gBa6aohc9hBViovBo_1EDDcYEJH57q_TqALrOG0AuHn1D4pnSsb95A==
x-xss-protection: 1; mode=block
x-frame-options: SAMEORIGIN
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
strict-transport-security: max-age=31536000
ディレクトリインデックスが動作していることも確認しておきます。
$ curl https://www.non-97.net/dir -IL
HTTP/2 200
content-type: text/html
content-length: 16
date: Thu, 28 Mar 2024 07:10:48 GMT
last-modified: Thu, 28 Mar 2024 06:45:05 GMT
etag: "64f1d28c08f68bb7a25dd16598eed1d2"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Miss from cloudfront
via: 1.1 c9203ba15af2ae82294719bd8bb5fcce.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT57-P2
alt-svc: h3=":443"; ma=86400
x-amz-cf-id: j0a9BgZiU2SAop_jWqmixYDUQPqihmz8_GFL5JvZPqIN6utiyBOlyg==
x-xss-protection: 1; mode=block
x-frame-options: SAMEORIGIN
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
strict-transport-security: max-age=31536000
$ curl https://www.non-97.net/dir/ -IL
HTTP/2 200
content-type: text/html
content-length: 16
date: Thu, 28 Mar 2024 07:10:48 GMT
last-modified: Thu, 28 Mar 2024 06:45:05 GMT
etag: "64f1d28c08f68bb7a25dd16598eed1d2"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Hit from cloudfront
via: 1.1 3bc9fc5ff5b1c7e58ac789581c13d0e4.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT57-P2
alt-svc: h3=":443"; ma=86400
x-amz-cf-id: rhoeQsW7atFnVL4Az4xIxkkIFNg3AMj6piy725hSqCc7D6RXOm4ZfA==
age: 7
x-xss-protection: 1; mode=block
x-frame-options: SAMEORIGIN
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
strict-transport-security: max-age=31536000
問題なく動作していますね。
CloudFrontアクセスログ、S3サーバーアクセスログも問題なく出力されています。
CloudFrontアクセスログ
サーバーアクセスログ
大量アクセス時のCloudFront Functionsの挙動確認
試しにApache Benchで大量にアクセスしてみます。事前にキャッシュは削除しておきます。
$ ab -n 10000 -c 100 https://www.non-97.net/dir/
This is ApacheBench, Version 2.3 <$Revision: 1903618 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking www.non-97.net (be patient)
Completed 1000 requests
Completed 2000 requests
Completed 3000 requests
Completed 4000 requests
Completed 5000 requests
Completed 6000 requests
Completed 7000 requests
Completed 8000 requests
Completed 9000 requests
Completed 10000 requests
Finished 10000 requests
Server Software: AmazonS3
Server Hostname: www.non-97.net
Server Port: 443
SSL/TLS Protocol: TLSv1.2,ECDHE-RSA-AES128-GCM-SHA256,2048,128
Server Temp Key: ECDH X25519 253 bits
TLS Server Name: www.non-97.net
Document Path: /dir/
Document Length: 16 bytes
Concurrency Level: 100
Time taken for tests: 36.458 seconds
Complete requests: 10000
Failed requests: 0
Total transferred: 7267060 bytes
HTML transferred: 160000 bytes
Requests per second: 274.29 [#/sec] (mean)
Time per request: 364.582 [ms] (mean)
Time per request: 3.646 [ms] (mean, across all concurrent requests)
Transfer rate: 194.65 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 56 298 110.3 296 1132
Processing: 9 54 64.3 29 1008
Waiting: 8 32 33.6 23 1000
Total: 71 352 131.1 333 1225
Percentage of the requests served within a certain time (ms)
50% 333
66% 359
75% 383
80% 403
90% 487
95% 563
98% 693
99% 872
100% 1225 (longest request)
CloudFront Functionsのメトリクスを確認すると、1万回実行されていました。
また、Compute Utilizationも一時的に跳ねていました。
デプロイ (Ver. Lambda@Edge)
デプロイ
次にディレクトリインデックスの機能をLambda@Edgeで動かしてみましょう。
export const websiteStackProperty: WebsiteStackProperty = {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION,
},
props: {
hostedZone: {
zoneName: "www.non-97.net",
},
certificate: {
certificateDomainName: "www.non-97.net",
},
contentsDelivery: {
domainName: "www.non-97.net",
contentsPath: path.join(__dirname, "../lib/src/contents"),
enableDirectoryIndex: "lambdaEdge",
enableS3ListBucket: true,
},
allowDeleteBucketAndObjects: true,
s3ServerAccessLog: {
enableAccessLog: true,
lifecycleRules: [{ expirationDays: 365 }],
},
cloudFrontAccessLog: {
enableAccessLog: true,
lifecycleRules: [{ expirationDays: 365 }],
},
},
};
npx cdk diff
の結果は以下のとおりです。
$ npx cdk diff
Bundling asset WebsiteStack/ContentsDeliveryConstruct/DirectoryIndexLambdaEdge/Code/Stage...
cdk.out/bundling-temp-a88b3b0269470979bc0b8d1f8ed8a4f028c4b7f1fe42067392775b7b09619397/index.mjs 163b
⚡ Done in 6ms
Stack WebsiteStack
Hold on while we create a read-only change set to get a diff with accurate replacement information (use --no-change-set to use a less accurate but faster template-only diff)
IAM Statement Changes
┌───┬──────────────────────────────────────────────────────────┬────────┬────────────────┬──────────────────────────────────────────────────────────────┬───────────┐
│ │ Resource │ Effect │ Action │ Principal │ Condition │
├───┼──────────────────────────────────────────────────────────┼────────┼────────────────┼──────────────────────────────────────────────────────────────┼───────────┤
│ + │ ${ContentsDeliveryConstruct/LambdaEdgeExecutionRole.Arn} │ Allow │ sts:AssumeRole │ Service:edgelambda.amazonaws.com │ │
│ │ │ │ │ Service:lambda.amazonaws.com │ │
└───┴──────────────────────────────────────────────────────────┴────────┴────────────────┴──────────────────────────────────────────────────────────────┴───────────┘
IAM Policy Changes
┌───┬──────────────────────────────────────────────────────┬────────────────────────────────────────────────────────────────────────────────┐
│ │ Resource │ Managed Policy ARN │
├───┼──────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────┤
│ + │ ${ContentsDeliveryConstruct/LambdaEdgeExecutionRole} │ arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole │
└───┴──────────────────────────────────────────────────────┴────────────────────────────────────────────────────────────────────────────────┘
(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)
Resources
[-] AWS::CloudFront::Function ContentsDeliveryConstruct/DirectoryIndexCF2 ContentsDeliveryConstructDirectoryIndexCF2F9EC47B1 destroy
[+] AWS::IAM::Role ContentsDeliveryConstruct/LambdaEdgeExecutionRole ContentsDeliveryConstructLambdaEdgeExecutionRoleF34170E4
[+] AWS::Lambda::Function ContentsDeliveryConstruct/DirectoryIndexLambdaEdge ContentsDeliveryConstructDirectoryIndexLambdaEdgeDD789DA4
[+] AWS::Lambda::Version ContentsDeliveryConstruct/DirectoryIndexLambdaEdge/CurrentVersion ContentsDeliveryConstructDirectoryIndexLambdaEdgeCurrentVersion93C358E81c9380644828904ade854fd138db7b43
[~] AWS::CloudFront::Distribution ContentsDeliveryConstruct/Default ContentsDeliveryConstructE854BE87
└─ [~] DistributionConfig
└─ [~] .DefaultCacheBehavior:
├─ [-] Removed: .FunctionAssociations
└─ [+] Added: .LambdaFunctionAssociations
✨ Number of stacks with differences: 1
npx cdk deploy
でデプロイします。デプロイは3分ほどで完了しました。
動作確認
ディレクトリインデックスが効いているか確認します。
$ curl https://www.non-97.net/dir/ -IL
HTTP/2 200
content-type: text/html
content-length: 16
date: Thu, 28 Mar 2024 07:52:14 GMT
last-modified: Thu, 28 Mar 2024 06:45:05 GMT
etag: "64f1d28c08f68bb7a25dd16598eed1d2"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Miss from cloudfront
via: 1.1 49b964f897a5e1c9f9d0e182630ef7ca.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT57-P2
alt-svc: h3=":443"; ma=86400
x-amz-cf-id: JntntZB9DIsrt0RJQkND0oCneZhrAN3B36Wk-u_dUvWl0OeyTdN4tg==
x-xss-protection: 1; mode=block
x-frame-options: SAMEORIGIN
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
strict-transport-security: max-age=31536000
$ curl https://www.non-97.net/dir/ -IL
HTTP/2 200
content-type: text/html
content-length: 16
date: Thu, 28 Mar 2024 07:52:14 GMT
last-modified: Thu, 28 Mar 2024 06:45:05 GMT
etag: "64f1d28c08f68bb7a25dd16598eed1d2"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
x-cache: Hit from cloudfront
via: 1.1 b93822242d240fe957b16155421ce866.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT57-P2
alt-svc: h3=":443"; ma=86400
x-amz-cf-id: Ks4bMLN58XaZ_wp4pq2pp_7BZBBvno71I0k3u9iknrE67mfbmoJVVw==
age: 12
x-xss-protection: 1; mode=block
x-frame-options: SAMEORIGIN
referrer-policy: strict-origin-when-cross-origin
x-content-type-options: nosniff
strict-transport-security: max-age=31536000
効いていますね。
大量アクセス時のLambda@Edgeの挙動確認
Lambda@EdgeでもApache Benchで10,000回アクセスしてみます。
ab -n 10000 -c 100 https://www.non-97.net/dir/
This is ApacheBench, Version 2.3 <$Revision: 1903618 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking www.non-97.net (be patient)
Completed 1000 requests
Completed 2000 requests
Completed 3000 requests
Completed 4000 requests
Completed 5000 requests
Completed 6000 requests
Completed 7000 requests
Completed 8000 requests
Completed 9000 requests
Completed 10000 requests
Finished 10000 requests
Server Software: AmazonS3
Server Hostname: www.non-97.net
Server Port: 443
SSL/TLS Protocol: TLSv1.2,ECDHE-RSA-AES128-GCM-SHA256,2048,128
Server Temp Key: ECDH X25519 253 bits
TLS Server Name: www.non-97.net
Document Path: /dir/
Document Length: 16 bytes
Concurrency Level: 100
Time taken for tests: 35.434 seconds
Complete requests: 10000
Failed requests: 0
Total transferred: 7266592 bytes
HTML transferred: 160000 bytes
Requests per second: 282.22 [#/sec] (mean)
Time per request: 354.338 [ms] (mean)
Time per request: 3.543 [ms] (mean, across all concurrent requests)
Transfer rate: 200.27 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 47 289 85.3 293 646
Processing: 8 52 45.6 37 1017
Waiting: 7 34 28.3 25 1010
Total: 79 342 102.2 339 1119
Percentage of the requests served within a certain time (ms)
50% 339
66% 373
75% 398
80% 417
90% 464
95% 528
98% 594
99% 636
100% 1119 (longest request)
CloudWatchメトリクスでLambda@Edgeが呼びされた回数を見ると、該当の時間に東京リージョンで呼び出された回数は1回だけでした。
Lambda@EdgeがCloudWatch Logsに出力したログを見ても、1回しか実行されていないことが分かります。キャッシュヒット率が高い場合はLambda@Edgeの方がコストが安くなるかもしれませんね。
キャッシュヒットされていることも確認しましょう。分かりづらいですが、CloudFront Functionsの場合もLambda@Edgeの場合もどちらもほぼ100%キャッシュヒットしていることが分かります。キャッシュヒットしていないのは最初のアクセスのみです。
用途に応じてCloudFront Functionsか、Lambda@Edgeを使うか判断しましょう。使い分けは以下記事が参考になります。
なお、Lambda@Edgeの関数を削除する際には、以下のように失敗を繰り返します。(最終的には正常に削除される)
これは以下記事でも紹介されているとおり、Lambdaがレプリカを持っているためです。
補足 : L2 ConstructでS3をオリジンに設定すると、問答無用でOAIが作成される
デプロイ後に気になる方がいるかもしれないので補足です。
CloudFrontとS3バケット間のアクセス制御はOACで行っています。OACの説明は以下記事をご覧ください。
OACを使うため、OAIは使用しません。AWS CDK上でもOAIの設定はしていません。しかし、OAIは自動で作成されます。
このOAIはCloudFormationのコンソールからコンストラクトツリーを表示すると、CloudFrontディストリビューションDefault
の子コンストラクトであることが分かります。
「じゃあthis.distribution.node.tryRemoveChild("Origin1");
で、このコンストラクトを削除すれば良いじゃん!」と思われるかもしれません。しかし、これはできません。やろうとすると、以下のように怒られます。
WebsiteStack: creating CloudFormation changeset...
❌ WebsiteStack failed: Error [ValidationError]: Template error: instance of Fn::GetAtt references undefined resource ContentsDeliveryConstructOrigin1S3Origin9C471993
at Request.extractError (/<ディレクトリパス>/node_modules/aws-cdk/lib/index.js:382:46692)
at Request.callListeners (/<ディレクトリパス>/node_modules/aws-cdk/lib/index.js:382:91452)
at Request.emit (/<ディレクトリパス>/node_modules/aws-cdk/lib/index.js:382:90900)
at Request.emit (/<ディレクトリパス>/node_modules/aws-cdk/lib/index.js:382:199296)
at Request.transition (/<ディレクトリパス>/node_modules/aws-cdk/lib/index.js:382:192848)
at AcceptorStateMachine.runTo (/<ディレクトリパス>/node_modules/aws-cdk/lib/index.js:382:157720)
at /<ディレクトリパス>/node_modules/aws-cdk/lib/index.js:382:158050
at Request.<anonymous> (/<ディレクトリパス>/node_modules/aws-cdk/lib/index.js:382:193140)
at Request.<anonymous> (/<ディレクトリパス>/node_modules/aws-cdk/lib/index.js:382:199371)
at Request.callListeners (/<ディレクトリパス>/node_modules/aws-cdk/lib/index.js:382:91620) {
code: 'ValidationError',
time: 2024-03-27T11:24:11.495Z,
requestId: '6761120e-8a9b-4871-b0a0-7ee1cd9e5545',
statusCode: 400,
retryable: false,
retryDelay: 60.263640837447284
}
これは自動で作成されたOAIを参照して、オリジンのS3バケットのバケットポリシーを設定しているためです。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Principal": {
"AWS": "*"
},
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::websitestack-websitebucketconstruct04d5d64f-6jt680fmu9eu",
"arn:aws:s3:::websitestack-websitebucketconstruct04d5d64f-6jt680fmu9eu/*"
],
"Condition": {
"Bool": {
"aws:SecureTransport": "false"
}
}
},
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::<AWSアカウントID>:role/WebsiteStack-CustomS3AutoDeleteObjectsCustomResourc-nHlT8dYG9tuj"
},
"Action": [
"s3:DeleteObject*",
"s3:GetBucket*",
"s3:List*",
"s3:PutBucketPolicy"
],
"Resource": [
"arn:aws:s3:::websitestack-websitebucketconstruct04d5d64f-6jt680fmu9eu",
"arn:aws:s3:::websitestack-websitebucketconstruct04d5d64f-6jt680fmu9eu/*"
]
},
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity E243GPZLTPBOD"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::websitestack-websitebucketconstruct04d5d64f-6jt680fmu9eu/*"
},
{
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::websitestack-websitebucketconstruct04d5d64f-6jt680fmu9eu",
"arn:aws:s3:::websitestack-websitebucketconstruct04d5d64f-6jt680fmu9eu/*"
],
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::<AWSアカウントID>:distribution/E8PZWTYQZP0PV"
}
}
}
]
}
GitHubのソースコードaws-cdk/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.tsを確認すると、addToResourcePolicy()
でポリシーを追加していることが分かります。
/**
* An Origin specific to a S3 bucket (not configured for website hosting).
*
* Contains additional logic around bucket permissions and origin access identities.
*/
class S3BucketOrigin extends cloudfront.OriginBase {
private originAccessIdentity!: cloudfront.IOriginAccessIdentity;
constructor(private readonly bucket: s3.IBucket, { originAccessIdentity, ...props }: S3OriginProps) {
super(bucket.bucketRegionalDomainName, props);
if (originAccessIdentity) {
this.originAccessIdentity = originAccessIdentity;
}
}
public bind(scope: Construct, options: cloudfront.OriginBindOptions): cloudfront.OriginBindConfig {
if (!this.originAccessIdentity) {
// Using a bucket from another stack creates a cyclic reference with
// the bucket taking a dependency on the generated S3CanonicalUserId for the grant principal,
// and the distribution having a dependency on the bucket's domain name.
// Fix this by parenting the OAI in the bucket's stack when cross-stack usage is detected.
const bucketStack = cdk.Stack.of(this.bucket);
const bucketInDifferentStack = bucketStack !== cdk.Stack.of(scope);
const oaiScope = bucketInDifferentStack ? bucketStack : scope;
const oaiId = bucketInDifferentStack ? `${cdk.Names.uniqueId(scope)}S3Origin` : 'S3Origin';
this.originAccessIdentity = new cloudfront.OriginAccessIdentity(oaiScope, oaiId, {
comment: `Identity for ${options.originId}`,
});
}
// Used rather than `grantRead` because `grantRead` will grant overly-permissive policies.
// Only GetObject is needed to retrieve objects for the distribution.
// This also excludes KMS permissions; currently, OAI only supports SSE-S3 for buckets.
// Source: https://aws.amazon.com/blogs/networking-and-content-delivery/serving-sse-kms-encrypted-content-from-s3-using-cloudfront/
this.bucket.addToResourcePolicy(new iam.PolicyStatement({
resources: [this.bucket.arnForObjects('*')],
actions: ['s3:GetObject'],
principals: [this.originAccessIdentity.grantPrincipal],
}));
return super.bind(scope, options);
}
protected renderS3OriginConfig(): cloudfront.CfnDistribution.S3OriginConfigProperty | undefined {
return { originAccessIdentity: `origin-access-identity/cloudfront/${this.originAccessIdentity.originAccessIdentityId}` };
}
}
AWS CDKでは指定したポリシーステートメントをピンポイントで削除する処理はできない認識です。そのため、OAIは削除せずにそのままにしています。
コスト試算
この環境のコストを試算してみましょう。
条件は以下のとおりです。
- コンテンツ量 : 20GB
- ログ出力量 : 1TB
- アクセス数 : 10,000,000回/month
- 1アクセス当たりの平均転送量 : 1MB
- 転送量 : 20TB/month
- キャッシュヒット率 : 90%
- Public Hosted Zone : 1つ
- ディレクトリインデックス : CloudFront Functionsで実装
試算結果は以下のとおりです。
- S3
- データサイズ料金 :
26.10 USD
- GETリクエスト料金 :
0.37 USD
- データサイズ料金 :
- CloudFrontの
- インターネットへのデータ転送料金 :
2,078.72 USD
- オリジンへのデータ転送料金 :
122.88 USD
- HTTPSリクエスト料金 :
12.00 USD
- CloudFront Functions実行料金 :
1.00 USD
- インターネットへのデータ転送料金 :
- Route 53 Public Hosted Zone
- Hosted Zone料金 :
0.5 USD
- Hosted Zone料金 :
- トータル料金
2,240.57 USD/month (= 336,086円)
※ 1ドル150円
それなりです。
こんな時にありがたいのがクラスメソッドメンバーズのEC2・CDN割引プランです。
クラスメソッドメンバーズのEC2・CDN割引プランを使うと、なんとCloudFrontのアウトバウンド通信費が従来$0.114/GBのところ$0.0456/GBと、60%オフの料金で使用できたり、GETリクエストの料金が無料になるなどの割引があります。
抜粋 : AWS請求代行・請求書払い(リセール) | クラスメソッド株式会社
これにより、トータルの料金はから、2,240.57 USD/month (= 336,086円)
から870.96 USD/month (= 130,644円)
と毎月約20万円のコスト削減になります。やったぜ。
CloudFrontとS3を使った静的Webサイトが欲しい時に
AWS CDKを使って一撃でCloudFrontとS3を使ったWebサイトを構築してみました。
CloudFrontとS3を使った静的Webサイトが欲しい時にご利用ください。上述のコードをベースにキャッシュポリシーをカスタムしたり、ログ分析用のAthenaを追加したり、コンテンツデプロイのCI/CDパイプラインを作っても良いと思います。
この記事が誰かの助けになれば幸いです。
以上、AWS事業本部 コンサルティング部の のんピ(@non____97)でした!