/ #cdk #s3 

【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:GetObject3:ListBucketの権限を読んでみるとなんとなくこういうことだろうなーとわかります。

GetObject

ListBucket

つまるところGetObjectだけだと key があれば取ってこれるけど、バケットの中にどんなオブジェクトが入っているか見られる権利(ListBucket)はもってないので、Key に対するオブジェクトが取得できなかったときに、オブジェクトが存在してないのか存在してるけどアクセスがブロックされているのか確認できない。ということだとおもいます。

(ちなみに 403 forbidden が、指定された存在しないオブジェクトにアクセスする権限がなくて forbidden が発生するのか、指定されたオブジェクトが取得できなかったから list を実行しようとして forbidden が発生しているのか、内部的な話は分からないです)

権限まわりの余談の注意事項ですが、GetObjectはオブジェクトを取得する権利なのでリソースがオブジェクト単位です。しかしListBucketはバケットに対してオブジェクト一覧を見る権利なのでリソースがバケット単位です。

まぁそんなこんなでs3:ListBucketが必要できたということです! CDK さんデフォルトでこの権限いれておいてくれてもいいんじゃないかなーという気がします。

ということで今回はこんな感じでした!

実際の動作とか、どんなものが deploy されるか気になる方は、↓ に今回のプロジェクトのコードを置いておきますので動作確認してみてくださいー!

以上です! 最後まで読んでくださってありがとうございます!

今回のリポジトリはこちら

https://github.com/sisi100/cdk_cloud_front_s3_oai

Author

Sisii

インフラが好きなエンジニアぶってるなにか