CloudFrontとS3で公開期間を指定したWebサイトを構築する 

## はじめに

AWSで静的Webサイトを構築する際は、CloudFront + S3の構成が一般的かと思います。

S3のバケットポリシーとCloudFrontのカスタムエラーレスポンスを組み合わせて、
特定の期間内のみアクセスを受け付け、
期間外はエラーページを表示するサイトを構築しました。

AWS環境はTerraformで作成します。

参考：

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/GeneratingCustomErrorResponses.html

https://cloud-aws-gcp.hateblo.jp/entry/2019/11/20/024813

## 実行環境

- macOS Ventura 13.2.1
- Terraform v1.5.2
- aws-cli 2.9.12

## 環境構築

構成はCloudFrontディストリビューションが1つに、S3バケットが2つです。
1つのS3バケット（公開用バケット）に公開用ページを格納し、
もう１つ（エラー用バケット）にエラーページを格納します。

公開用ページは公開用バケットの`app_page/index.html`に配置し、
エラーページはエラー用バケットの`error/error.html`に配置します。

CloudFrontのオリジンには両方を指定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1649149/af5f60df-52ab-7df2-0290-080842729cef.png)

CloudFrontをマルチオリジンにする際の注意点はこちらを参考に

https://qiita.com/uramotot/items/592b43cf564060e8a365

### S3

まずはS3バケットの作成です。2つ用意します。

```terraform
# 公開用
resource "aws_s3_bucket" "s3_bucket" {
  bucket = "open-bucket"
}

# エラー用
resource "aws_s3_bucket" "s3_standby_bucket" {
  bucket = "error-bucket"
}
```

次にS3のバケットポリシーを作成します。
ここで公開用バケットのポリシーにアクセスを許可する日時を指定することで、
期間外にアクセスした場合は`403 forbidden`が返るようになります。

```terraform
# 公開用
resource "aws_s3_bucket_policy" "s3_bucket" {
  bucket = aws_s3_bucket.s3_bucket.id
  policy = data.aws_iam_policy_document.s3_policy.json
}

data "aws_iam_policy_document" "s3_policy" {
  statement {
    actions   = ["s3:GetObject"]
    resources = ["${aws_s3_bucket.s3_bucket.arn}/*"]

    principals {
      type        = "AWS"
      identifiers = [aws_cloudfront_origin_access_identity.origin_access_identity.iam_arn]
    }

    # アクセスを許可する期間を指定
    condition {
      test = "DateGreaterThan"
      variable = "aws:CurrentTime"
      # 標準時なので公開開始の時間-9時間にする
      values = ["2023-7-11T15:00:00Z"] # 公開開始 7/12 0:00
    }

    condition {
      test = "DateLessThan"
      variable = "aws:CurrentTime"
      values = ["2023-7-12T06:00:00Z"] # 公開終了 7/12 15:00
    }
  }
}

# エラー用
resource "aws_s3_bucket_policy" "s3_standby_bucket" {
  bucket = aws_s3_bucket.s3_standby_bucket.id
  policy = data.aws_iam_policy_document.s3_standby_policy.json
}

data "aws_iam_policy_document" "s3_standby_policy" {
  statement {
    actions   = ["s3:GetObject"]
    resources = ["${aws_s3_bucket.s3_standby_bucket.arn}/*"]

    principals {
      type        = "AWS"
      identifiers = [aws_cloudfront_origin_access_identity.origin_access_identity.iam_arn]
    }
  }
}
```

### CloudFront

OriginAccessIdentity

```terraform
resource "aws_cloudfront_origin_access_identity" "origin_access_identity" {
  comment = "origin access identity for s3"
}
```

ディストリビューション

まずは公開用の設定

公開ファイルのパスが`app_page/index.html`なので、
`default_root_object`を`index.html`に、
`origin`ブロックの`origin_path`を`/app_page`にします。

その他の設定はご自身の環境に合わせてください。

