AnsibleのTemplate機能を使ってGCPを管理するTerraform設定を生成する

#はじめに

[LOB Advent Calendar](https://qiita.com/advent-calendar/2018/lob-inc) 向けの記事ということで、今回はLOB内で使っている運用系ツールの記事を書こうと思います。
LOBではGoogle Cloud Platform(GCP)のインスタンスの管理にTerraformというツールを使っているのですが、設定ファイル(.tf)でサポートされている文法がAnsibleのテンプレートエンジンと比較して機能が少ないので、現在はAnsibleでTerraformの設定を生成するようになりました。本稿ではテンプレートファイルから複数のGCPのプロジェクトの設定を生成して実際に反映するまでの流れを紹介したいと思います。

## メリット・デメリット

もちろんメリットばかりではなくてデメリットもありますので導入の際は注意してください。

- メリット
    - Ansibleのテンプレート(Jinja2)が高機能
    - 生成後の設定ファイルがわかりやすい
        - Terraformの変数定義を使わなくなるので設定がわかりやすくなります。
    - 異なるツール間で値を共有しやすい
        - Terraformで設定した値をKubernetesのYAMLの設定に埋め込んだりするのがやりやすくなります。

- デメリット
    - 設定の生成に時間がかかる
        - 今回は触れませんがansibleのタグの機能とセットでxargsやparallelをなどの並列実行コマンドを使うと高速化できます
    - 生成された設定が冗長
        - Terraformのループ機構などは使わなくなるので生成される設定は冗長的になります
    - Ansibleの学習コストが必要


# 全体の概要
それではまず最終的な成果物の概要を説明します。

プロジェクトがある程度の規模になりますと複数の結合環境を用意することが多いと思いますので、今回は「template」ディレクトリ以下に配置してあるテンプレートファイルから「dev」,「stg」,「prd」という3つの環境の設定を生成してみることにします。

最終的にはディレクトリの中に以下のファイルが配置されます。

```
.
├── ansible.cfg (ansibleの設定。リトライファイルの生成を抑制する目的で配置)
├── pre.yml　(dev.yml, stg.yml, prd.ymlを生成するためのPlaybook)
├── dev.yml (devの設定を生成するためのPlaybook)
├── stg.yml (stgの設定を生成するためのPlaybook)
├── prd.yml (prdの設定を生成するためのPlaybook)
├── dev
│   └── gcp
│       └── terraform (dev.ymlを実行すると生成される設定)
│           ├── cloudsql.tf
│           ├── credential.json
│           └── providor.tf
├── stg
│   └── gcp
│       └── terraform (stg.ymlを実行すると生成される設定)
│           ├── cloudsql.tf
│           ├── credential.json
│           └── providor.tf
├── prd
│   └── gcp
│       └── terraform (prd.ymlを実行すると生成される設定)
│           ├── cloudsql.tf
│           ├── credential.json
│           └── providor.tf
└── template 
    ├── env.yml.j2 (dev.yml, stg.yml, prd.ymlのベースとなるテンプレート)
    └── gcp
        └── terraform
            ├── dev
            │   └── credential.json.j2　(サービスアカウントのクレデンシャル(dev))
            ├── stg
            │   └── credential.json.j2　(サービスアカウントのクレデンシャル(stg))
            ├── prd
            │   └── credential.json.j2　(サービスアカウントのクレデンシャル(prd))
            └── share
                ├── cloudsql.tf.j2 (Terraform で cloud sql を生成するための設定)
                └── providor.tf.j2 (Terraform で GCP を使うための設定)
```


#ディレクトリ作成

では、ディレクトリの作成から始めましょう。
ディレクトリは「/<環境>/<操作対象>/<ツール名>」の構成にしておくのがオススメです。少しディレクトリの階層が深いような気もしますが、この構成にしておきますとgcpの操作をするツールが増えた場合でも柔軟に対応できます。

以下のコマンドを実行してください。

```makedir.sh
$ mkdir -p ./infra/dev/gcp/terraform/
$ mkdir -p ./infra/stg/gcp/terraform/
$ mkdir -p ./infra/prd/gcp/terraform/
$ mkdir -p ./infra/template/gcp/terraform/dev
$ mkdir -p ./infra/template/gcp/terraform/stg
$ mkdir -p ./infra/template/gcp/terraform/prd
$ mkdir -p ./infra/template/gcp/terraform/share
```

上記のコマンドを実行すると以下のようなディレクトリ構造になります。

```
infra
├── dev
│   └── gcp
│       └── terraform
├── stg
│   └── gcp
│       └── terraform
├── prd
│   └── gcp
│       └── terraform
└── template
    └── gcp
        └── terraform
            ├── dev
            ├── stg
            ├── prd
            └── share
```


# インストール

terraform と ansible はお好みの方法で入れてください。Mac だとサクッとインストールができて便利ですね。

```Macの場合の例
$ brew install terraform ansible
```


# ansibleの設定ファイル

Ansibleの実行でエラーが起こるとretryファイルが生成されるので事前に無効化しておきましょう。

```
infra
├── ansible.cfg (ここに作成する)
├── dev
│   └── gcp
│       └── terraform
├── stg
│   └── gcp
│       └── terraform
├── prd
│   └── gcp
│       └── terraform
└── template
    └── gcp
        └── terraform
            ├── dev
            ├── stg
            ├── prd
            └── share
```

```ansible.cfg
[defaults]
retry_files_enabled = False
```

# GCPのサービスアカウントの生成と設置

Terraform から利用する GCP のサービスアカウントを管理コンソールで生成してください。なお、サービスアカウントの権限は Terraform から GCP のインスタンスを生成できるようにプロジェクトの編集者にしておいてください。今回は対象としていませんが今後ユーザのアクセス権限(Iam)まで操作する場合はプロジェクトの編集権限よりも上のオーナー権限が必要になりますので注意してください。

![service_account.png](https://qiita-image-store.s3.amazonaws.com/0/258949/a5e4f532-5ed0-f85a-ae84-a08d717f16c2.png)

管理コンソール上で生成したサービスアカウントのクレデンシャルファイル(JSON)はリネームしつつ以下のディレクトリに設置してください。設置時に .json の拡張子に追加で .j2 の拡張子を付与していますが、これは ansible の jinja2 テンプレートの標準の拡張子です。気にせずそのまま進めてください。

dev, stg, prd のクレデンシャルファイルの配置が完了すると以下のようなディレクトリ構成になります。

```
infra
├── ansible.cfg
├── dev
│   └── gcp
│       └── terraform
├── stg
│   └── gcp
│       └── terraform
├── prd
│   └── gcp
│       └── terraform
└── template
    └── gcp
        └── terraform
            ├── dev
            │   └── credential.json.j2 (devのプロジェクトの鍵を配置する)
            ├── stg
            │   └── credential.json.j2 (stgのプロジェクトの鍵を配置する)
            ├── prd
            │   └── credential.json.j2 (prdのプロジェクトの鍵を配置する)
            └── share
```

# CloudSQLのインスタンスを構築するTerraformの設定を作成

次に Terraform の設定のテンプレートを作成します。
以下のように providor.tf.j2 と cloudsql.tf.j2 を作成して保存してください。

```
infra
├── ansible.cfg
├── dev
│   └── gcp
│       └── terraform
├── stg
│   └── gcp
│       └── terraform
├── prd
│   └── gcp
│       └── terraform
└── template
    └── gcp
        └── terraform
            ├── dev
            │   └── credential.json.j2
            ├── stg
            │   └── credential.json.j2
            ├── prd
            │   └── credential.json.j2
            └── share
                ├── providor.tf.j2 (terraformでgcpを使うための設定を配置)
                └── cloudsql.tf.j2 (cloud sql の共通設定を配置)

```

以下の「{{ ... }}」で囲まれた部分の値がAnsibleを実行すると置き換わります。

```providor.tf.j2
provider "google" {
  credentials = "${file("credential.json")}"
  project     = "{{project}}"
  region      = "asia-northeast1-a"
}
```

```cloudsql.tf.j2
resource "google_sql_database_instance" "mysql-{{env}}" {
  name             = "mysql-{{env}}"
  database_version = "MYSQL_5_7"
  region           = "asia-northeast1"
  project          = "{{project}}"

  settings {
    tier      = "{{terraform.cloudsql.default.tier}}"
    disk_size = "{{terraform.cloudsql.default.disk_size}}"
  }
}
```

# Ansible Playbook の作成

dev, stg, prd の設定を生成するための Ansible の Playbook を準備します。
Playbook の名前は dev.yml, stg.yml, prd.yml で作成するのですが、この３つのファイルは内容がほとんど同じなので、pre.yml と env.yml.j2 の 2 つから生成するようにします。

以下の通りファイルを設置してください。

```
infra
├── ansible.cfg
├── pre.yml (dev.yml, stg.yml, prd.yml を生成するためのplaybook)
├── dev
│   └── gcp
│       └── terraform
├── stg
│   └── gcp
│       └── terraform
├── prd
│   └── gcp
│       └── terraform
└── template
    └── env.yml.j2 (dev.yml, stg.yml, prd.ymlのテンプレート)
    └── gcp
        └── terraform
            ├── dev
            │   └── credential.json.j2
            ├── stg
            │   └── credential.json.j2
            ├── prd
            │   └── credential.json.j2
            └── share
                ├── providor.tf.j2
                └── cloudsql.tf.j2

```

以下の pre.yml の中に環境ごとの設定差分を記述していきます。

```pre.yml
- hosts: localhost
  gather_facts: no
  vars:
    loop_list:
      - env: dev
        gcp:
          terraform:
            cloudsql:
              default:
                tier: db-n1-standard-1
                disk_size: 10
      - env: stg
        gcp:
          terraform:
            cloudsql:
              default:
                tier: db-n1-standard-1
                disk_size: 100
      - env: prd
        gcp:
          terraform:
            cloudsql:
              default:
                tier: db-n1-standard-8
                disk_size: 100

  tasks:
    - name: ./{{ env }}.yml
      template:
        src: ./template/env.yml.j2
        dest: ./{{ env }}.yml
      vars:
        env: "{{ item.env }}"
        gcp: "{{ item.gcp }}"
      loop: "{{ loop_list }}"
```

以下の env.yml のテンプレートをベースに pre.yml に定義された変数を埋め込んで dev.yml, stg.yml, prd.yml を生成します。

```env.yml.j2
- hosts: localhost
  gather_facts: no
  vars:
    env: {{env}}
    project: lob-{{env}}
  tasks:
    - name: {{env}}/gcp/terraform/credential.tf
      tags: {{env}}/gcp/terraform/credential.tf
      template:
        src: ./template/gcp/terraform/{{env}}/credential.json.j2
        dest: ./{{env}}/gcp/terraform/credential.json
      vars:
        terraform: {{gcp.terraform}}

    - name: {{env}}/gcp/terraform/providor.tf
      tags: {{env}}/gcp/terraform/providor.tf
      template:
        src: ./template/gcp/terraform/share/providor.tf.j2
        dest: ./{{env}}/gcp/terraform/providor.tf
      vars:
        terraform: {{gcp.terraform}}

    - name: {{env}}/gcp/terraform/cloudsql.tf
      tags: {{env}}/gcp/terraform/cloudsql.tf
      template:
        src: ./template/gcp/terraform/share/cloudsql.tf.j2
        dest: ./{{env}}/gcp/terraform/cloudsql.tf
      vars:
        terraform: {{gcp.terraform}}
```

# 各環境のコンフィグを生成する playbook の生成

それでは ansible を実行して playbook の生成を行います。
以下のコマンドを実行すると dev.yml, stg.yml, prd.yml が生成されます。

```
$ ansible-playbook pre.yml
PLAY [localhost] **************************************************************************************************

TASK [./{{ env }}.yml] ********************************************************************************************
changed: [localhost] => (item={'env': 'dev', 'gcp': {'terraform': {'cloudsql': {'default': {'tier': 'db-n1-standard-1', 'disk_size': 10}}}}})
changed: [localhost] => (item={'env': 'stg', 'gcp': {'terraform': {'cloudsql': {'default': {'tier': 'db-n1-standard-1', 'disk_size': 100}}}}})
changed: [localhost] => (item={'env': 'prd', 'gcp': {'terraform': {'cloudsql': {'default': {'tier': 'db-n1-standard-8', 'disk_size': 100}}}}})

PLAY RECAP ********************************************************************************************************
localhost                  : ok=1    changed=1    unreachable=0    failed=0
```

env.yml の内容がベースとなって dev.yml が生成されていると思います。

```dev.yml
- hosts: localhost
  gather_facts: no
  vars:
    env: dev
    project: lob-dev
  tasks:
    - name: dev/gcp/terraform/credential.tf
      tags: dev/gcp/terraform/credential.tf
      template:
        src: ./template/gcp/terraform/dev/credential.json.j2
        dest: ./dev/gcp/terraform/credential.json
      vars:
        terraform: {'cloudsql': {'default': {'tier': 'db-n1-standard-1', 'disk_size': 10}}}

    - name: dev/gcp/terraform/providor.tf
      tags: dev/gcp/terraform/providor.tf
      template:
        src: ./template/gcp/terraform/share/providor.tf.j2
        dest: ./dev/gcp/terraform/providor.tf
      vars:
        terraform: {'cloudsql': {'default': {'tier': 'db-n1-standard-1', 'disk_size': 10}}}

    - name: dev/gcp/terraform/cloudsql.tf
      tags: dev/gcp/terraform/cloudsql.tf
      template:
        src: ./template/gcp/terraform/share/cloudsql.tf.j2
        dest: ./dev/gcp/terraform/cloudsql.tf
      vars:
        terraform: {'cloudsql': {'default': {'tier': 'db-n1-standard-1', 'disk_size': 10}}}
```

上記のタスクのうち、cloud sqlのインスタンスを生成する部分を抜粋すると以下になります。
このタスクは「./template/gcp/terraform/share/cloudsql.tf.j2」を「./dev/gcp/terraform/cloudsql.tf」にコピーする際にvarsで定義した値を埋めて出力するという動作をします。

```dev.yml(抜粋)
    - name: dev/gcp/terraform/cloudsql.tf
      tags: dev/gcp/terraform/cloudsql.tf
      template:
        src: ./template/gcp/terraform/share/cloudsql.tf.j2
        dest: ./dev/gcp/terraform/cloudsql.tf
      vars:
        terraform: {'cloudsql': {'default': {'tier': 'db-n1-standard-1', 'disk_size': 10}}}
```

```cloudsql.tf.j2
resource "google_sql_database_instance" "mysql-{{env}}" {
  name             = "mysql-{{env}}"
  database_version = "MYSQL_5_7"
  region           = "asia-northeast1"
  project          = "{{project}}"

  settings {
    tier      = "{{terraform.cloudsql.default.tier}}"
    disk_size = "{{terraform.cloudsql.default.disk_size}}"
  }
}
```

# dev 環境のコンフィグを生成する

それではdev環境のコンフィグを生成してみましょう。
以下のコマンドを実行してください。

```
$ ansible-playbook dev.yml
PLAY [localhost] **************************************************************************************************

TASK [dev/gcp/terraform/credential.tf] ****************************************************************************
changed: [localhost]

TASK [dev/gcp/terraform/providor.tf] ******************************************************************************
changed: [localhost]

TASK [dev/gcp/terraform/cloudsql.tf] ******************************************************************************
changed: [localhost]

PLAY RECAP ********************************************************************************************************
localhost                  : ok=3    changed=3    unreachable=0    failed=0
```

上記のコマンドを実行した後は以下のディレクトリにファイルが生成されているはずです。ansible-playbook コマンドの引数にstg.yml, prd.ymlを指定すれば同様にstg, prdのコンフィグが生成されます。

```
└─ dev
    └── gcp
        └── terraform
            ├── cloudsql.tf
            ├── credential.json
            └── providor.tf

```

cloud　sqlの設定を確認すると以下のようにきちんと変数が実際の値で置き換わっているはずです。

```cloudsql.tf
resource "google_sql_database_instance" "mysql-dev" {
  name             = "mysql-dev"
  database_version = "MYSQL_5_7"
  region           = "asia-northeast1"
  project          = "rd2n-dev"

  settings {
    tier      = "db-n1-standard-1"
    disk_size = "10"
  }
}
```

# Terraform でインスタンスを生成する

生成したterraformの設定で実際にGCP上にインスタンスを作成してみましょう。
これはterraformのディレクトリに移動して初期化を行った後に反映するだけです。

初期化

```
$ cd ./dev/gcp/terraform
$ terraform init

Initializing provider plugins...
- Checking for available provider plugins on https://releases.hashicorp.com...
- Downloading plugin for provider "google" (1.19.1)...
...
```

反映

```
$ terraform apply

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  + google_sql_database_instance.mysql-dev
      id:                                <computed>
      connection_name:                   <computed>
      database_version:                  "MYSQL_5_7"
      first_ip_address:                  <computed>
      ip_address.#:                      <computed>
      master_instance_name:              <computed>
      name:                              "mysql-dev"
      project:                           "lob-dev"
      region:                            "asia-northeast1"
      replica_configuration.#:           <computed>
      self_link:                         <computed>
      server_ca_cert.#:                  <computed>
      service_account_email_address:     <computed>
      settings.#:                        "1"
      settings.0.activation_policy:      <computed>
      settings.0.availability_type:      <computed>
      settings.0.backup_configuration.#: <computed>
      settings.0.crash_safe_replication: <computed>
      settings.0.disk_autoresize:        "true"
      settings.0.disk_size:              "10"
      settings.0.disk_type:              <computed>
      settings.0.ip_configuration.#:     <computed>
      settings.0.location_preference.#:  <computed>
      settings.0.pricing_plan:           "PER_USE"
      settings.0.replication_type:       "SYNCHRONOUS"
      settings.0.tier:                   "db-n1-standard-1"
      settings.0.version:                <computed>


Plan: 1 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes
```

上記のコマンドに成功すると GCP 上に cloud sql のインスタンスが生成されていると思います。


# まとめ

以後、この構成を基本として少しずつ拡張するようにしていくと、少ない記述で柔軟な設定が生成できるようになると思います。

## dev環境の設定を生成する場合

```
$ ansible-playbook pre.yml (dev.yml, stg.yml, prd.ymlの生成)
$ ansible-playbook dev.yml (devの設定の生成)
```

## stg環境の設定を生成する場合

```
$ ansible-playbook pre.yml (dev.yml, stg.yml, prd.ymlの生成)
$ ansible-playbook stg.yml (stgの設定の生成)
```

## prd環境の設定を生成する場合

```
$ ansible-playbook pre.yml (dev.yml, stg.yml, prd.ymlの生成)
$ ansible-playbook prd.yml (prdの設定の生成)
```


今回、説明した内容を実現するための方法は世の中に無数にありますがasibleという利用者が比較的多いツールはプロジェクト内での導入や運用が比較的容易なのではないでしょうか。同様のやり方を模索している人の参考になれば幸いです。