はじめに
「ウェブサイトを公開したいけど、サーバーは持ちたくない」「静的なHTMLサイトを世界中から速く見せたい」という要件に、S3 + CloudFront の組み合わせはベストな選択肢のひとつです。
この記事では、AWS SAM(Serverless Application Model) を使って、S3バケットにHTMLを配置してCloudFront CDN経由で配信する静的サイトホスティング環境をゼロから構築するハンズオンを紹介します。
ブラウザ
↓ HTTPS(HTTPは自動リダイレクト)
CloudFront(CDN・エッジキャッシュ)
↓ OAC(AWS Signature v4 署名付きリクエスト)
S3 バケット(非公開・パブリックアクセスブロック)
└── index.html / error.htmlこのハンズオンで体験できること:
- S3バケットを非公開のまま CloudFront 経由でのみ公開する OAC(Origin Access Control) の仕組み
- HTTP → HTTPS の自動リダイレクト
- カスタム 404 エラーページ(
error.html) - CDN キャッシュの動作と、更新時の キャッシュ無効化(Invalidation) の操作
このハンズオンの特徴:
- Lambda 関数なしの SAM ハンズオン — SAMはCloudFormationの拡張のため、Lambdaなしの純粋なインフラ構成もそのままデプロイできます
- S3・OAC・CloudFront・バケットポリシーの4リソースを1ファイルで定義し、依存関係をSAMが自動解決
sam deleteでCloudFront含む全リソースを一括削除(コンソール版の「月末まで待つ」問題がない)
CDN と OAC の仕組みを理解する
キーワード解説
| 用語 | 意味 |
|---|---|
| CDN | コンテンツをユーザーに近いエッジサーバーからキャッシュ配信する仕組み。日本からアクセスするユーザーには日本のエッジから配信される |
| OAC(Origin Access Control) | CloudFront が S3 に安全にアクセスするための認証方式(現在の推奨。旧: OAI) |
| オリジン | CDN が配信元として参照するサーバー(今回は S3 バケット) |
| エッジロケーション | CloudFront の世界中にあるキャッシュサーバーの拠点(日本は東京・大阪等) |
| キャッシュ無効化(Invalidation) | エッジのキャッシュを削除してオリジンから最新版を取得させる操作 |
なぜ S3 を非公開にして CloudFront 経由で配信するのか
S3 バケットを直接公開することもできますが、現在の推奨構成は S3 を非公開 + CloudFront OAC 経由 です。
| 比較項目 | S3 直接公開 | CloudFront + OAC(推奨) |
|---|---|---|
| HTTPS | 独自ドメイン接続は複雑 | 標準でHTTPS |
| CDNキャッシュ | なし | 世界中のエッジでキャッシュ |
| S3への直接アクセス | 防げない | 防げる(CloudFront経由のみ) |
| WAF連携 | 不可 | 可能 |
前提条件
| ツール | 確認コマンド | 最低バージョン目安 |
|---|---|---|
| AWS CLI v2 | aws --version | 2.x |
| AWS SAM CLI | sam --version | 1.x |
| Git | git --version | - |
| VSCode | - | 最新版推奨 |
Python は不要: 今回は Lambda 関数がないため Python のインストールは必要ありません。
aws sts get-caller-identity使用するAWSサービス
| サービス | 役割 | 料金 |
|---|---|---|
| S3 | HTMLファイルの保存 | 5GBまで無料(12ヶ月) |
| CloudFront | CDN配信・HTTPS・エッジキャッシュ | 1TB/月・1,000万リクエスト/月まで無料 |
| OAC | CloudFront → S3 の安全なアクセス制御 | 無料(CloudFrontの機能) |
| S3(デプロイ用) | SAMのデプロイパッケージ置き場 | 少量なのでほぼ無料 |
Step 1: プロジェクトフォルダを開く
cd C:\my-aws\aws-learning-projects\s3-cloudfront-hostingフォルダ構造
s3-cloudfront-hosting/
├── template.yaml # SAMテンプレート(S3 + OAC + CloudFront + バケットポリシー)
├── samconfig.toml # デプロイ設定(gitignore対象・毎回手動作成が必要)
├── website/
│ ├── index.html # トップページ
│ └── error.html # カスタム 404 ページ
└── docs/
├── 1_console.md # AWSコンソール版手順
└── 2_sam.md # SAM版手順Step 2: SAMテンプレートの確認(template.yaml)
template.yaml が4つのAWSリソース全体の設計図です。それぞれのリソースに依存関係があるため、SAMが自動的に正しい順序で作成します。
リソースの依存関係
WebsiteBucket(S3バケット)
↓ バケット名を参照
CloudFrontOAC(Origin Access Control)
↓ バケット名 + OAC ID を参照
WebsiteDistribution(CloudFrontディストリビューション)
↓ バケット名 + ディストリビューションARN を参照
WebsiteBucketPolicy(S3バケットポリシー)template.yaml の主要部分
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: S3 + CloudFront static site hosting with OAC
Resources:
WebsiteBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub "${AWS::StackName}-website-${AWS::AccountId}" # アカウントIDでユニーク化
DeletionPolicy: Delete # sam delete 時にバケットも削除(空の場合のみ)
# OAC: CloudFrontがS3に署名付きリクエストを送るための設定
CloudFrontOAC:
Type: AWS::CloudFront::OriginAccessControl
Properties:
OriginAccessControlConfig:
Name: !Sub "${AWS::StackName}-oac"
OriginAccessControlOriginType: s3
SigningBehavior: always # 常に署名付きで送信
SigningProtocol: sigv4 # AWS Signature Version 4
# CloudFront ディストリビューション
WebsiteDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
DefaultRootObject: index.html # / → index.html
Enabled: true
Origins:
- DomainName: !GetAtt WebsiteBucket.RegionalDomainName # リージョナルドメインを使用
Id: S3Origin
S3OriginConfig:
OriginAccessIdentity: "" # OAC使用時は空文字列が必須
OriginAccessControlId: !GetAtt CloudFrontOAC.Id
DefaultCacheBehavior:
ViewerProtocolPolicy: redirect-to-https # HTTP → HTTPS リダイレクト
CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6 # Managed-CachingOptimized
AllowedMethods: [GET, HEAD]
TargetOriginId: S3Origin
CustomErrorResponses:
- ErrorCode: 403 # S3が非公開のため404ではなく403が返る場合がある
ResponseCode: 404
ResponsePagePath: /error.html
- ErrorCode: 404
ResponseCode: 404
ResponsePagePath: /error.html
# バケットポリシー: このCloudFrontディストリビューションからのみ GetObject を許可
WebsiteBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref WebsiteBucket
PolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: cloudfront.amazonaws.com
Action: s3:GetObject
Resource: !Sub "arn:aws:s3:::${WebsiteBucket}/*"
Condition:
StringEquals:
AWS:SourceArn: !Sub "arn:aws:cloudfront::${AWS::AccountId}:distribution/${WebsiteDistribution}"
Outputs:
BucketName:
Value: !Ref WebsiteBucket
CloudFrontDomain:
Value: !Sub "https://${WebsiteDistribution.DomainName}"
DistributionId:
Value: !Ref WebsiteDistributiontemplate.yaml のポイント解説
!GetAtt WebsiteBucket.RegionalDomainName(リージョナルドメインを使用)
OACを使う場合、S3オリジンのドメイン名はリージョナルドメイン(バケット名.s3.ap-northeast-1.amazonaws.com)を使用する必要があります。S3ウェブサイトエンドポイント(バケット名.s3-website-ap-northeast-1.amazonaws.com)は使えません。
S3OriginConfig.OriginAccessIdentity: ""(空文字列)
OACを使う場合は、旧方式(OAI)のフィールドに空文字列を設定することがCloudFormationの仕様上必要です。
ErrorCode: 403 → 404のマッピング
S3バケットが非公開のため、存在しないパスへのアクセスは 403 Forbidden が返ります。それを 404 Not Found にマッピングして error.html を表示します。
CachePolicyId: 658327ea-...(Managed-CachingOptimized)
AWSが提供するマネージドキャッシュポリシー「CachingOptimized」のIDです。S3の静的コンテンツ配信に最適化されたTTL設定が含まれています。
Step 3: website/ の確認
website/index.html と website/error.html はプロジェクトに既に含まれています。内容を確認しておきます。
website/index.html(トップページ)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>S3 + CloudFront ハンズオン</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
max-width: 640px;
margin: 60px auto;
padding: 20px;
color: #333;
}
h1 { color: #232f3e; }
.badge {
background: #ff9900;
color: white;
padding: 3px 10px;
border-radius: 4px;
font-size: 0.85em;
font-weight: bold;
}
.info {
background: #f4f6f8;
border-left: 4px solid #ff9900;
padding: 12px 16px;
margin: 20px 0;
border-radius: 0 4px 4px 0;
}
</style>
</head>
<body>
<h1>デプロイ成功!</h1>
<p>
<span class="badge">CloudFront CDN</span> 経由でこのページが配信されています。
</p>
<div class="info">
<strong>このページの配信構成:</strong><br>
S3 バケット(オリジン) → CloudFront(CDN)→ あなたのブラウザ
</div>
<p>S3 + CloudFront による静的サイトホスティングのハンズオンです。</p>
</body>
</html>website/error.html(カスタム404ページ)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>404 - ページが見つかりません</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
max-width: 640px;
margin: 60px auto;
padding: 20px;
color: #333;
text-align: center;
}
h1 { color: #cc0000; }
a { color: #ff9900; }
</style>
</head>
<body>
<h1>404 - ページが見つかりません</h1>
<p>お探しのページは存在しないか、移動・削除された可能性があります。</p>
<p><a href="/">トップページへ戻る</a></p>
</body>
</html>重要:
sam deployではHTMLファイルはS3にアップロードされません。
SAMデプロイはインフラ(S3バケット・CloudFront等)を作成するだけです。
HTMLファイルは Step 7 で別途aws s3 syncコマンドでアップロードします。
Step 4: samconfig.toml を作成する
samconfig.toml は .gitignore で管理外のため、毎回手動で作成する必要があります。
s3-cloudfront-hosting/samconfig.toml を新規作成して以下を貼り付けてください。
version = 0.1
[default.deploy.parameters]
stack_name = "s3-cloudfront-hosting-stack"
resolve_s3 = true
s3_prefix = "s3-cloudfront-hosting-stack"
region = "ap-northeast-1"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"
disable_rollback = true
image_repositories = []
[default.global.parameters]
region = "ap-northeast-1"Step 5: sam build(ビルド)
cd C:\my-aws\aws-learning-projects\s3-cloudfront-hosting
sam buildLambda関数がないため、ビルド処理は短時間で完了します。
Build Succeeded
Built Artifacts : .aws-sam/build
Built Template : .aws-sam/build/template.yamlStep 6: sam deploy(デプロイ)
sam deployDeploy this changeset? [y/N]: yy を入力して進めます。
CloudFront のデプロイには 5〜15分かかります。
CloudFront はグローバルに世界中のエッジロケーションに設定を反映するため、他のプロジェクトよりデプロイ完了に時間がかかります。ターミナルがフリーズしているように見えても正常に処理中ですので、そのまま待ちます。
デプロイ完了の確認
CloudFormation outputs from deployed stack
----------------------------------------------------------------------
Outputs
----------------------------------------------------------------------
Key BucketName
Value s3-cloudfront-hosting-stack-website-123456789012
Key CloudFrontDomain
Value https://xxxxxxxxxxxx.cloudfront.net
Key DistributionId
Value EXXXXXXXXXXXXXXXXX
----------------------------------------------------------------------BucketName と CloudFrontDomain(と DistributionId)を控えておきます。
Step 7: HTMLファイルをアップロード
インフラのデプロイが完了したら、HTMLファイルをS3バケットにアップロードします。
set BUCKET=s3-cloudfront-hosting-stack-website-123456789012aws s3 sync C:\my-aws\aws-learning-projects\s3-cloudfront-hosting\website\ ^
s3://%BUCKET%/ ^
--region ap-northeast-1upload: website\error.html to s3://s3-cloudfront-hosting-stack-website-123456789012/error.html
upload: website\index.html to s3://s3-cloudfront-hosting-stack-website-123456789012/index.htmlStep 8: 動作テスト
8-1. トップページの確認
ブラウザでCloudFrontドメインにアクセスします。
https://xxxxxxxxxxxx.cloudfront.netindex.html の内容が表示されれば成功です。
8-2. HTTP → HTTPS リダイレクトの確認
http://xxxxxxxxxxxx.cloudfront.netHTTPでアクセスすると自動的にHTTPSにリダイレクトされることを確認します。ブラウザのアドレスバーが https:// に変わるのを確認してください。
8-3. カスタム 404 ページの確認
https://xxxxxxxxxxxx.cloudfront.net/notfound存在しないパスにアクセスすると error.html の内容(「404 - ページが見つかりません」)が表示されることを確認します。
8-4. CDN キャッシュのテスト(任意)
CloudFront CDNの重要な概念「キャッシュ」と「無効化(Invalidation)」を体験します。
① index.html を更新して再アップロード
aws s3 cp C:\my-aws\aws-learning-projects\s3-cloudfront-hosting\website\index.html ^
s3://%BUCKET%/index.html ^
--region ap-northeast-1ブラウザで CloudFront URL にアクセスしても、古いキャッシュが表示される場合があります(TTLが切れるまで)。
② キャッシュを無効化(Invalidation)する
set DIST_ID=EXXXXXXXXXXXXXXXXXaws cloudfront create-invalidation ^
--distribution-id %DIST_ID% ^
--paths "/*" ^
--region ap-northeast-1レスポンス例:
{
"Invalidation": {
"Id": "IXXXXXXXXXXXXXXXXX",
"Status": "InProgress"
}
}③ 無効化の完了確認(1〜5分後)
aws cloudfront get-invalidation ^
--distribution-id %DIST_ID% ^
--id IXXXXXXXXXXXXXXXXX ^
--region ap-northeast-1"Status": "Completed" になったらブラウザを強制リロード(Ctrl+Shift+R)して最新版が表示されることを確認します。
キャッシュ無効化の料金:
毎月最初の 1,000 パスは無料。それ以降は $0.005/パス。/*は「1パス」としてカウントされるため、ハンズオン数回程度なら無料枠内です。
Step 9: AWSコンソールで確認(任意)
- S3: S3 →
s3-cloudfront-hosting-stack-website-XXXX→index.html/error.htmlが存在することを確認 - S3 バケットポリシー: S3 → バケット → 「アクセス許可」タブ → バケットポリシーにOACの条件が含まれていることを確認
- CloudFront: CloudFront → ディストリビューション → ステータスが「有効」、「オリジン」タブでOACが設定されていることを確認
Step 10: リソースの削除
課金を止めるために、ハンズオン完了後は必ずリソースを削除してください。
削除時の注意点 2つ:
- S3バケットにオブジェクトが残っていると
sam deleteが失敗 → 先に空にする- CloudFrontの削除には 15〜20分かかります(グローバルに反映を待つため)
① S3バケットを先に空にする
aws s3 rm s3://%BUCKET% --recursive --region ap-northeast-1② スタックを削除する
sam delete --stack-name s3-cloudfront-hosting-stack --region ap-northeast-1Are you sure you want to delete the stack s3-cloudfront-hosting-stack? [y/N]: y
Are you sure you want to delete the folder s3-cloudfront-hosting-stack in S3? [y/N]: yCloudFrontの削除処理が走るため、完了まで15〜20分かかる場合があります。ターミナルはそのまま待ちます。
削除完了の確認
aws cloudformation describe-stacks --stack-name s3-cloudfront-hosting-stack --region ap-northeast-1An error occurred (ValidationError): Stack with id s3-cloudfront-hosting-stack does not existコンソール版との大きな違い(SAMのメリット):
コンソールの新UIで作成したディストリビューションは「Pricing plan」に加入するため、プランキャンセル後に月末まで削除できない制約があります。
SAMで作成したディストリビューションにはPricing planの概念がなく、sam delete1コマンドで即座に削除手続きが完了します。
トラブルシューティング
| 症状 | 原因 | 対処 |
|---|---|---|
sam deploy が BucketAlreadyExists エラー | 同名バケットが存在する | samconfig.toml の stack_name を変更して別名にする |
| CloudFront URL で 403 が返る | HTMLのアップロードを忘れている | Step 7 の aws s3 sync を実行する |
| CloudFront URL で 403 が返る | CloudFrontがまだデプロイ中 | ステータスが「有効」になるまで5〜15分待つ |
| 更新したのに古い内容が表示される | CloudFrontキャッシュが残っている | Step 8-4 の create-invalidation を実行する |
sam delete が失敗する | S3バケットにオブジェクトが残っている | aws s3 rm s3://バケット名 --recursive で先に空にする |
sam delete が20分経っても終わらない | CloudFrontの削除処理中(正常) | そのまま待つ。CloudFrontの削除は時間がかかる |
まとめ
今回のハンズオンで実現したこと:
| 確認項目 | 内容 |
|---|---|
| HTTPS配信 | CloudFront経由でHTTPS配信 / HTTPは自動リダイレクト |
| S3非公開 | S3の直接URLは403 / CloudFront OAC経由のみアクセス可能 |
| カスタム404 | 存在しないパスで error.html が表示される |
| CDNキャッシュ | ファイル更新後にキャッシュ無効化で最新版を反映 |
| 削除 | S3を先に空にして sam delete で一括クリーンアップ |
SAMのメリットを実感できたポイント
- 4リソース(S3・OAC・CloudFront・バケットポリシー)の依存関係をSAMが自動解決
!GetAtt WebsiteDistribution.DomainNameでディストリビューションのドメインを自動参照(コンソール版では手動でコピー)sam deleteでPricing planなしに即座に削除手続き完了(コンソール版は月末まで待機が必要)
コンソール操作と比較してみる
SAMが裏で何をやっているか、同じ構成をAWSコンソールのみで構築する手順をまとめました。2026年のCloudFront新UI(6ステップウィザード形式)での手順と、削除時に新たに必要になった「Pricing planキャンセル」など、コンソールならではのつまずきポイントを解説しています。
- AWSコンソールだけでS3 + CloudFront静的サイトホスティングを構築する手順【SAM版との比較付き / 新UI対応】

コメント