話題の記事

CloudFrontとS3で作成する静的サイト構成の私的まとめ

2022.07.31

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

しばたです。

以前の記事でも触れた様にCloudFrontとS3を使って静的サイトを作る構成に対する理解にあいまいな部分があったので改めてまとめてみました。
特に目新しい話も無く知っている人には当たり前の内容かもしれませんが、まあ、自分自身の理解を整理するために記事にしていきます。

1. S3静的ウェブサイトを使うパターン

はじめの構成は「S3静的ウェブサイト」を使ったパターンです。

S3にはバケットの内容を静的ウェブサイトとしてホストできる静的ウェブサイトホスティングの機能があります。
この機能ではHTTPのみ利用可能なためHTTPSを使う場合はCloudFrontと組み合わせる必要があります。

S3静的ウェブサイトを使うにはバケット内のコンテンツを公開する必要があり、S3バケットはパブリックアクセス可能にする必要があります。
また、必ずHTTPのWEBサイトが公開されることになるためユーザーのアクセスをCloudFrontからのみに制限するのが一筋縄ではいきません。

設定例

簡単な例としてs3-site-test-20220730-1という名前のS3バケットを用意し設定してみます。
単純にHTTPサイトとして公開したい場合はバケット名の付け方に制約 *1がありますが、今回はCloudFrontと組み合わせますので名前はなんでも構いません。

S3バケットのプロパティから「静的ウェブサイトホスティング」を編集し、機能を有効にしてホスティングタイプを「静的ウェブサイトをホストする」にします。

その他の設定は環境に応じて異なりますが、今回はインデックスドキュメントをindex.htmlにしています。

これで静的ウェブサイトが公開されhttp://s3-site-test-20220730-1.s3-website-ap-northeast-1.amazonaws.comというエンドポイントが割り当てられます。

この状態ではまだコンテンツが公開できないので「ブロックパブリックアクセス」設定を無効にして、

