Terraform と Wercker で DNS レコードを管理する

# はじめに
先日、 Terraform で Google Cloud DNS を管理して GitHub 経由で Wercker から反映する環境を構築し、その便利さに感動しました。

これには、大きく2つの恩恵があります。

- Terraform と Git による DNS レコードのバージョン管理
- GitHub と Wercker による自動反映
- チーム開発などで複数人がレコードを管理する事が容易になる (Pull Request を出してもらってレビュー後に master へ merge するだけ)

割と嵌ったので、手順を纏めておきます。

尚、 GCP のアカウントは発行済で、プロジェクトも作成済みとします。
https://console.cloud.google.com/

## 余談
筆者はこれまで [Gehirn DNS](https://www.gehirn.jp/gis/dns.html) を利用していました。このサービスは、自前で Git などを使うことなく DNS レコードのバージョン管理が行えます。API も用意されている上に指定した時刻に任意のバージョンへ切り替える機能(マイグレーション)まで備えています。
今回 Google Cloud DNS へ移行する理由は、 Gehirn DNS にしかない機能を活用していない事と、 Amazon Route 53 を含めた3つのサービスでコスト試算して一番安かったという理由があります。
Web コンソール上から手軽に DNS レコードのバージョン管理がしたい、 API さえ用意されていれば Terraform は使えなくても構わない方は Gehirn DNS が向いているかもしれません。

# Terraform から Google Cloud DNS を設定する
## ファイル構成
```
┬ .gitignore
├ account.json
├ dns_example_com.tf
├ gcp.tf
├ variables.tf
└ variables.tfvars
```

※ tf ファイルが同じディレクトリにあれば構成は自由なので、参考までに。

```:.gitignore
.*
!.gitignore

# Terraform
*.log
*.tfstate
*.tfstate.backup

account.json
```

## Google Provider の設定
https://www.terraform.io/docs/providers/google/

```:gcp.tf
provider "google" {
  credentials = "${file("account.json")}"
  project     = "${var.project}"
  region      = "${var.region}"
}
```

## Zone と Record の設定
https://www.terraform.io/docs/providers/google/r/dns_managed_zone.html

```:dns_example_com.tf
resource "google_dns_managed_zone" "example_com" {
  name        = "example-com"
  dns_name    = "example.com."
}
```

`dns_name` は末尾に `.` (ピリオド) が必要なので注意。

## Record の設定
https://www.terraform.io/docs/providers/google/r/dns_record_set.html

Zone の設定と同じファイルに追記します。

```:dns_example_com.tf
resource "google_dns_record_set" "a_example_com" {
  managed_zone = "${google_dns_managed_zone.example_com.name}"
  name         = "${google_dns_managed_zone.appcloud_info.dns_name}"
  type         = "A"
  ttl          = 300
  rrdatas      = [
    "192.30.252.153",
    "192.30.252.154"
  ]
}
```

## Variable
```:variables.tf
variable "region" {
  default = "asia-northeast1"
}
variable "project" {}
```

```:variables.tfvars
project = "project-id"
```

今回 `region` は東京リージョン以外には変えないので default 値を設定しておきます。

## 認証情報
https://console.cloud.google.com/apis/credentials

サービスアカウントキー (JSON) を作成します。

![service account key](https://qiita-image-store.s3.amazonaws.com/0/1804/9e3d4c85-7239-17b4-3624-7178aa04cab6.png)

新しいサービスアカウントを作成します。

![create service account](https://qiita-image-store.s3.amazonaws.com/0/1804/b37eea06-97c7-7d96-87b4-41cf2716dfc2.png)

作成すると認証情報が JSON 形式でダウンロードされるので、 `account.json` にリネームして同じ階層に配置します。

## nameservers
Google Cloud DNS や Amazon Route 53 では、新規ゾーンを作成した際にネームサーバが割り当てられます。

そこで、リソースが作成された後にネームサーバ一覧を出力するようにしておくと、 Web コンソールを開いて確認する手間が省けて便利です。

Zone の設定と同じファイルに追記します。

```:dns_example_com.tf
resource "null_resource" "nameservers_example_com" {
  triggers {
    name_servers = "${join("\n", google_dns_managed_zone.example_com.name_servers)}"
  }

  provisioner "local-exec" {
    command = <<EOF
echo "nameservers: ${google_dns_managed_zone.example_com.dns_name}"
echo "${self.triggers.name_servers}"
EOF
  }
}
```

## plan & apply
https://www.terraform.io/downloads.html

Terraform をインストールしておきます。
Mac なら brew で入れるのがおすすめです。

```
terraform plan -var-file=variables.tfvars
```

syntax に問題がある場合はこの時点でエラーが出ます。
問題なく、更新されるリソースが確認できたら apply します。

```
terraform apply -var-file=variables.tfvars
```

次のように、ネームサーバ一覧が出力されます。

```shell-session:null_resource
null_resource.nameservers_example_com (local-exec): nameservers: example.com.
null_resource.nameservers_example_com (local-exec): ns-cloud-d1.googledomains.com.
null_resource.nameservers_example_com (local-exec): ns-cloud-d2.googledomains.com.
null_resource.nameservers_example_com (local-exec): ns-cloud-d3.googledomains.com.
null_resource.nameservers_example_com (local-exec): ns-cloud-d4.googledomains.com.
```

ローカルから実行する分にはこれで完了です。

# Wercker で自動反映する
GitHub か Bitbucket でリポジトリを作成します。
Wercker ならプライベートリポジトリでも無料で利用することが出来ます。(2017年1月現在)

## ファイル構成
```
┬ .gitignore
├ account.json
├ dns_example_com.tf
├ gcp.tf
├ gcs.tf
├ variables.tf
├ variables.tfvars
└ wercker.yml
```

追加されたのは `gcs.tf` と `wercker.yml` です。

## remote state
ローカルで実行する場合は `terraform.tfstate` というファイルで設定状態が保存されています。ローカル以外で適切に反映させるには、この state を何らかの形で同期する必要があります。
それが、 remote state という機能です。

remote state の backend として S3 や GCS (Google Cloud Storage) が対応しています。ここでは GCS を選定しました。

### remote state backend
```:gcs.tf
resource "google_storage_bucket" "wercker_tfstate" {
  name          = "${var.remote_state_bucket}"
  location      = "${var.region}"
  storage_class = "REGIONAL"

  provisioner "local-exec" {
    # Enable versioning (not supported by terraform)
    # https://cloud.google.com/storage/docs/object-versioning
    command = <<EOF
gcloud auth activate-service-account --key-file account.json
gsutil versioning set on "${var.remote_state_bucket}"
gsutil versioning get "${var.remote_state_bucket}"
EOF
  }
}
```

state が何らかの事故により誤った内容で上書きされてしまった時の対策として、 GCS のバージョニング機能を設定します。しかしながら、現在 terraform ではサポートされていないので local-exec provisioner を使って直接 `gsutil` コマンドを叩いて有効化します。

次に、 variables.tf と variables.tfvars に `remote_state_bucket` と `remote_state_path` を追加します。

```:variables.tf
variable "region" {
  default = "asia-northeast1"
}
variable "project" {}
variable "remote_state_bucket" {}
variable "remote_state_path" {
  default = "terraform.tfstate"
}
```

```:variables.tfvars
project = "project-id"
remote_state_bucket  = "bucket-name"
```

※ `bucket-name` は他のユーザが同じ名前で登録していると使えません。重複しないように設定する必要があります。

### ローカル環境で remote state を試す
まずは、 GCS bucket を作るために apply しておきます。

```shell-session
terraform plan -var-file=variables.tfvars
terraform apply -var-file=variables.tfvars
```

次に `terraform remote config` を実行して、ローカルの tfstate を同期します。

```shell-session
terraform remote config \
          -backend=gcs \
          -backend-config='bucket=bucket_name' \
          -backend-config='path=terraform.tfstate' \
          -backend-config='project=project_id' \
          -backend-config="credentials=$(cat account.json)"
```


## Wercker Application を作成する
https://app.wercker.com/applications/create

リポジトリを登録して Wercker Application を作成します。

![make-my-app-public](https://qiita-image-store.s3.amazonaws.com/0/1804/6527f434-81da-7b34-5632-6416199347af.png)

**Make my app public** を選択してしまうと、プライベートリポジトリだったとしても公開されてしまうので、注意が必要です。

## build step の設定
`wercker.yml` で build と deploy の設定をします。

build では `terraform plan` を実行して tf ファイルを検証し、 deploy では `terraform apply` で実際に反映させます。

```yaml:wercker.yml
box: python:2.7

build:
  steps:
    - script:
      name: generate account.json
      code: |
        cat <<EOF > account.json
        {
          "private_key": "$GCP_PRIVATE_KEY",
          "client_email": "$GCP_CLIENT_EMAIL"
        }
        EOF

    - ww24/terraform:
      command: plan
      var_file: variables.tfvars
      remote_config: |
        -backend=gcs \
        -backend-config='bucket=bucket_name' \
        -backend-config='path=terraform.tfstate' \
        -backend-config='project=project_id' \
        -backend-config='credentials={ \
          "private_key": "$GCP_PRIVATE_KEY", \
          "client_email": "$GCP_CLIENT_EMAIL" \
        }'

deploy:
  steps:
    - script:
      name: install gsutil
      code: |
        pip install gsutil

    - ww24/terraform:
      command: apply
      var_file: variables.tfvars
      remote_config: |
        -backend=gcs \
        -backend-config='bucket=bucket_name' \
        -backend-config='path=terraform.tfstate' \
        -backend-config='project=project_id' \
        -backend-config='credentials={ \
          "private_key": "$GCP_PRIVATE_KEY", \
          "client_email": "$GCP_CLIENT_EMAIL" \
        }'
```

`bucket_name` と `project_id` は適宜変更してください。

拙作ですが [`ww24/terraform`](https://app.wercker.com/applications/584d8fe1a9be3a01005e268d/tab/details/) を使って terraform のインストールと plan, apply を実行しています。

`remote_config` に `-backend` や `-backend-config` を記述することで、 plan や apply の前に必ず `terraform remote config` が実行されます。

## Workflow の設定
master branch に変更があった際に deploy (terraform apply) されるように設定します。

![image](https://qiita-image-store.s3.amazonaws.com/0/1804/8845da3e-21c3-b488-75e4-6820534125bb.png)

__Add new pipeline__ から `deploy` を追加します。

![image](https://qiita-image-store.s3.amazonaws.com/0/1804/73451d74-2df3-ae79-ba76-448912f69fdb.png)

↓ pipeline に deploy が追加されました。

![image](https://qiita-image-store.s3.amazonaws.com/0/1804/2e975574-8352-1efe-3bf6-ddf34831022c.png)

Workflow editor の build の右にある __+__ ボタンを押して、 branch に `master`、 Execute pipeline に `deploy` を指定して Add します。

![image](https://qiita-image-store.s3.amazonaws.com/0/1804/32bf43e4-6e5d-9317-921c-191e996f3963.png)

以下のようになれば Workflow の設定は完了です。

![image](https://qiita-image-store.s3.amazonaws.com/0/1804/71d19cf8-87c5-7b8a-e2d8-4c5e1fcb5a06.png)

## Environment の設定
認証情報を `wercker.yml` に直接するわけにはいかないので、環境変数に設定します。

![image](https://qiita-image-store.s3.amazonaws.com/0/1804/41a7af79-c9d6-b8df-e7a7-74b316d6d580.png)

Protected に☑を入れると、基本的に Web 上から見れなくなるので有用です。
しかし、 build や deploy 時に思い掛けないエラーが発生して、エラーメッセージとして出力されてしまう場合があるので、 Wercker Application を public にしない等、対策が必要です。

あとは、 git push して GitHub 上のリモートリポジトリに変更があれば、 Wercker で build (terraform plan) が走ります。master に merge すると build + deploy (terraform apply) が走ります。

# さいごに
Terraform ではありませんが、業務で似たような構成で DNS レコードの管理を行っているのを拝見し、触発されました。

実際に動いているものがあるので、設定で分からないことがあれば参考にどうぞ。

https://github.com/ww24/dns
(ディレクトリ構成は若干異なります)