Terraformのlocal-execで詰まったこと

こんにちは。最近、Terraform Associateが無事に取れたので、より普段書いているコードへの理解が深まってさらに楽しくなってきたところです。今回は、TerraformとAnsibleでGCEを作ったり、中身を設定する時の話です。

## なにをしたいか
前々からTerraformとAnsibleでリソースの管理を行なっていますが、

- Terraformコマンドを実行してリソースの作成（ここではGCE）
- Ansibleでさらに色々流しこむ

と2段構えでコマンド実行するのがなかなか面倒なんですよね（ものぐさと言うんですが）。なので、Terraformのプロビジョナーとして`local-exec`があるので、これを使って、**Terraformの実行のみでAnsibleまで完結させる**ことを目標にやっていきます。

## `local-exec`について
ここで、簡単に今回使うプロビジョナーである`local-exec`について簡単に説明します。

```instance.tf
resource "google_compute_instance" "instance" {
  name         = "sample-instance"
  machine_type = "n1-standard-1"
  zone         = "asia-northeast1-a"
  tags         = []

  boot_disk {
    initialize_params {
      image = "debian-cloud/debian-9"
    }
  }

  network_interface {
    network = "default"
  }

  service_account {
    scopes = ["cloud-platform"]
  }

  provisioner "local-exec" {
    command = "echo "Hello, World. >> hello.txt"
  }
}
```
上のようにGCEを作ることを仮定します。一番下に

```tf
provisioner "local-exec" {
  command     = "echo "Hello, World. >> hello.txt"
}
```
を配置していますが、**Terraformを実行しているマシン**で実行するコマンドを該当のリソースの作成途中に実行することができます。なので、GCEが出来上がった頃にはプロビジョナーで実行されたコマンドも終わっているので、作業しているところに`hello.txt`ができ上がっていることが確認できます。なので、今回は`local-exec`プロビジョナーを使ってAnsibleを実行します。

## 実際に作る
今回は作ったインスタンスにnginxを仕込む簡単なAnsibleを考えます。

```
compute_instance.tf
ansible
  ansible.cfg
  inventory
  nginx.yaml
  sshkey
  roles
    nginx
      tasks
        main.yaml
```
のディレクトリ構成で考えます。

### 実行する
```instance.tf
resource "google_compute_instance" "instance" {
  name         = "sample-instance"
  machine_type = "n1-standard-1"
  zone         = "asia-northeast1-a"
  tags         = []

  boot_disk {
    initialize_params {
      image = "debian-cloud/debian-9"
    }
  }

  network_interface {
    network = "default"
  }

  metadata = {
    ssh-keys = "ansible:${file("./ansible/sshkey/id_rsa.pub")}"
  }

  service_account {
    scopes = ["cloud-platform"]
  }

  provisioner "local-exec" {
    working_dir = "./ansible/"
    command     = "chmod 400 sshkey/id_rsa && ansible-playbook -i inventory nginx.yaml"
  }
}
```
Ansibleユーザーがコマンドを実行できるように事前に`metadata`で鍵を埋め込んでおきます。これで`terraform plan`を実行すればnginxの入ったインスタンスが完成します！

### 落ちる
ところが、インスタンスを作成していると、sshはできているようだが、ansibleを実行できずに終わってしまいます。この時、Terraformとしては、リソースは完成しているが、全部設定完了できなかったことになるので、`taint`がマークされます。なので、次回実行時は再作成になります。

### 改善する
結論、Terraformで作成完了したGCEをAnsibleで即時認識しようとしていたため起こった自称だと分かりました。改善するためには、`sleep`コマンドを実行を挟むことで改善できました。

```instance_after.tf
resource "google_compute_instance" "instance" {
  name         = "sample-instance"
  machine_type = "n1-standard-1"
  zone         = "asia-northeast1-a"
  tags         = []

  boot_disk {
    initialize_params {
      image = "debian-cloud/debian-9"
    }
  }

  network_interface {
    network = "default"
  }

  metadata = {
    ssh-keys = "ansible:${file("./ansible/sshkey/id_rsa.pub")}"
  }

  service_account {
    scopes = ["cloud-platform"]
  }

  provisioner "local-exec" {
    working_dir = "./ansible/"
    command     = "chmod 400 sshkey/id_rsa && sleep 15 && ansible-playbook -i inventory nginx.yaml"
  }
}
```

これで、Terraform内でAnsibleを実行できます。（sleepコマンドの期間は任意です）

## 何がおいしいのか
TerraformをAnsibleをコマンド分けて実行しているとき、Terraformはリソースの作成がゴールになります。しかし、`local-exec`を使うことでミドルウェアがインストール完了するところまでがTerraformのゴールになります。そのため、よりインフラの状態を厳密に定義することができるのではないかと私は思います。
ただ、一方でTerraformの分界点、Ansibleの分界点それぞれあると思いますが、それを分けるために`local-exec`をAnsibleに使わないという選択肢もあると思います。