AWSハンズオン - S3 + CloudFrontで静的サイトをCDN配信しよう【SAM版 / Windows対応】

AWS Basic
スポンサーリンク
スポンサーリンク

はじめに

「ウェブサイトを公開したいけど、サーバーは持ちたくない」「静的な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含む全リソースを一括削除(コンソール版の「月末まで待つ」問題がない)

Amazon検索[本 AWS 開発]

スポンサーリンク

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 v2aws --version2.x
AWS SAM CLIsam --version1.x
Gitgit --version-
VSCode-最新版推奨

Python は不要: 今回は Lambda 関数がないため Python のインストールは必要ありません。

aws sts get-caller-identity

使用するAWSサービス

サービス役割料金
S3HTMLファイルの保存5GBまで無料(12ヶ月)
CloudFrontCDN配信・HTTPS・エッジキャッシュ1TB/月・1,000万リクエスト/月まで無料
OACCloudFront → 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 WebsiteDistribution

template.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.htmlwebsite/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 build

Lambda関数がないため、ビルド処理は短時間で完了します。

Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

Step 6: sam deploy(デプロイ)

sam deploy
Deploy this changeset? [y/N]: y

y を入力して進めます。

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
----------------------------------------------------------------------

BucketNameCloudFrontDomain(と DistributionId)を控えておきます。


Step 7: HTMLファイルをアップロード

インフラのデプロイが完了したら、HTMLファイルをS3バケットにアップロードします。

set BUCKET=s3-cloudfront-hosting-stack-website-123456789012
aws s3 sync C:\my-aws\aws-learning-projects\s3-cloudfront-hosting\website\ ^
  s3://%BUCKET%/ ^
  --region ap-northeast-1
upload: 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.html

Step 8: 動作テスト

8-1. トップページの確認

ブラウザでCloudFrontドメインにアクセスします。

https://xxxxxxxxxxxx.cloudfront.net

index.html の内容が表示されれば成功です。

8-2. HTTP → HTTPS リダイレクトの確認

http://xxxxxxxxxxxx.cloudfront.net

HTTPでアクセスすると自動的に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=EXXXXXXXXXXXXXXXXX
aws 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-XXXXindex.html / error.html が存在することを確認
  • S3 バケットポリシー: S3 → バケット → 「アクセス許可」タブ → バケットポリシーにOACの条件が含まれていることを確認
  • CloudFront: CloudFront → ディストリビューション → ステータスが「有効」、「オリジン」タブでOACが設定されていることを確認


Step 10: リソースの削除

課金を止めるために、ハンズオン完了後は必ずリソースを削除してください。

削除時の注意点 2つ:

  1. S3バケットにオブジェクトが残っていると sam delete が失敗 → 先に空にする
  2. 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-1
Are 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]: y

CloudFrontの削除処理が走るため、完了まで15〜20分かかる場合があります。ターミナルはそのまま待ちます。

削除完了の確認

aws cloudformation describe-stacks --stack-name s3-cloudfront-hosting-stack --region ap-northeast-1
An error occurred (ValidationError): Stack with id s3-cloudfront-hosting-stack does not exist

コンソール版との大きな違い(SAMのメリット):
コンソールの新UIで作成したディストリビューションは「Pricing plan」に加入するため、プランキャンセル後に月末まで削除できない制約があります。
SAMで作成したディストリビューションにはPricing planの概念がなく、sam delete 1コマンドで即座に削除手続きが完了します。


トラブルシューティング

症状原因対処
sam deployBucketAlreadyExists エラー同名バケットが存在するsamconfig.tomlstack_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対応】
AWSコンソールだけでS3 + CloudFront静的サイトホスティングを構築する手順【SAM版との比較付き / 新UI対応】
はじめにこの記事は、SAM版ハンズオン記事の比較版です。SAM版ではコマンド数本でS3・OAC・CloudFront・バケットポリシーが一括デプロイできましたが、実際にコンソールで操作してみると思わぬハマりポイントがいくつかあります。特に ...

関連記事

Amazon検索[本 AWS 開発]

コメント