AWS CloudFrontのアクセスを一律リダイレクトするように設定できるまでの話

# 背景

この間、運営している２つのメディアサイトを統合させたくて、AWSのCloudFrontで配信している古いサイトから外部のサイトに移行する必要があった。

- `old.example.com`はCloudFrontを使ってS3に入っているコンテンツを配信しているサイト
- `new.example.com`はAWS外でホスティングしているサイト

古いサイトのページはアクセス数が少ないため、内容は移行せず`old.example.com/*`に来たアクセスを一律`new.example.com/home`にリダイレクトするのが今回の要件。

なお、AWSのリソースや設定はTerraformで管理している。

# やったことその① `HostName`と`ReplaceKeyWith`でURLを完全に書き換える

まずはリダイレクトの基本設定として、`RoutingRules`に`RedirectRule`を追加する。

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-s3-websiteconfiguration-routingrules.html

`ReplaceKeyWith`を使ってすべてのリクエストを同じURLにリダイレクトする。そして、今回リダイレクトしたいURLはドメインも違うので`HostName`でリダイレクト先のドメインになる。（外部のドメインになるということは特に意識する必要はない。）

```terraform
website {
  routing_rules = jsonencode([
    {
      Redirect = {
        HostName         = "new.example.com"
        HttpRedirectCode = "301"
        Protocol         = "https"
        ReplaceKeyWith   = "home"
      }
    }
  ])
}
```

こうするとドメインもそれ以降のURL（CloudFormationでS3オブジェクトのキーとして使われていた部分）も置き換えられる。

# やったことその② `IndexDocument `を追加する

すべてのリクエストをリダイレクトしているから`IndexDocument`はいらないと思っていたけど、 `terraform apply`をしたら以下のエラーが発生した。

`Error: Must specify either index_document or redirect_all_requests_to.`

どうやらTerraformの使用上、`redirect_all_requests_to`を使っていない限り、`IndexDocument`の設定が必須。

```terraform
website {
  index_document = "home"
  routing_rules = jsonencode([
    {
      Redirect = {
        HostName         = "new.example.com"
        HttpRedirectCode = "301"
        Protocol         = "https"
        ReplaceKeyWith   = "home"
      }
    }
  ])
}
```

ページを含めて移行していたら、`redirect_all_requests_to`ですべてのリクエストのドメインだけを変えてリダイレクトを設定できるけど、どのリクエストでも固定したページにリダイレクトしたいので`IndexDocument`として現在の「Default Root Object」のキーを設定することにした。

# やったことその③ CloudFrontのOriginを変更する

上記のリダイレクト設定は正しいが、そもそもリダイレクトを使うためにCloudFrontが使っているS3 BucketのURLを変更する必要があるのが分かった。

https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/WebsiteEndpoints.html

NG: `old-example-files.s3.amazonaws.com`
OK: `old-example-files.s3-website.ap-northeast-1.amazonaws.com`

（CloudFrontのOrigin設定なのでブラウザに入力するURLと関係ない）

```diff
origin {
- domain_name = aws_s3_bucket.old_example_files.bucket_domain_name
+ domain_name = aws_s3_bucket.old_example_files.website_endpoint
  origin_id   = "S3-old-example-files"
}
```

Terraformの場合、`aws_s3_bucket`のリソースに`website_endpoint`という属性が使える。

https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket#website_endpoint

# やったことその④ Custom Origin Configを追加する

リソースの変更は成功したけど、ブラウザで古いサイトのページにアクセスしてみるとそもそも開かない…

> When we want to create a CloudFront origin for a S3 static website, we must define a custom_origin_config, otherwise it won't work.

https://github.com/hashicorp/terraform-provider-aws/issues/7847#issuecomment-470492989

Custom Origin Configの設定も必要か。

```terraform
origin {
  domain_name = aws_s3_bucket.old_example_files.website_endpoint
  origin_id   = "S3-old-example-files"

  custom_origin_config {
    http_port              = 80
    https_port             = 443
    origin_protocol_policy = "https-only"
    origin_ssl_protocols   = ["TLSv1.2"]
  }
}
```

# やったことその⑤ 使っているAWSリージョンの正しいウェブサイトエンドポイントを使うようにする

AWSリージョンによってURLが違うよ、とAWSのドキュメンテーションに書いてあったからTerrraformがよしなにやってくれると思いきや、そうでもなかった…

NG: `old-example-files.s3-website-ap-northeast-1.amazonaws.com`
OK: `old-example-files.s3-website.ap-northeast-1.amazonaws.com`

https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/WebsiteEndpoints.html

どのリージョンでも、Terraformの`website_endpoint`が生成するのは上の方になる。

リージョンはTerraformの変数として用意して、正しいURLを生成するロジックを自前で実装した。

```diff
-domain_name = aws_s3_bucket.old_example_files.website_endpoint
+domain_name = "${aws_s3_bucket.old_example_files.bucket}.s3-website.${var.aws_region}.amazonaws.com"
```

※ `ap-northeast-1`以外のリージョンを使う予定はなかったけど、他のリージョンも使使いたい場合は適切なロジックを組めばいい。各リージョンのウェブサイトエンドポイントが[こちら](https://docs.aws.amazon.com/general/latest/gr/s3.html#s3_website_region_endpoints)で確認できる。


# やったことその⑥ HTTPステータスによってリダイレクトするように変更する

ここまで来てやっとリダイレクトできるようになった :tada: 

ただ、そもそもBucketの中身も消したい…と思った時、この設定ではいけない。S3にファイルがある前提の設定になるので、コンテンツがなくても対応できるように少し変える必要がある。

```diff
website {
  index_document = "home"
  routing_rules = jsonencode([
    {
      Redirect = {
        HostName         = "new.example.com"
        HttpRedirectCode = "301"
        Protocol         = "https"
        ReplaceKeyWith   = "home"
      }
+     RoutingRuleCondition = {
+       HttpErrorCodeReturnedEquals = "404"
+     }
    }
  ])
}
```

`RoutingRuleCondition`を追加することで、リクエストされたオブジェクトが見つからない時だけリダイレクトするように変わる。S3 Bucketのすべてのオブジェクトを削除する予定なので、どのリクエストも404になる。つまり、どのリクエストもリダイレクトされる。

# 最後に

ここまですれば古いサイトへのリクエストはすべて新しいサイトにリダイレクトされるし、古いS3の中身を消してもOK！

:warning: 1点だけ注意が必要 :warning: 

リダイレクトさせるためにS3のBucket自体とCloudFrontのDistributionが必要になるので、リダイレクトさせたい間はリソースを残し続けないといけない。

リダイレクトをやめて古いサイトを完全に捨てる、ということになったらもちろんこれらのリソースも削除しちゃっても問題ない。