CloudFrontを用いたS3静的サイトの構築


# TL;DR
- CloudFront + S3 で静的サイトを構築できる
- OAC；Origin Access Control を用いてS3のオブジェクトへのアクセスをCloudFront経由に限定する事ができる
- OAC を用いる場合は CloudFront のオリジンは **オブジェクトストレージの S3 エンドポイント(URL)** を指定する
- TTL関連はキャッシュポリシーで設定する

# はじめに
CloudFront + S3 で静的サイトを構築してみましたが、意外と苦労をしたので作業手順をまとめました。



# 作業手順

- S3静的サイトを構築
    - バケット選択 > プロパティ > 静的ウェブサイトホスティング
    - アクセス許可 > パブリックアクセスをすべてブロック のチェックを外す
    - バケットポリシーを変更して s3:GetObject をAllowする。
    参考：[公式（バケットポリシー）](https://docs.aws.amazon.com/AmazonS3/latest/userguide/HostingWebsiteOnS3Setup.html#step4-add-bucket-policy-make-content-public)

⇒ S3(website)単体で動作確認


<br>

- CloudFront の OACを作成
    -  ナビゲーションペイン：セキュリティ：オリジンアクセス > コントロール設定を作成
        - 「署名動作」は「署名リクエスト（推奨）」
        - 「オリジンタイプ」は「S3」
<br>

- Cloudfront のディストリビューションを作成
    - オリジンドメイン：**S3バケットのエンドポイント**を指定（後述）
    - オリジンアクセスでOACを選択。先ほど作成したOACを指定する
    - デフォルトルートオブジェクトに index.html を指定
    （実際のS3のオブジェクトに合わせる）
> s3エンドポイントの補足
> 〇 mybucket.s3.ap-northeast-1.amazonaws.com
> × mybucket.s3-website-ap-northeast-1.amazonaws.com


⇒ 他はデフォルトのままでもよい。（WAFだけ選択必須）  
⇒ ディストリビューションを作成  



<br>

- S3バケットの設定を修正
   - CloudFrontディストリビューション > ディストリビューションを選択 > 「オリジン」タブ >オリジンを選択 > 編集 > 「オリジンアクセス」セクションの「ポリシーをコピー」をクリック
   - S3へ移動 > バケット選択 > アクセス許可 > バケットポリシー > 編集 > 先ほどコピーしたポリシーを張り付ける
   - バケット選択 > プロパティ > 静的ウェブサイトホスティングを無効に
   - アクセス許可 > パブリックアクセスをすべてブロック のチェックを入れる

補足：CloudFrontのディストリビューションでバケットポリシーをコピー
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3092759/e5ef521b-63da-1063-8a1d-74968baf2eb5.png)


<details>
<summary>変更されたバケットポリシーの例</summary><div>

```
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PolicyForCloudFrontPrivateContent",
            "Effect": "Allow",
            "Principal": {
                "Service": "cloudfront.amazonaws.com"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::mybucket/*",
            "Condition": {
                "StringEquals": {
                    "AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/YOUR_DISTRIBUTION"
                }
            }
        }
    ]
}
```
</div></details>

<br>


⇒ CloudFrontのドメイン名（*********.cloudfront.net）で静的サイトにアクセスできる事を確認する。


# 追加設定例
## Route 53 のドメイン名を適用する
Route 53 を利用して作成した独自ドメインを静的サイト用のURLにする方法。

> 前提条件
> - Route53でドメイン、ホストゾーンが作成済である

- ACMでドメインの証明書を発行
補足：*.mydomainname.com といったドメイン全体の証明書

- CloudFrontディストリビューションの設定を修正
   - 「一般」タブ > 設定：編集 > 代替ドメイン名 (CNAME) の「項目を追加」
   （ここではsample.mydomainname.com とする）
   - Custom SSL certificate で先ほど作成した証明書を選択


- Route 53でレコードを作成
    -  Route 53でホストゾーンmydomainname.com を選択
    -  レコードを作成
        - レコード名：sample（mydomainname.com）
        - エイリアス：オンにする
        - エンドポイント：CloudFront
        - ディストリビューション：先ほど作成したディストリビューションのCNAME



## キャッシュポリシー
### 概要
Default TTL, Max TTL, Min TTL などを設定する為のもの。

> 操作（ポリシーの割り当て）：CloudFront > ディストリビューションを選択 > ビヘイビア > 編集 > キャッシュキーとオリジンリクエスト > Cache policy and origin request policy (recommended)  

