S3の静的WebサイトホスティングをCloudFrontでキャッシュしてみる

# はじめに
S3の静的Webサイトホスティングシリーズ第3弾。
S3の静的Webサイトホスティングで遊んでいると、色々な人から「CloudFrontオススメだよ！」と言われるので触ってみた。
CloudFrontって何？というよりは、基本の設定をTerraformで書きながら確認していく感じだ。

# 前提条件
- [AWS Black Belt Online SeminarのCloudFront編](https://www.youtube.com/watch?v=mmRKzzOvJJY)をさらっと見ていること。全部理解していなくても問題ないが、これを見て、マネコンでディストリビューションを軽く触ってみたことがないと難しいと思う
- 手前味噌ではあるが、[過去のS3の静的Webサイトホスティングの記事](https://qiita.com/neruneruo/items/d6e8776d3599f483fc55)を読んでおいてもらえると入りが早いと思う

# 事前準備(S3バケットの作成)
以下のような感じで、静的WebサイトホスティングしているS3と、そこにコンテンツを突っ込んでおこう。
なお、バケットに格納するオブジェクトのコンテンツはテキトーに準備して、Terraformのディレクトリとは別の場所に突っ込んである。パス構成等はお好みで変えてもらえれば。

```HCL
################################################################################
# S3 Bucket                                                                    #
################################################################################
resource "aws_s3_bucket" "my" {
  bucket = local.bucket_name
  acl    = "public-read"

  website {
    index_document = "index.html"
    error_document = "error.html"
  }
}

################################################################################
# S3 Object                                                                    #
################################################################################
resource "aws_s3_bucket_object" "contents" {
  bucket       = aws_s3_bucket.my.id
  source       = "../contents/test-contents.png"
  key          = "test-contents.png"
  acl          = "public-read"
  content_type = "image/png"
  etag         = filemd5("../contents/test-contents.png")
}

resource "aws_s3_bucket_object" "index" {
  bucket       = aws_s3_bucket.my.id
  source       = "../contents/index.html"
  key          = "index.html"
  acl          = "public-read"
  content_type = "text/html"
  etag         = filemd5("../contents/index.html")
}

resource "aws_s3_bucket_object" "error" {
  bucket       = aws_s3_bucket.my.id
  source       = "../contents/error.html"
  key          = "error.html"
  acl          = "public-read"
  content_type = "text/html"
  etag         = filemd5("../contents/error.html")
}
```

# ディストリビューションを作ってみる
CloudFrontのディストリビューションは、```aws_cloudfront_distribution```のリソースで作成する。
他のリソースと違い、必須項目が多くて面倒だったりする。
```origin_id```はテキトーなユニーク名を付けておく。

その他、```default_cache_behavior```では色々なキャッシュの設定ができるが、今回はとりあえず動けばいいや的な設定にしておく。```viewer_certificate```もデフォルト設定でとりあえず動くような感じに。```restrictions```も、何も書かなければ「制限なし」として扱ってくれればいいのに、必須項目なので書くだけ書いて無効化しておく。

```HCL
################################################################################
# CloudFront                                                                   #
################################################################################
resource "aws_cloudfront_distribution" "s3" {
  origin {
    domain_name = aws_s3_bucket.my.bucket_regional_domain_name
    origin_id   = local.origin_id
  }

  enabled             = true
  comment             = "CloudFrontお試し"
  default_root_object = "index.html"

  default_cache_behavior {
    allowed_methods  = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = local.origin_id

    forwarded_values {
      query_string = false

      cookies {
        forward = "none"
      }
    }

    viewer_protocol_policy = "allow-all"
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  viewer_certificate {
    cloudfront_default_certificate = true
  }
}
```

さて、これで```terraform apply```すると、CloudFrontのコンソールで以下のように表示されるようになる。

![キャプチャ1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/eb7768b5-0d3b-9949-d06f-42721c47fc0c.png)

ここで、払い出されたドメインに対してアクセスすると、

```
$ curl -i http://xxxxxxxxxxxxxx.cloudfront.net/
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 213
Connection: keep-alive
Date: Sat, 29 Aug 2020 12:13:34 GMT
Last-Modified: Sat, 29 Aug 2020 02:10:22 GMT
ETag: "1dde2af6633cb7a90946986afff7b0ca"
Accept-Ranges: bytes
Server: AmazonS3
X-Cache: Hit from cloudfront
Via: 1.1 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.cloudfront.net (CloudFront)
X-Amz-Cf-Pop: NRTXX-XX
X-Amz-Cf-Id: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Age: 2938

<html xmlns="http://www.w3.org/1999/xhtml" >
<head>
<title>Test Contents</title>
</head>
  <body>
    <p>test contents</p>
  </body>
</html>
```

やった！CloudFront経由でコンテンツを取得できたぞ！

# S3への直接アクセスを防ぐ
さて、せっかくCloudFrontを作ったにもかかわらず、S3に直接アクセスされてしまっては負荷軽減の意味がなくなってしまう。こういうときは、オリジンアクセスアイデンティティの機能を使えば良い。詳しくは、[CloudFrontの開発者ガイド](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html)を参照してもらいたい。

実際に設定する場合は以下のようにする。
```aws_cloudfront_origin_access_identity```のリソースを作り、```aws_cloudfront_distribution```から参照させる。また、バケットポリシーにも```Principals```で作ったOAIをAllowする。

```HCL:aws_cloudfront_distributionのoriginのブロック
  origin {
    domain_name = aws_s3_bucket.my.bucket_regional_domain_name
    origin_id   = local.origin_id

    s3_origin_config {
      origin_access_identity = aws_cloudfront_origin_access_identity.s3.cloudfront_access_identity_path
    }
  }
```

```HCL
resource "aws_cloudfront_origin_access_identity" "s3" {
  comment = "S3アクセスのOAIの設定"
}

resource "aws_s3_bucket_policy" "my" {
  bucket = aws_s3_bucket.my.id
  policy = data.aws_iam_policy_document.my_s3.json
}

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

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

これだけだと、S3に直接アクセスできてしまうので、ACLの設定を

```HCL
  acl    = "private"
```

に変更しておこう。

これで```terraform apply```すると、CloudFrontコンソールには

![キャプチャ2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/f67fa706-f03f-8d07-1acc-e73930bf3039.png)

のように反映され、curlでアクセスしてみると

```
$ curl -i http://xxxxxxxxxxxxxxxxxxxx-cloudfront-test-bucket.s3-website-ap-northeast-1.amazonaws.com | head -1
HTTP/1.1 403 Forbidden
```
```
$ curl -i http://xxxxxxxxxxxxxx.cloudfront.net/ | head -1
HTTP/1.1 200 OK
```

な感じで、オリジンでは拒否され、CloudFront側ではコンテンツを取得できるようになった！

# エラー応答をカスタマイズする
デフォルトの設定では、エラー応答が味気ない感じになってしまうので、```aws_cloudfront_distribution```のリソースに以下のブロックを追加することで設定を変更できる。

```HCL
  custom_error_response {
    error_code         = "404"
    response_code      = "404"
    response_page_path = "/error.html"
  }

  custom_error_response {
    error_code         = "403"
    response_code      = "403"
    response_page_path = "/error.html"
  }
```

```terraform apply```するとCloudFrontコンソール上では

![キャプチャ3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/fe907a4c-53b8-f31f-3fd5-572d49707573.png)

のように反映され、存在しないコンテンツにcurlすると

```
$ curl -i http://xxxxxxxxxxxxxx.cloudfront.net/hoge.html
HTTP/1.1 403 Forbidden
Content-Type: text/html
Content-Length: 52
Connection: keep-alive
Date: Sat, 29 Aug 2020 12:12:53 GMT
Last-Modified: Sat, 29 Aug 2020 02:10:22 GMT
ETag: "427e8f7bac6989431936b55912fd830c"
Accept-Ranges: bytes
Server: AmazonS3
X-Cache: Error from cloudfront
Via: 1.1 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.cloudfront.net (CloudFront)
X-Amz-Cf-Pop: NRTXX-XX
X-Amz-Cf-Id: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Age: 4311

<html>
  <body>
    error contents
  </body>
</html>
```

と、自前のエラーコンテンツを取得できるようになった！

# アクセスログを取る
CloudFrontはWebサーバのアクセスログも取得可能。```aws_cloudfront_distribution```のリソースに以下のブロックを追加することで設定できる。

```HCL
  logging_config {
    include_cookies = false
    bucket          = aws_s3_bucket.log.bucket_domain_name
    prefix          = "CFLog"
  }
```

もちろん、出力先のバケットも準備しておこう。

```HCL
resource "aws_s3_bucket" "log" {
  bucket = local.log_bucket_name
  acl    = "private"
}
```

```terraform apply```した後にアクセスしてからバケットを見に行くと、

![キャプチャ4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/4b1dd525-0f73-f8f8-0999-85aaa51de257.png)

gzipされたログが吐かれるようになった！
これはBlack Beltでも言われているけど、定型のログだから、S3 Selectで好きなように分析することもできるぞ！

# CloudFrontって結局速いの？
さて、最後に、せっかくだからCloudFrontのキャッシュがどれだけ強力か見てみよう。
と言っても、本気でAWSのインフラにトラフィックを叩き込むと破産するので、静的Webサイトホスティングと比較をしてみよう。

CloudFrontにLocustで放り込んでみると以下のようなグラフになった。

![number_of_users_1598704332.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/0d943612-6035-eae6-f4bb-af8026d4c1b3.png)

![total_requests_per_second_1598704332.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/2869b9d5-a5cc-fda6-3b45-f5057f1dc482.png)

![response_times_(ms)_1598704332.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/f5f77fc1-2999-f1b8-fc78-263d8ea4bc07.png)

速い！55KBくらいのコンテンツを使用しているが、95%タイルのレスポンスタイムが10ms程度で、しかも安定している。
[同条件での静的Webサイトホスティングの結果](https://qiita.com/neruneruo/items/d6e8776d3599f483fc55#%E9%9D%99%E7%9A%84web%E3%82%B5%E3%82%A4%E3%83%88%E3%83%9B%E3%82%B9%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0%E3%81%AE%E7%B4%A0%E6%80%A7%E8%83%BD)が40~50ms(これも充分すごいと思うけど)と考えるとかなり速いぞ！