デプロイ先の選択: 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.dev と www.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を補完することで解決します。