```terraform
resource "aws_cloudfront_distribution" "cf_dist" {
    comment = "cf-comment"
    default_root_object = "index.html"
    enabled             = "true"
    http_version        = "http2"
    is_ipv6_enabled     = "true"
    retain_on_delete    = "false"

    default_cache_behavior {
        allowed_methods = ["GET", "HEAD"]
        cached_methods  = ["GET", "HEAD"]
        compress        = "false"
        default_ttl     = "86400"
        
        forwarded_values {
          cookies {
            forward = "none"
          }
        
          query_string = "false"
        }
        
        max_ttl                = "31536000"
        min_ttl                = "0"
        smooth_streaming       = "false"
        target_origin_id       = aws_s3_bucket.s3_bucket.id
        viewer_protocol_policy = "redirect-to-https"
    }

    origin {
        domain_name = aws_s3_bucket.s3_bucket.bucket_regional_domain_name
        origin_id   = aws_s3_bucket.s3_bucket.id
        origin_path = "/app_page"
    
        s3_origin_config {
          origin_access_identity = aws_cloudfront_origin_access_identity.origin_access_identity.cloudfront_access_identity_path
        }
      }
    }

    restrictions {
        geo_restriction {
            restriction_type = "none"
        }
    }

    viewer_certificate {
        cloudfront_default_certificate = true
    }

    # 以下にエラーページ表示用の設定を追加
```

次にエラーページ表示用の設定

`custom_error_response`ブロックを追加し、
エラーコード403をキャッチしたら`/error/error.html`を返すようにします。
また、`ordered_cache_behavior`ブロックを追加します。
`path_pattern`に`/error/*`を設定し、
`/error`へのアクセスはエラー用バケットを見に行くようにします。


```terraform
resource "aws_cloudfront_distribution" "cf_dist" {
    # 省略
    custom_error_response {
        error_caching_min_ttl = "0"
        error_code            = "403"
        response_code         = "200"
        response_page_path    = "/error/error.html"
    }

    origin {
        domain_name = aws_s3_bucket.s3_standby_bucket.bucket_regional_domain_name
        origin_id   = aws_s3_bucket.s3_standby_bucket.id
    
        s3_origin_config {
          origin_access_identity = aws_cloudfront_origin_access_identity.origin_access_identity.cloudfront_access_identity_path
        }
    }

    ordered_cache_behavior {
        allowed_methods = [
          "GET",
          "HEAD",
        ]
        cached_methods = [
          "GET",
          "HEAD",
        ]
        compress               = false
        default_ttl            = 86400
        max_ttl                = 86400
        min_ttl                = 0
        path_pattern           = "/error/*"
        smooth_streaming       = false
        target_origin_id       = aws_s3_bucket.s3_standby_bucket.id
        trusted_signers        = []
        viewer_protocol_policy = "https-only"
    
        forwarded_values {
          cookies {
            forward = "none"
          }
          query_string = "false"
        }
    }
}

```

AWSの設定は以上です。

### S3にコンテンツを配置

公開用バケットの`/app_page/index.html`と
エラーバケットの`error/error.html`にそれぞれコンテンツを配置しましょう。

## 動作確認

CloudFrontのURLにアクセスすると、公開期間内には公開用バケットのコンテンツが表示され、
期間外はエラーページが表示されるはずです。

## つまづきポイント

何回か失敗して試していると、CloudFrontのキャッシュが残っていて、
設定はあっているのに正常に表示されないことがあったので、
設定変更する度にキャッシュを削除することをおすすめします。

なお、公開開始前→開始後でキャッシュが残ってエラーページがずっと表示されてしまう、
といったことは心配しなくてよさそうです。
正常なレスポンスとエラーレスポンスのキャッシュは分けられているようなので、
デフォルトキャッシュのTTLに長い時間を設定していても問題ありません。
公開停止前→停止後の場合も同様です。

## おわりに

イベントの期間だけアクセスできるようにしたい、
深夜の公開作業を自動で行いたい、など、
期間指定で公開したい場合って結構あるのではと思います。
参考になれば幸いです。