はじめに
どうもとーくんです。
自分のプロフィールサイトを今までVercelを使って公開していましたが、今回サイトをリニューアルするに当たって自分のサイトなのでできるだけ自分で構築した感が欲しくなり、今回S3+CloudFrontにサイトを移行しました。
ドメインはCloudflareに持たせたままTerraformで管理する形にしてます。
また、途中でACMの証明書発行がCAA_ERRORで失敗してハマったので、その原因と対処も書いてます。
公開してるサイトはこちらになります。
https://kaito071831.com
この記事でわかること
- CloudFront用のACM証明書(us-east-1)をTerraformモジュールに渡す方法
- ACMのDNS検証レコードをCloudflareに作る方法
- Cloudflare DNSでapexドメインをCloudFrontに向ける方法
- 移行元のCAAレコードでACMの証明書発行が失敗する原因と対処
環境
- Terraform 1.16.4
- AWS provider 6.66.0
- Cloudflare provider 5.25.0(v4とはリソース名が異なるので注意)
構成
- S3:パブリックアクセスブロック、Origin Access Control(OAC)経由でのみ読み取り
- CloudFront:ACM証明書でapexとwwwの両方を配信
- Cloudflare:DNSのみ(プロキシはオフ)
- state:S3バックエンド+use_lockfile
要点①:ACM証明書はus-east-1で作る必要がある
CloudFrontで使うSSL証明書はus-east-1で発行しないと使えません。
プロバイダを分けて、us-east-1用のconfiguration_aliasesを宣言してモジュールに渡してます。
以下プロバイダの設定例です。
provider "aws" {
alias = "japan"
region = "ap-northeast-1"
default_tags {
tags = {
terraform = "true"
}
}
}
provider "aws" {
alias = "global"
region = "us-east-1"
default_tags {
tags = {
terraform = "true"
}
}
}
上記で設定したaliasをモジュール側でconfiguration_aliasesとして設定する設定例です。
terraform {
required_version = "1.16.4"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">=6.66.0"
configuration_aliases = [aws.global]
}
cloudflare = {
source = "cloudflare/cloudflare"
version = ">=5.25.0"
}
}
}
モジュール呼び出し側のprovidersでaliasを渡す例です。
module "static_site" {
source = "../../modules/static_site"
providers = {
aws = aws.japan
aws.global = aws.global
cloudflare = cloudflare
}
domain_name = var.domain_name
bucket_name = var.domain_name
tags = {
"Project": "kaito071831.com"
}
}
要点②:ACMの検証レコードをCloudflareに作る
DNSがRoute 53ではないので、ACMでSSL証明書をDNS検証でリクエストする際の検証レコードをCloudflareプロバイダで作る必要があります。
検証用のCNAME情報が含まれるdomain_validation_optionsをCloudflare DNSに登録します。
今回はapexドメイン以外にwwwがついてるSAN(Subject Alternative Name)についても検証するためfor_eachでまとめてます。
以下ACMの検証レコードの設定例です。
resource "cloudflare_dns_record" "acm_validation" {
for_each = {
for dvo in aws_acm_certificate.this.domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
type = dvo.resource_record_type
value = dvo.resource_record_value
}
}
zone_id = data.cloudflare_zone.this.id
name = each.value.name
type = each.value.type
content = each.value.value
ttl = 60
proxied = false
}
僕はACM側のリソースでSANの設定をしていたのを忘れてたので証明書の検証が終わらずterraform applyをタイムアウトさせてしまいました。
要点③:Cloudflare DNSのapexにCNAMEを置く
CloudFrontは通常だと固定IPアドレスを持たないので、CNAMEでドメインをCloudFrontに向ける必要があります。
ただしapexドメインにはNSレコードとSOAレコードが必ず存在するため、DNSの仕様上、apexに同名のCNAMEを置くことはできません。今回はさらにGoogle Workspace用のMX/TXTレコードもあります。
そこでCloudflareのCNAME Flatteningを使います。(公式はこちら)
CNAME Flatteningを使うと、CloudflareがCNAME先のIPアドレスをA/AAAAレコードとして返してくれます。外から見るとapexにAレコードやAAAAレコードがあるように見えるので、NS/SOA/MX/TXTレコードと共存させることができます。
また、CloudFrontが既にCDNとして機能しているため、Cloudflare側のDNSのプロキシはオフにしています。
あくまで今回Cloudflare側は名前解決だけを担当してもらう方針です。
以下Cloudflare側のCNAME設定例です。
CNAME Flatteningについて書きましたが、Terraform上では通常と同様にCNAMEを登録するように書くだけなので特別な設定は不要です。
resource "cloudflare_dns_record" "cloudfront_apex" {
zone_id = data.cloudflare_zone.this.id
name = var.domain_name
type = "CNAME"
content = aws_cloudfront_distribution.this.domain_name
ttl = 1
proxied = false
}
resource "cloudflare_dns_record" "cloudfront_www" {
zone_id = data.cloudflare_zone.this.id
name = "www"
type = "CNAME"
content = aws_cloudfront_distribution.this.domain_name
ttl = 1
proxied = false
}
要点④:移行元のCAAレコードでACMの発行が失敗した
terraform applyを実行したところ、ACMの証明書発行が失敗しました。
Error: waiting for ACM Certificate (arn:aws:acm:us-east-1:************:certificate/xxxxxxxx) to be issued: unexpected state 'FAILED', wanted target 'ISSUED'. last error: CAA_ERROR
原因はVercelで使っていたCAAレコードが残っていたことでした。
CAAレコードはそのドメインの証明書をどの認証局が発行していいかを決めるレコードです。
VercelはLet's Encryptを使っているので、letsencrypt.orgだけが許可された状態で残っていました。
そのためAmazonの認証局は発行を許可されておらず、ACMがCAA_ERRORで失敗していました。
CAAレコードを削除したあと、以下のコマンドで証明書を作り直したところ、無事に発行されました。
terraform apply -replace={ACMのSSL証明書リソース}
一度FAILEDになったACM証明書は、CAAレコードを修正しても自動では再試行されないため、証明書自体を作り直す必要があります。
また、CAAの効果を残したい場合は、CAAレコードを削除するのではなくAmazonの認証局(amazon.comなど)を許可するCAAレコードを追加することもできます。
移行手順
- plan で作成リソースを確認
- Vercelの既存Aレコードを手動で削除
(apexに同名のAとCNAMEは共存できないため) - apply でリソースを作成
今回は上記手順でAレコードを先に削除したため、apply完了までの数分間サイトに繋がらない状態になりました。
ダウンタイムを避けるなら、先にCloudFrontとACMだけを作成してデプロイ完了した後に、Aレコードの削除とCNAMEの作成を行う2段階にするのが良いと思います。
また要点④のようにAmazon以外のCAAレコードが存在する可能性があるため、移行前に以下のコマンドで既存のCAAレコードを確認しておくと良いです。
dig CAA example.com +short
既存レコードを壊さない工夫点
Cloudflareのゾーンについてはdataで参照するだけにして、importしていません。
そのため今回TerraformではGoogle WorkspaceのMX/TXTには一切触れないようにしています。
今後の業務に向けて
今回の個人サイトを対象に移行を実施しましたが、実際の業務でも活かせそうなポイントがいくつかありました。
移行前の事前確認を手順化する
今回のCAA_ERRORは、移行元の設定が残っていたことが原因でした。
業務でDNSや証明書が絡む移行をするのであれば、CAA・既存のA/CNAME・MX/TXTなどを移行前に洗い出すことをチェックリストに入れておくべきだと感じました。
Terraformの管理範囲を明確にする
CloudflareのゾーンをimportせずにDNSレコードだけを管理したことで、既存のメール設定を壊すリスクを避けられました。
既存環境にTerraformを導入する際にも、「どこまでをTerraformで管理し、どこからを管理外にするか」を最初に明確に分けておくことが大事だと思いました。
おわりに
今回自分のプロフィールサイトのデプロイ先をTerraformで構築しました。
何気にCloudflareのプロバイダを使ったことがなかったのでプロバイダのドキュメントを読むのが面白かったです。
CNAME Flatteningを知らなかったので、構築前にRoute 53だとALIASレコードとかでやってるけどCloudflareどうするんだっけ?となったので良い機会になりました。
