【Python/CDK】CloudFront+S3で静的なウェブサイトを作る
明けましておめでとうございます!今年もよろしくおねがいします!
突然ですが AWS を使って静的なウェブサイトを作ろう!となったときに方法はいくつかあります。
S3 単体でもできますし、S3 の前に CloudFront を置いてもできますし、amplify とかとかを使ってできます。それ以外の方法もあります。
一体どれがいいのかなー?とかあると思いますが、今回 S3+CloudFront でやってみます。
ちなみにそれぞれのあくまで私のメリデメの話だと、
S3単体
:一番簡単にできる。キャッシュとかヘッダーとかの制御は効かない。カスタムドメインの場合はバケット名とドメインが一致する必要あり。
S3+CloudFront
:初期設定がちょっと手間。キャッシュが効く。ヘッダーも頑張れば使える。WAF や Shield が対応するのでセキュリティ的にも強い。
amplify
:初期設定がけっこう手間。リポジトリにプッシュするだけで build してデプロイしてくれるぜやったー!!ついでにCloud Front
にも使えるよ!
くらいの軽い気持ちでいます。
今回作ったもの
cdk-diaだとこんな感じ。
ただし、上記の lambda と BucketDeployment は CDK が良しなに作ったリソースなので、実際作ったものはバケットとディストリビューションです。
では 作っていきましょう 👻
動くコードを紹介してから、補足説をします。
まず動く全体のコード。
import aws_cdk as cdk
from aws_cdk import aws_cloudfront as cloudfront
from aws_cdk import aws_cloudfront_origins as origins
from aws_cdk import aws_iam as iam
from aws_cdk import aws_s3 as s3
from aws_cdk import aws_s3_deployment as deployment
app = cdk.App()
stack = cdk.Stack(app, "cdk-cloud-front-s3-oai-stack")
# バケットを作成する
bucket = s3.Bucket(stack, "Bucket", block_public_access=s3.BlockPublicAccess.BLOCK_ALL)
deployment.BucketDeployment(
stack,
"BucketDeployment",
sources=[deployment.Source.asset("html")],
destination_bucket=bucket,
)
# CloudFrontのディストリビューションを作成する
distribution = cloudfront.Distribution(
stack,
"Distribution",
default_behavior=cloudfront.BehaviorOptions(origin=origins.S3Origin(bucket)),
default_root_object="index.html",
error_responses=[
cloudfront.ErrorResponse(http_status=404, response_http_status=200, response_page_path="/errors/404.html"),
cloudfront.ErrorResponse(http_status=403, response_http_status=200, response_page_path="/errors/403.html"),
],
)
# バケットにpathが存在しない場合404で返答させる
oai: cloudfront.OriginAccessIdentity = distribution.node.find_child("Origin1").node.find_child("S3Origin")
policy = iam.PolicyStatement(
effect=iam.Effect.ALLOW,
principals=[iam.CanonicalUserPrincipal(oai.cloud_front_origin_access_identity_s3_canonical_user_id)],
resources=[bucket.bucket_arn],
actions=["s3:ListBucket"],
)
bucket.add_to_resource_policy(policy)
app.synth()
そしてhtml
ディレクトリの中にはこんな感じでファイルが入ってます。
html
├── errors
│ ├── 403.html
│ └── 404.html
└── index.html
あまり説明するところもないのですが cdk のコードを解説していきます。
bucket = s3.Bucket(...)
deployment.BucketDeployment(
...
sources=[deployment.Source.asset("html")],
destination_bucket=bucket,
)
ここではバケットを新規に作成して、ローカルのプロジェクトディレクトリ直下のディレクトリ名がhtml
のディレクトリのファイルの中身を S3 にデプロイします!という内容が書いてあります。
次に ↓ の部分。
distribution = cloudfront.Distribution(
stack,
"Distribution",
default_behavior=cloudfront.BehaviorOptions(origin=origins.S3Origin(bucket)),
default_root_object="index.html",
error_responses=[
cloudfront.ErrorResponse(http_status=404, response_http_status=200, response_page_path="/errors/404.html"),
cloudfront.ErrorResponse(http_status=403, response_http_status=200, response_page_path="/errors/403.html"),
],
)
ここはディストリビューションの設定をしています。
デフォルトビヘイビアにさきほど作ったバケットをオリジンに設定していれてあげます。こうすることでオリジンとビヘイビアの設定がまとめて行われます。
次にデフォルトルートオブジェクトを設定します。この設定によってhttps:/xxxxxxx/
のリクエストがきたときに自動でhttps:/xxxxxxx/index.html
を見に行ってくれます。
最後に error ですが、これはオリジンに設定した S3 からエラーが返されたときに、クライアント側に表示するステータスコードとかを入れ替える設定です。
例えば今の状態だと、S3 から 404 が返されたらクライアントには 200 の body は/errors/404.html
を表示するように設定されています。
つぎに ↓ の部分。
oai: cloudfront.OriginAccessIdentity = distribution.node.find_child("Origin1").node.find_child("S3Origin")
xxx.node.find_child
が出てくるととたんに胡散臭くなりますね。「このid名
いつ変わるかわかったもんじゃなくね?このコードをいつまで動くんだよ」と思ってしまいます。
でもDistribution
のコンストラクトからどうしてもOriginAccessIdentity
が取り出せなくて、こんな気持ち悪い書き方になりました。node
使わず取り出す方法しってる方いたらぜひ教えて下さい。
OAI を取り出す理由は次で解説します。
最後に ↓ の部分。
policy = iam.PolicyStatement(
effect=iam.Effect.ALLOW,
principals=[iam.CanonicalUserPrincipal(oai.cloud_front_origin_access_identity_s3_canonical_user_id)],
resources=[bucket.bucket_arn],
actions=["s3:ListBucket"],
)
bucket.add_to_resource_policy(policy)
ここでは S3 のバケットポリシーに権限を 1 つ追加しています。ちなみに CDK はデフォルトでs3:GetObject
の権限は付与してくれます。
これだけでコンテンツは十分配信できるのですが、なぜs3:ListBucket
の権限を追加するのでしょうか?
いったん話しをかえますが、仮に S3 に存在しない path がクライアントからリクエストされたとき、どのステータスコードを返すのが自然でしょうか?
一般的には 404 だと思います。しかしListBucket
の権限がないと S3 は CloudFront に対して 403 を返してきます。
なんでやねん?という話ですが、s3:GetObject
と3:ListBucket
の権限を読んでみるとなんとなくこういうことだろうなーとわかります。
つまるところGetObject
だけだと key があれば取ってこれるけど、バケットの中にどんなオブジェクトが入っているか見られる権利(ListBucket
)はもってないので、Key に対するオブジェクトが取得できなかったときに、オブジェクトが存在してないのか存在してるけどアクセスがブロックされているのか確認できない。ということだとおもいます。
(ちなみに 403 forbidden が、指定された存在しないオブジェクトにアクセスする権限がなくて forbidden が発生するのか、指定されたオブジェクトが取得できなかったから list を実行しようとして forbidden が発生しているのか、内部的な話は分からないです)
権限まわりの余談の注意事項ですが、GetObject
はオブジェクトを取得する権利なのでリソースがオブジェクト単位です。しかしListBucket
はバケットに対してオブジェクト一覧を見る権利なのでリソースがバケット単位です。
まぁそんなこんなでs3:ListBucket
が必要できたということです! CDK さんデフォルトでこの権限いれておいてくれてもいいんじゃないかなーという気がします。
ということで今回はこんな感じでした!
実際の動作とか、どんなものが deploy されるか気になる方は、↓ に今回のプロジェクトのコードを置いておきますので動作確認してみてくださいー!
以上です! 最後まで読んでくださってありがとうございます!