CloudflareのDNS設定をTerraformで管理する

今日はTerraformでCloudflareのDNS設定をしっかり管理してみたので、その手順を記しておきます。

## 大前提
- backendがすでに用意・設定されている
    - 以下のドキュメントを参考に設定しておきましょう！

https://www.terraform.io/language/settings/backends

## 構成
今回は複数のドメインを管理することも視野に入れて、このようなディレクトリ構成にしました。

- `example-com/`
    - `required_providers.tf`
        - `required_providers`をここで定義
        - 若干腑に落ちないですが、各モジュールに定義しないと、`you must specify that source address in each module which requires that provider`と怒られてしまうのでモジュールごとに設定します
    - `records.tf`
        - 各Aレコード、AAAAレコードなどの`cloudflare_record`をここに定義
    - `vars.tf`
        - CloudflareのAPIキーなどの`variable`をここに定義(外側から渡せるようにする)
- `example-net/`
    - `example-com/` と同様の構成
- `main.tf`
    - `example-com/` と `example-net/` の `module` をここから呼び出す
- `secrets.auto.tfvars`
    - CloudflareのAPIキーなどの秘匿情報をここに記載
    - `.gitignore` に忘れずに入れておきましょう！
- `vars.tf`
    - secrets.tfvarsから指定する`variable`をここに指定
- `provider.tf`
    - 親モジュールにCloudflareのmoduleを定義しておきます

https://qiita.com/bigwheel/items/2b420183639416b5c6bb

を参考に、子モジュール内に`provider`を置かないようにしています。

### 各ドメインのモジュールの定義
こちらは定義しておかないと、`you must specify that source address in each module which requires that provider`のエラーになってしまうので各モジュールに置いておきます。
(設定しておかないと、親モジュールの設定と反して`hashicorp/cloudflare`を見に行くようになってしまいエラーになってしまうようです。ちなみに`cloudflare/cloudflare`を見に行く設定が正しいです。)

```tf:example-com/required_providers.tf
terraform {
  required_providers {
    cloudflare = {
      source  = "cloudflare/cloudflare"
    }
  }
}
```

variableを定義して外から設定値を渡せるようにしておきます。

zone_idは各ドメイン固有のものなので、`locals`を使っても良いかと思いますが今回はvarsで外から渡すようにしました。

```tf:example-com/vars.tf
variable "cloudflare_zone_id" {
  type = string
}
```

肝心のドメインの設定ですが、例としていくつかAレコードを設定する際の設定を記しておきます。
もちろん、`toset`などの便利なものも使えるので必要に応じてlocalsなども活用してシンプルに設定していきましょう。

設定可能な項目についてはこちらの公式ドキュメントが参考になります。

https://registry.terraform.io/providers/cloudflare/cloudflare/latest/docs

```tf:example-com/records.tf
# test.example.com
resource "cloudflare_record" "a-test" {
  name    = "test"
  type    = "A"
  value   = "1.1.1.1"
  zone_id = var.cloudflare_zone_id
  proxied = true
}

# test2.example.com
resource "cloudflare_record" "a-test2" {
  # もちろんtosetも使えます！
  for_each = toset([
    "1.1.1.1",
    "1.0.0.1",
  ])
  name    = "test2"
  type    = "A"
  value   = each.key
  zone_id = var.cloudflare_zone_id
  proxied = true
}
```
### 親モジュールの定義
各モジュールを削除したい時を配慮してproviderは子モジュール内ではなく親モジュールに定義しておきます。(削除時の厄介な挙動にぶち当たらないために)
また、バージョンの罠にハマらないようにしっかりバージョンは固定しておきます。

```tf
terraform {
  required_providers {
    cloudflare = {
      source  = "cloudflare/cloudflare"
      version = "3.11.0"
    }
  }
}

provider "cloudflare" {
  email   = var.cloudflare_email
  api_key = var.cloudflare_api_key
}
```

こちらはシンプルにzone_idだけ外から渡すようにしました。ここから各ドメインのモジュールを呼び出します。

```tf:main.tf
module "example-com" {
  source             = "./example-com"
  cloudflare_zone_id = "xxxxxxxxxxxxxxxxxxxxxxxx"
}

module "example-net" {
  source             = "./example-net"
  cloudflare_zone_id = "xxxxxxxxxxxxxxxxxxxxxxxx"
}
```

シークレットは`secrets.auto.tfvars`に定義しておきます。(忘れずに.gitignoreに入れておきましょう)

ちなみに、通常tfvarsファイルは`-var-file`オプションで明示的に渡してあげる必要がありますが、`auto.tfvars`にすることで指定しなくても自動的に読み込むようにできます。

```tf:secrets.auto.tfvars
cloudflare_email   = "<YOUR_CF_EMAIL_HERE>"
cloudflare_api_key = "<YOUR_CF_API_KEY_HERE>"
```

tfvarsから読み出したい設定はvars.tfに置いておきます。

```tf:vars.tf
variable "cloudflare_email" {
  type = string
}

variable "cloudflare_api_key" {
  type = string
}
```

## さいごに
Terraformからapplyした後に、Cloudflareのダッシュボードを見に行くと設定が反映されているのが確認出来ると思いますが、Terraformから作成したリソースの編集保護などの機能はないためそのまま普通に編集出来てしまいます。

編集してしまうとそのリソースはTerraform側と差分が生じてエラーになってしまうのでうっかり触らないように注意しましょう。