DNSimpleをTerraform+GitHub+CircleCIで管理する

処理の流れとしては次のような感じ。

- GitHubにpushされるとCircleCI上で`terraform plan`が走る
- そのPRがmasterにマージされると`terraform apply`が走る

# Terraformのテンプレート

まずは[dnsimpleプロバイダ](https://www.terraform.io/docs/providers/dnsimple/index.html)について記述する。

```:dnsimple.tf
variable "dnsimple_email" {} # 環境変数TF_VAR_dnsimple_email
variable "dnsimple_token" {} # 環境変数TF_VAR_dnsimple_token

provider "dnsimple" {
  token = "${var.dnsimple_token}"
  email = "${var.dnsimple_email}"
}
```

DNSimpleに接続する際のtokenとemailをTerraformに渡す必要があるのだけれど、それらはリポジトリに載せたくないので環境変数として渡すようにしておく。その際[環境変数には接頭辞TF_VAR_が必要](https://www.terraform.io/docs/configuration/variables.html)で、例えば環境変数`TF_VAR_foo`は`$var.foo`として参照する。

続けて`www.example.com`に対して`example.herokuapp.com`へのALIASレコードを設定する場合は次のように記述する。

```:dnsimple.tf
resource "dnsimple_record" "example-com" {
  domain  = "example.com"           # 設定する対象のドメイン
  name    = "www"                   # サブドメイン部分。ルートドメインなら空文字を渡す
  value   = "example.herokuapp.com" # ALIAS先。AレコードならIPアドレス、CNAMEなら転送先を渡す
  type    = "ALIAS"                 # レコードのタイプ
}
```

[dnsimple_recordリソース](https://www.terraform.io/docs/providers/dnsimple/r/record.html)にはほかにもTTLのオプションなどがある。設定値については[DNSimpleのレコード作成のAPI](https://developer.dnsimple.com/domains/records/#create)も参考になる。

# DNSimpleへの反映

先のテンプレートファイルのあるディレクトリで`terraform apply`コマンドを実行する。

```
$ terraform apply
```

# tfstateをAWS S3に保存する

`terraform apply`でリソースへ変更を行うと、最新の状態を記録した`terraform.tfstate`というファイルが作成・更新される。詳しくは[ドキュメント](https://www.terraform.io/docs/state/index.html)に任せるとして、このファイルがCircleCI上で毎回作成されては破棄されるようだといざ手元で作業したときに困りそう。ということでS3に記録するようにする。[^1]

たとえば`s3://example-terraform-state/terraform.tfstate`として保存する場合、まず下準備として次のことを行う。

1. S3で example-terraform-state バケットを作る
2. そのバケットにファイルを書き込める権限（S3FullAccessとか）を持ったIAMユーザを作成する
3. 鍵情報とバケットのリージョン情報として、次の3つの環境変数を設定する
    - AWS_ACCESS_KEY_ID
    - AWS_SECRET_ACCESS_KEY
    - AWS_DEFAULT_REGION

それが終わったら`terraform remote config`[^2]で保存先を設定する。

```
$ terraform remote config \
  -backend=S3 \
  -backend-config="bucket=example-terraform-state" \
  -backend-config="key=terraform.tfstate"
```

そして反映の前後で、S3から取得（pull）、更新（push）を行う。

```
$ terraform remote pull
$ terraform apply # もしくはplan
$ terraform remote push
```

ちなみに`terraform remote config`すると、それまでカレントディレクトリにできていた`terraform.tfstate`と`terraform.tfstate.backup`が、`.terraform`というディレクトリに作られるようになる。このディレクトリの中身はS3に上がるものと同じなので`.gitignore`に追加しても問題ないと思う。

# circle.yml

内容としては次のことが行われるようにする。

- Terraformをダウンロードしてきて~/.terraformに展開、パスを通す
- ~/.terraformをキャッシュする
- testで
    - terraform remote config
    - terraform remote pull
    - terraform plan
    - terraform remote push
- masterブランチにマージされたら
    - terraform remote config
    - terraform remote pull
    - terraform apply
    - terraform remote push

```yaml:circle.yml
machine:
  environment:
    PATH: $HOME/.terraform:$PATH
    TERRAFORM_VERSION: 0.5.1
dependencies:
  cache_directories:
    - ~/.terraform
  pre:
    - |
      mkdir -p $HOME/.terraform
      if [ -z "$(ls -A $HOME/.terraform)" ]; then
        cd $HOME/.terraform
        curl -LO https://dl.bintray.com/mitchellh/terraform/terraform_${TERRAFORM_VERSION}_linux_amd64.zip
        unzip terraform_${TERRAFORM_VERSION}_linux_amd64.zip
        rm terraform_${TERRAFORM_VERSION}_linux_amd64.zip
      fi
test:
  pre:
    - |
      terraform remote config -backend=S3 -backend-config="bucket=example-terraform-state" -backend-config="key=terraform.tfstate"
      terraform remote pull
  override:
    - |
      terraform plan
  post:
    - |
      terraform remote push
deployment:
  production:
    branch: master
    commands:
      - |
        terraform remote config -backend=S3 -backend-config="bucket=example-terraform-state" -backend-config="key=terraform.tfstate"
        terraform remote pull
        terraform apply
        terraform remote push
```


ちなみに、circle.ymlに処理を記述するだけだといざ手元で実行しようとしたときに不便なので、実際には[rakeタスクとして定義](https://gist.github.com/a2ikm/23529a7452bbc702f13c)し、それを呼び出すようにしている。

# CircleCIでの設定

GitHubとCircleCIを連携する。

またこれまでに出てきた次の5つの環境変数をCircleCIのプロジェクトごとの画面から設定する。

| 変数名 | 概要 |
|---|---|
| TF_VAR_dnsimple_email | DNSimpleに登録してるemail |
| TF_VAR_dnsimple_token | DNSimpleのAPI Token |
| AWS_ACCESS_KEY_ID | AWS IAMで発行したユーザのAccess Key ID |
| AWS_SECRET_ACCESS_KEY | AWS IAMで発行したユーザのSecret Access Key |
| AWS_DEFAULT_REGION | AWSで作ったS3のバケットのリージョン。Tokyoだとap-northeast-1 |

# 所感

- Terraformで管理する前にすでに同じレコードが登録されている場合、applyで上書きできずに失敗する。遊びで使っているドメインなら削除してからapplyして作りなおせばいいけど、業務で使っているような場合には厳しい。
    - ちなみに今回の場合はtfファイルで一度でも記述したドメインのみが管理対象になるのに対し、[Roadworker](https://github.com/winebarrel/roadworker)の場合はドメイン（Hosted Zone）以下全てが管理対象になる。
- テストとして`terraform plan`が走るだけだと、実際の記述が正しいかどうかはわからない。ただtfファイルの記述の時点で処理が入り乱れるようなものでもないし、あくまでも誰か人間が`terraform apply`しなきゃいけないっていう手間を省くものとしてはアリな気がしている。

# 参考

- [Amazon S3 で Terraform の状態管理ファイル terraform.tfstate を管理 / 共有する](http://qiita.com/dtan4/items/04632f1c2f35388a3283)
- [Terraform + GitHub + CircleCI + Atlasを利用してAWSの操作を自動化した](http://blog.glidenote.com/blog/2015/02/18/terraform-github-circleci-atlas-aws/)
- [CircleCIで任意のツールをインストールして使う際の定番スニペット](http://stormcat.hatenablog.com/entry/2015/03/05/113000)
- [Route53のレコードをRoadworkerとCircleCIを使ってPull Requestベースで管理する](http://ikm.hatenablog.jp/entry/2014/11/21/021921)

[^1]: Terraform v0.5.0以上が必要
[^2]: https://www.terraform.io/docs/commands/remote.html