S3 で静的 Web サイトをホスティングする
概要
HTML/CSS/JS だけで完結する静的サイト(ランディングページ、製品ドキュメント、SPA のビルド成果物など)は、サーバーを立てずに Amazon S3 でホスティングできる。本記事では次の 2 パターンをハンズオン形式で構築する。
- S3 の「静的ウェブサイトホスティング」機能による最小構成(学習・検証向け)
- CloudFront + OAC(Origin Access Control)でバケットを非公開のまま配信する本番向け構成(HTTPS・独自ドメイン対応)
結論として、本番運用なら 2 を選ぶ。1 は HTTPS に対応せず、バケットを公開設定にする必要があるためだ。まず 1 で仕組みを理解し、そのうえで 2 へ移行する流れで進める。
環境
- AWS CLI v2(
aws --versionで確認) - 認証情報が設定済み(
aws configure済み、または SSO ログイン済み) - リージョンは
ap-northeast-1(東京)を例に使用 - バケット名は全世界で一意である必要があるため、本記事では
example-static-site-20260912のようなプレースホルダを使う
パターン1: S3 静的ウェブサイトホスティング(最小構成)
1. バケットを作成する
aws s3api create-bucket \ --bucket example-static-site-20260912 \ --region ap-northeast-1 \ --create-bucket-configuration LocationConstraint=ap-northeast-1
ap-northeast-1 など us-east-1 以外のリージョンでは --create-bucket-configuration LocationConstraint=... が必須。us-east-1 の場合は逆に付けるとエラーになる。
2. コンテンツを用意してアップロードする
mkdir -p site cat > site/index.html <<'HTML' <!doctype html> <html lang="ja"> <head><meta charset="utf-8"><title>Static on S3</title></head> <body><h1>Hello from S3</h1></body> </html> HTML cat > site/error.html <<'HTML' <!doctype html> <html lang="ja"> <head><meta charset="utf-8"><title>Not Found</title></head> <body><h1>404 Not Found</h1></body> </html> HTML aws s3 sync ./site s3://example-static-site-20260912
3. 静的ウェブサイトホスティングを有効化する
aws s3 website s3://example-static-site-20260912 \ --index-document index.html \ --error-document error.html
4. パブリックアクセスを許可する
新規バケットはデフォルトで全公開がブロックされている。パターン1 では直接アクセスを許可する必要があるため、ブロックを解除してからバケットポリシーを付与する。
aws s3api put-public-access-block \ --bucket example-static-site-20260912 \ --public-access-block-configuration \ BlockPublicAcls=false,IgnorePublicAcls=false,BlockPublicPolicy=false,RestrictPublicBuckets=false
cat > policy.json <<'JSON'
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::example-static-site-20260912/*"
}
]
}
JSON
aws s3api put-bucket-policy \
--bucket example-static-site-20260912 \
--policy file://policy.json
5. アクセスして確認する
ウェブサイトエンドポイントは次の形式になる(リージョンによってドメインが異なる)。
http://example-static-site-20260912.s3-website-ap-northeast-1.amazonaws.com/
curl -I http://example-static-site-20260912.s3-website-ap-northeast-1.amazonaws.com/ # HTTP/1.1 200 OK が返ればOK
このパターンの限界
- HTTPS 非対応。ウェブサイトエンドポイントは HTTP のみ
- バケットを公開する必要がある。セキュリティ観点では避けたい
- アクセスログ・キャッシュ・地理制限などの制御ができない
学習用途としては十分だが、本番では次のパターン2 に進む。
パターン2: CloudFront + OAC(バケット非公開のまま HTTPS 配信)
構成の考え方
- S3 バケットは非公開のままにする(パブリックアクセスブロックは全部 ON のまま)
- CloudFront ディストリビューションを前段に置き、OAC で CloudFront からのみ S3 を読めるようにする
- HTTPS は CloudFront のデフォルト証明書、独自ドメインなら ACM 証明書を使う
かつては OAI(Origin Access Identity)が使われていたが、現在は OAC が推奨。SSE-KMS 暗号化バケットや新しいリージョンにも対応する。新規構築では OAC を選ぶ。
1. バケットは非公開のまま作る
パターン1 の手順4(パブリックアクセス許可とバケットポリシー)は実施しない。パブリックアクセスブロックは ON のままにしておく。オリジンとして使うため、静的ウェブサイトホスティング(手順3)も不要で、通常の S3 バケットとして扱う。
2. OAC を作成する
aws cloudfront create-origin-access-control \ --origin-access-control-config \ Name=example-static-site-oac,SigningProtocol=sigv4,SigningBehavior=always,OriginAccessControlOriginType=s3
返ってくる Id(OAC の ID)を控えておく。
3. CloudFront ディストリビューションを作成する
マネジメントコンソールで作る場合は次を指定する。
- オリジンドメイン:
example-static-site-20260912.s3.ap-northeast-1.amazonaws.com(ウェブサイトエンドポイントではなく REST API エンドポイントを選ぶ) - オリジンアクセス: Origin access control settings を選び、手順2 の OAC を指定
- ビューアプロトコルポリシー: Redirect HTTP to HTTPS
- デフォルトルートオブジェクト:
index.html
CLI で作る場合は distribution-config.json を組み立てて aws cloudfront create-distribution --distribution-config file://distribution-config.json を実行する。項目が多いため、初回はコンソールで作って設定を把握するのがおすすめ。
4. バケットポリシーで CloudFront からのアクセスだけ許可する
CloudFront ディストリビューションを作ると、コンソールに「S3 バケットに貼るべきポリシー」が表示される。内容は次の形になる。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipal",
"Effect": "Allow",
"Principal": { "Service": "cloudfront.amazonaws.com" },
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::example-static-site-20260912/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::<ACCOUNT_ID>:distribution/<DISTRIBUTION_ID>"
}
}
}
]
}
このポリシーにより「特定の CloudFront ディストリビューション経由のリクエストだけ」が GetObject を許可される。バケット自体は非公開のままだ。
5. 独自ドメインと HTTPS(任意)
- ACM で証明書を発行する。CloudFront で使う証明書は
us-east-1(バージニア北部)で発行する必要がある(東京リージョンではない) - CloudFront ディストリビューションの Alternate domain name (CNAME) に独自ドメインを追加し、証明書を紐付ける
- Route 53 で A レコード(Alias)を CloudFront ディストリビューションに向ける
aws acm request-certificate \ --domain-name www.example.com \ --validation-method DNS \ --region us-east-1
実装例: デプロイと更新の自動化
# 1. ビルド成果物を同期(不要ファイルは削除、キャッシュ制御ヘッダを付与) aws s3 sync ./dist s3://example-static-site-20260912 \ --delete \ --cache-control "public, max-age=31536000, immutable" \ --exclude "index.html" # 2. index.html は短めのキャッシュにする(内容が変わるため) aws s3 cp ./dist/index.html s3://example-static-site-20260912/index.html \ --cache-control "public, max-age=60" # 3. CloudFront のキャッシュを無効化 aws cloudfront create-invalidation \ --distribution-id <DISTRIBUTION_ID> \ --paths "/*"
ハッシュ付きのアセット(app.abc123.js など)は長期キャッシュ、index.html は短期キャッシュにするのがセオリー。無効化リクエストは無料枠を超えると課金対象なので /* の乱発には注意する。
確認方法
- パターン1: ウェブサイトエンドポイントに
curl -Iして200 OKを確認。存在しないパスでerror.htmlが返るかも確認する - パターン2: CloudFront ドメイン(
dxxxxxxxxxxxxx.cloudfront.net)または独自ドメインに HTTPS でアクセスし、200が返ること、S3 の REST エンドポイントに直アクセスするとAccessDeniedになること(=バケットが非公開)を確認する
# 非公開が効いているかの確認(AccessDenied が返れば正しい) curl -I https://example-static-site-20260912.s3.ap-northeast-1.amazonaws.com/index.html
注意点
- バケット名は世界で一意。プレフィックス+日付などで一意性を確保する
- CloudFront 用の ACM 証明書は
us-east-1のみ。ここで詰まる人が多い - SPA のルーティングを使う場合、存在しないパスで 403/404 になる。CloudFront の「カスタムエラーレスポンス」で 403/404 を
/index.html・ステータス 200 に書き換える - パブリックアクセスブロックの扱い。パターン2 では絶対に解除しない。解除すると OAC で守っている意味がなくなる
- キャッシュ無効化のコスト。デプロイのたびに
/*を無効化すると無料枠を超える可能性がある。パスを絞るか、ファイル名にハッシュを含めて無効化自体を減らす
まとめ
- 静的サイトのホスティングは、まず S3 の静的ウェブサイトホスティングで仕組みを理解できる
- HTTPS・独自ドメイン・バケット非公開を満たすなら CloudFront + OAC が本番の定番構成
- ACM 証明書は
us-east-1、SPA はカスタムエラーレスポンス、キャッシュ無効化のコスト、この 3 点が主なハマりどころ - デプロイは
aws s3 sync --delete+キャッシュ制御ヘッダ + CloudFront invalidation をセットで自動化する
サーバーレスで低コスト、スケールを気にせず配信できるのが S3 ホスティングの強み。最小構成から始めて、要件に応じて CloudFront を前段に足していくのが実務的な進め方だ。
