デプロイ先の選択: S3 + CloudFront を選んだ理由

静的サイトのホスティング先として EC2 を使う方法もありますが、今回は S3 + CloudFront を選びました。理由は大きく 2 つです。

1つ目はコストです。EC2 はインスタンスが起動している間ずっと課金されますが、S3 は保存量とリクエスト数に応じた従量課金です。静的サイトであれば月数円〜数十円程度に収まります。

2つ目はサーバー管理が不要な点です。EC2 では OS のパッチ当てや Nginx の設定管理が必要になりますが、S3 + CloudFront ならそれが一切不要です。SSG を選んだ理由(ビルド済みの静的ファイルを配信するだけにする)と方針が一致しています。

ビルド: cargo run --bin ntea -- build

このブログは Rust で自作した SSG です。デプロイ前にまずローカルでビルドします。

cargo run --bin ntea -- build

public/ ディレクトリに HTML・CSS・JS・画像など静的ファイルがすべて出力されます。このディレクトリの中身を S3 にアップロードすれば公開完了、というシンプルな構造です。

ドメイン・証明書の取得

Route 53 でドメインを取得

Route 53 は AWS のマネージド DNS サービスです。ここで ntea.dev ドメインを取得しました。登録と同時にホストゾーンが作成され、ネームサーバー (NS) レコードが自動で設定されます。

ACM で SSL 証明書を取得 (us-east-1 必須)

ACM (AWS Certificate Manager) で SSL/TLS 証明書を取得します。ここで注意点があります。CloudFront に証明書を紐付けるには、証明書のリージョンが us-east-1 (バージニア北部) でなければなりません

これは CloudFront がグローバルサービスとして us-east-1 から証明書を参照する設計になっているためです。東京リージョン (ap-northeast-1) などで発行した証明書は CloudFront のディストリビューション設定画面に表示されないので注意が必要です。

ACM のコンソールでリージョンを us-east-1 に切り替えてから、ドメイン名 ntea.devwww.ntea.dev を追加し、DNS 検証を選択します。Route 53 を使っていると「Route 53 でレコードを作成」ボタンが表示されて CNAME レコードを自動追加できるので、検証が数分で完了します。

S3 バケットと CloudFront の設定

S3 バケット作成

バケット名を ntea.dev とし、パブリックアクセスはすべてブロックした状態で作成します。S3 の静的ウェブサイトホスティング機能は使いません。後述の OAC を使って CloudFront 経由のみアクセスを許可する構成にするためです。

ビルドした public/ の中身を S3 にアップロードします。

aws s3 sync public/ s3://ntea.dev/ --delete

CloudFront ディストリビューション作成

CloudFront は AWS の CDN (コンテンツデリバリーネットワーク) です。世界中のエッジロケーションにコンテンツをキャッシュし、ユーザーに近いサーバーから高速に配信します。

オリジンとして先ほどの S3 バケットを指定します。このとき「S3 バケットエンドポイント」ではなく「REST API エンドポイント (<bucket>.s3.<region>.amazonaws.com)」を使います。

OAC 設定: S3 への直アクセスを防ぐ

OAC (Origin Access Control) は CloudFront から S3 へのアクセスを制御する仕組みです。以前は OAI (Origin Access Identity) が使われていましたが、現在は OAC が推奨されています。

OAC を使うと、S3 バケットへのアクセスを「CloudFront からのリクエストのみ」に絞れます。S3 バケットポリシーに以下の設定を追加します。

{
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "cloudfront.amazonaws.com"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::ntea.dev/*",
      "Condition": {
        "StringEquals": {
          "AWS:SourceArn": "arn:aws:cloudfront::<account-id>:distribution/<distribution-id>"
        }
      }
    }
  ]
}

Route 53 エイリアスレコードで CloudFront に向ける

Route 53 のホストゾーンで A レコードを作成します。通常の A レコードは IP アドレスを指定しますが、ここでは エイリアスレコード を使います。エイリアスレコードは AWS リソース (CloudFront ディストリビューションなど) をドメイン名で直接参照できる Route 53 固有の機能です。

レコード タイプ 向き先
ntea.dev A (エイリアス) CloudFront ディストリビューション
www.ntea.dev CNAME ntea.dev

トラブル①: DNS プロパゲーションで繋がらなかった

設定完了後に https://ntea.dev にアクセスしても、しばらく繋がりませんでした。DNS プロパゲーション——DNS の変更が世界中のリゾルバに伝播するまでの時間——がかかっていたためです。

まず dig コマンドで現在の解決先を確認しました。

dig ntea.dev

ANSWER SECTION に CloudFront のドメイン (*.cloudfront.net) が返ってきていれば DNS 側の設定は正しいです。返ってこない場合は Route 53 の設定ミスを疑います。

dig で正しく返っているのにブラウザで繋がらない場合は、ブラウザや OS の DNS キャッシュが古い状態を保持しているケースがあります。dig +short ntea.dev @8.8.8.8 でパブリック DNS (Google) に直接問い合わせて確認するのが切り分けの基本です。今回は数分待ったところ解消しました。

トラブル②: サブページが 404 になる

トップページ (/) は表示されたものの、個別記事ページ (/programming/building-your-own-ssg/) にアクセスすると 404 エラーが返ってきました。

原因は CloudFront + S3 の index.html 補完問題です。S3 の静的ウェブサイトホスティング機能はパスの末尾に index.html を補完しますが、今回はこの機能を使わない構成にしています。そのため /programming/building-your-own-ssg/ というパスに対して、S3 はそのまま programming/building-your-own-ssg/ というキーを探しますが、実際には programming/building-your-own-ssg/index.html として保存されているため 404 になります。

解決策は CloudFront Functions を使うことです。CloudFront Functions はビューワーリクエスト時に軽量な JavaScript を実行できる機能で、パスを書き換えるのに適しています。

function handler(event) {
  var request = event.request;
  var uri = request.uri;

  // パスが / で終わる場合は index.html を補完
  if (uri.endsWith('/')) {
    request.uri += 'index.html';
  }
  // 拡張子がないパスにも index.html を補完
  else if (!uri.includes('.')) {
    request.uri += '/index.html';
  }

  return request;
}

この関数をビューワーリクエストイベントに関連付けることで、/programming/building-your-own-ssg/ へのリクエストが自動的に /programming/building-your-own-ssg/index.html に書き換えられ、S3 から正しくファイルが返ってくるようになりました。

まとめ

  • S3 + CloudFront はサーバー管理不要・低コストで静的サイトのホスティングに適しています。
  • ACM 証明書は us-east-1 で発行する必要があります (CloudFront の制約)。
  • OAC で S3 への直アクセスを遮断し、CloudFront 経由のみ許可する構成が推奨です。
  • DNS が繋がらないときは dig コマンドでプロパゲーションの状態を切り分けます。
  • サブページ 404 は CloudFront Functions で index.html を補完することで解決します。