> 操作（ポリシーの作成）：CloudFront > ナビゲーションペイン：ポリシー > キャッシュポリシーを作成  


以前は「Legacy cache settings」の追加設定で直接指定していたようですが、2024年5月現在非推奨との事。

### Amazonマネージドポリシー
以下のポリシーが用意されているようです（詳細は割愛）：
- Amplify
- CachingDisabled
- CachingOptimized
- CachingOptimizedForUncompressedObjects
- Elemental-MediaPackage

情報元（公式）：https://go.aws/3bJid3k

### カスタムポリシー
文字通り、自分で作成するキャッシュポリシー。
これを説明する公式資料は無い...かも？[ここ](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/working-with-policies.html)とか？  

自分は[CloudFormationの資料](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-cachepolicy-cachepolicyconfig.html)を見ましたが。

### Cloudfrontテンプレート例（キャッシュポリシー）
参考までに、Cloudfrontテンプレート例です。但し、正直あんまり理解できてない部分があります。（2024.05.22現在）
<details>
<summary>テンプレート例（Amazon 管理の CachingOptimized 同等）</summary><div>

```
  StaticSiteBucketCachePolicy:
    Type: AWS::CloudFront::CachePolicy
    Properties:
      CachePolicyConfig:
        Name: my-cache-policy
        DefaultTTL: 86400
        MaxTTL: 31536000
        MinTTL: 1
        ParametersInCacheKeyAndForwardedToOrigin:
          EnableAcceptEncodingGzip: true
          EnableAcceptEncodingBrotli: true
          CookiesConfig: 
            CookieBehavior: none
          HeadersConfig: 
            HeaderBehavior: none
          QueryStringsConfig: 
            QueryStringBehavior: none
            
  # 参照の仕方
  StaticSiteBucketDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        DefaultCacheBehavior:
          CachePolicyId: !Ref StaticSiteBucketCachePolicy
```
</div></details>

## ロギング
### 概要
操作：CloudFront > ナビゲーションペイン：ログ

CloudFrontのログは以下の２つの方法があるようです。
ここでは**標準ログ**について書きます。（リアルタイムログは試してません）

| タイプ   | 転送先 | 備考 |
|--|--|--|
| 標準ログ | S3　   | |
| リアルタイムログ　|	Kinesis Data Streams ||

### 特徴

- ログの書き方は「W3C 拡張形式」
- .gz アーカイブが作成される
- 初回アクセスの5分後にその間のアクセス分が書き込まれ、以後 5分毎に同様の動作をするっぽい
- アクセスがなかった場合は 5分経過してもログは作成されない

参考：https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/AccessLogs.html#AccessLogsOverview

### 使用条件

- ログ保存用の S3 バケット（以後 LogBucketとする）を用意
    - 静的サイトのバケットと別にする事が推奨
- LogBucket は「オブジェクト所有者」で ACL 有効にする必要がある
    - 2023 年 4 月以降の変更
    - 「オブジェクト所有者」はオブジェクトライター、希望するバケット所有者　のどちらでも良い
    操作：バケット選択 > アクセス許可 > オブジェクト所有者 > 編集
- LogBucket のバケットポリシーで対象のCloudFrontディストリビューションへ s3:PutObject を Allow する必要がある
- LogBucket のブロックパブリックアクセスの無効化は**必要ない**

### 補足
[公式資料](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/AccessLogs.html#AccessLogsBucketAndFileOwnership)の一部がまだ理解できていない（当然達成できていない）にも関わらずログは出力されているので少し不安はあります。

> 理解できていない部分：
> - FULL_CONTROL を付与
> - s3:GetBucketAcl と s3:PutBucketAcl を付与

### Cloudfrontテンプレート例（ロギング）

<details>
<summary>テンプレート例（抜粋）</summary><div>

```
  LoggingBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub ${ProjectName}-logging-bucket
      OwnershipControls:
        Rules:
          - ObjectOwnership: BucketOwnerPreferred
  LoggingBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref LoggingBucket
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: DistributionLoggingPermission
            Effect: Allow
            Principal:
              Service: cloudfront.amazonaws.com
            Action: s3:PutObject
            Resource:
              - !Sub ${LoggingBucket.Arn}/*
            Condition:
              ArnLike:
                AWS:SourceArn: arn:aws:cloudfront::${AWS::AccountId}:distribution/*
  StaticSiteBucketDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Logging:
          Bucket: !GetAtt LoggingBucket.RegionalDomainName
          Prefix: cloudfront/
```
</div></details>

# 資料
## 自分がつまづいたポイント

以下のミスマッチにより苦労しました。
- CloudfrontのオリジンにS3ウェブサイトエンドポイントを指定した場合、**OACによるアクセスコントロールはできない**
- Webで S3サイトのCloudfront経由制限の情報はOACを用いたものが多かった
- CloudfrontでS3静的サイトをオリジンとする場合、マネージメントコンソールは**S3ウェブサイトエンドポイント推奨してくる**（下図参照）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3092759/13e222b2-b864-078c-0e2b-b5302dd2e463.png)  