さらに以下の様なバケットポリシーを設定してコンテンツをパブリックにアクセス可能にします。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::s3-site-test-20220730-1/*"
        }
    ]
}

これでバケット内のオブジェクトがパブリックに公開されます。
今回はバケットのルートに簡単なindex.htmlを一つ配置しておきます。

index.html

<!doctype html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>テストサイト</title>
</head>
<body>
Hello Classmethod!
</body>
</html>

これでhttp://s3-site-test-20220730-1.s3-website-ap-northeast-1.amazonaws.comにアクセスするとindex.htmlの内容が表示されます。

続けてCloudFrontと連携してやります。

ディストリビューションを一つ作成し、オリジン設定にhttp://s3-site-test-20220730-1.s3-website-ap-northeast-1.amazonaws.comを指定します。
S3バケット名ではないのでご注意ください。

S3静的ウェブサイトを使う場合オリジンタイプがCustom Originになります。

合わせてビヘイビアも設定してやります。
今回はシンプルにデフォルト (*)のみ設定しています。

これでCloudFrontからのアクセスもこの様に期待した表示となります。

本記事ではこれ以上解説しませんが、必要に応じて独自ドメインやサーバー証明書の設定をしてください。

補足 : カスタムヘッダーを使ったアクセス制御

先述の通りCloudFrontと連携してもHTTPサイトhttp://s3-site-test-20220730-1.s3-website-ap-northeast-1.amazonaws.comはアクセス可能です。

残念ながらユーザーのアクセスをCloudFrontからのみに強制する手段はありません。
代替策としてCloudFrontで独自のヘッダー(リファラ)を付けてやり、このリファラが設定されている場合のみS3静的ウェブサイトにアクセスできる様にすることであれば可能です。

まずはCloudFrontのオリジン設定から「カスタムヘッダーを追加」してやり、Referer = xxxxxxxxxxxxxxxxxxxx (推測しにくい任意の値)とリファラを追加します。

次にS3のバケットポリシーに以下の様なConditionを追加してやります。
IAMポリシーのグローバル条件キーに接続元のリファラを検査するaws:Refererがあるので、この値がCloudFrontで設定したものと一致するか判定し、一致した場合のみアクセスを許可してやるわけです。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::s3-site-test-20220730-1/*",
            "Condition": {
                "StringEquals": {
                    "aws:Referer": "xxxxxxxxxxxxxxxxxxxx"
                }
            }
        }
    ]
}

この設定をした後はブラウザから直接http://s3-site-test-20220730-1.s3-website-ap-northeast-1.amazonaws.comにアクセスしてもリファラが付いていないのでアクセスエラーとなります。

ただ、リファラは簡単に偽装できますのでこの設定が完璧ではないことは予めご了承ください。

2. S3 REST APIを使うパターン

次のパターンはAWS上決まった呼称が無い *2様なので便宜上「REST APIを使うパターン」と表記しています。

このパターンではCloudFrontのオリジンに通常のプライベートなS3バケットを指定します。
CloudFrontにOrigin Access Identity (OAI)と呼ばれる特別なユーザーを作成し、このOAIに対しs3:GetObjectを許可するバケットポリシーを設定することでプライベートなバケット内のコンテンツにアクセス可能にしています。

このためCloudFrontからS3のアクセスは通常のAWS REST API(HTTPS通信)となります。

設定例

こちらの例としてs3-site-test-20220730-2という名前の新しいS3バケットを用意して設定していきます。

コンテンツは前のパターンと同じindex.htmlを配置しておきます。

このバケットでは静的ウェブサイトホスティングは無効のままです。

ブロックパブリックアクセス機能も有効にしています。

ここで一旦CloudFrontに移り、「オリジンアクセスアイデンティティ」設定から新しい「オリジンアクセスアイデンティティを作成」します。

作成ダイアログが表示されたら任意の名前を入力し「作成」します。

作成されたOAIにはランダムなID(下図ではEU82XXXXXXXXXX)が付きますのでこのIDを控えておきます。

S3に戻り以下の様にPrincipalをOAIにしてs3:GetObjectを許可するバケットポリシーを設定します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PolicyForCloudFrontPrivateContent",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity EU82XXXXXXXXXX"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::s3-site-test-20220730-2/*"
        }
    ]
}

これでCloudFrontからS3に対してGetObject出来る様になるわけです。

S3バケットの設定はこれで完了です。
あとはCloudFrontのオリジンにS3バケットs3-site-test-20220730-2を追加します。

その際に「S3バケットアクセス」設定を

  • OAIを使用する
  • オリジンアクセスアイデンティティを前節で作ったものを指定
  • バケットポリシーの更新は手動 (今回は事前にポリシー設定済みのため)

にしてやります。
作成後のオリジンタイプがS3になっていればOKです。

併せてビヘイビアも変えておきます。

あとは環境に応じてよしなにCloudFrontの設定を変更してやればOKです。
今回は一般設定の「デフォルトルートオブジェクト」をindex.htmlにしています。

こちらは以前の記事で紹介した通りの挙動の違いがあるためです。

最後にブラウザからアクセスしてやればS3静的ウェブサイトを使うパターンと同様にコンテンツが表示されます。

パターンの使い分け

2つのパターンについて説明しましたが、それぞれどう使い分けるべきかについては未だ明確な答えを出せていません。

HTTPSが普及していなかった昔であれば「まずは静的ウェブサイトホスティングを使いHTTPでサイト公開、必要に応じてCloudFrontを使いHTTPS化する」といった使い方がメジャーだったと思います。
2022年現在であれば逆にHTTPでサイトを公開する必要性が薄いので「REST APIを使いHTTPSサイトのみ公開」の方が良い気がしています。

また、静的ウェブサイトホスティングで持っている「インデックスドキュメント機能」や「リダイレクト機能」を使いたい場合は今でも静的ウェブサイトホスティングを選んだ方が楽でしょう。
とはいえREST APIを使うパターンでもLambda@EdgeやCloudFront Functionsを組み合わせることでこれらの機能を代替することが可能です。

これ以外の両者の細かい差異が以下に記載されていますので、こちらの内容を踏まえて選択しても良いでしょう。

他にはセキュリティ要件により経路暗号化が求められる場合はREST APIを使うパターンを選ぶしかありません。
(とはいえ静的なサイト構築で経路暗号化が求められるケースは少ないとは思いますが...)

個人的には「まずはREST APIを使うパターンで設計し、インデックスドキュメントなどの機能が必要で構成をシンプルにまとめたい場合にのみ静的ウェブサイトホスティングを使う」のが良いのかな、と、いまのことろ考えています。
(まだ完全に考えがまとまってないので将来的には考えを改めるかもしれません。)

最後に

以上となります。

個人的な理解の整理のためにダラダラ書き連ねてきましたが、本記事をご覧の皆さんにもなにか役立つことがあれば幸いです。

脚注

  1. 例えば http://www.example.com/ でサイト公開したい場合は www.example.com というバケット名する必要がある
  2. ドキュメント上では「ウェブサイトエンドポイント」と「REST APIエンドポイント」とエンドポイントの種別で二者が区別されています