<br>

なお[公式](
https://repost.aws/ja/knowledge-center/cloudfront-serve-static-website)ではS3静的サイトとCloudfrontの連携には以下の方法が言及されています
> 1. オリジン：REST API エンドポイント＋OAC（またはOAI）
> 2. オリジン：S3ウェブサイト + パブリックアクセス許可
> 3. オリジン：S3ウェブサイト + Referer ヘッダーでアクセスを制限する
> 4. AWS CloudFormation を使用して、CloudFront を指すカスタムドメインをデプロイする

:::note
今回の方法は1. に近い方法です。  
3 は試してませんがなんとなく有力な気がします。
:::


2 はS3に直接アクセスできるので却下（自分の理解が正しければ）
4 はよく分からない　笑  

## S3静的サイトの特徴
- HTTP エンドポイントのみをサポート

## Cloudfront の特徴
独断と偏見で役に立つと感じた順に：
- HTTPS(TLS1.3) をサポート
- クライアントからのリクエストを受けたとき、レスポンスを返す時に関数を実行できる
    - 認証認可やレスポンスの生成に役立つ 
    - CloudFront Functions(js)とLambda@Edgeがある
- オリジンとなるAWSリソースへのアクセス回数を減らす
- WAFの利用が可能
　補足：レイヤー3,4 への DDos は AWS Sheildによって守られる
- コンテンツ読み込みの速度を上げる（通常のCDNに期待する機能）
- キャッシュヒット率の設定ができる（ビヘイビア）

参考（CloudFrontを使う理由）：https://zenn.dev/funteractiveinc/articles/d2a0d96f82bd2f


料金
- オリジン⇔CloudFront間は無料
- CloudFrontからインターネットへデータ転送　0.114USD/GB
（※東京リージョン、最初の10TB/月まで）
- CloudFront Functions 　100 万件の呼び出しあたり 0.10 USD 

公式（料金）：https://aws.amazon.com/jp/cloudfront/pricing/


## Cloudformationのテンプレート例
今回の S3 + Cloudfront を構築するテンプレート例です。

<details>
<summary>テンプレート例</summary><div>

```
AWSTemplateFormatVersion: "2010-09-09"
Description: my-static-site-with-cloudfront
Parameters:
  ProjectName:
    Type: String
    Default: my-static-site-with-cloudfront
Resources:
  StaticSiteBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub ${ProjectName}-static-site
  StaticSiteBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref StaticSiteBucket
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: PolicyForCloudFrontPrivateContent
            Effect: Allow
            Principal:
              Service: cloudfront.amazonaws.com
            Action: s3:GetObject
            Resource:
              - !Sub ${StaticSiteBucket.Arn}/*
            Condition:
              StringEquals:
                AWS:SourceArn: !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${StaticSiteBucketDistribution}
  StaticSiteBucketOAC:
    Type: AWS::CloudFront::OriginAccessControl
    Properties:
      OriginAccessControlConfig: 
        Name: !Sub ${ProjectName}-oac
        OriginAccessControlOriginType: s3
        SigningBehavior: always
        SigningProtocol: sigv4
  StaticSiteBucketDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Origins:
          - Id: S3Origin
            DomainName: !GetAtt StaticSiteBucket.RegionalDomainName
            OriginAccessControlId: !GetAtt StaticSiteBucketOAC.Id
            S3OriginConfig:
              OriginAccessIdentity: ""
        DefaultCacheBehavior:
          TargetOriginId: S3Origin
          AllowedMethods:
           - HEAD
           - GET
          ForwardedValues: 
            QueryString: true
          ViewerProtocolPolicy: redirect-to-https
        HttpVersion: http2 
        Enabled: true
        DefaultRootObject: index.html      
```
</div></details>


## 参考資料
基本概念から主要機能まで説明（わかりやすい）：

https://zenn.dev/issy/articles/zenn-cloudfront-overview

AWS WAFとAWS Shieldの違い：

https://www.wafcharm.com/jp/blog/aws-waf-vs-aws-shield-for-beginners/