Ansibleのタグの機能を使ってタスクを並列実行する

#はじめに

前回、[LOB Advent Calendar](https://qiita.com/advent-calendar/2018/lob-inc) 向けの記事で書いた「[AnsibleのTemplate機能を使ってGCPを管理するTerraform設定を生成する](https://qiita.com/tatsugogo55/items/2216dcbb5b399d8d8b59)」の内容から派生させてAnsibleのタグの機能を利用して依存関係が無いタスクを並列処理する方法を紹介したいと思います。そもそも Playbook のタスクを並列実行したい場合は Playbook を分けるべきという意見はあると思いますが、その辺はご容赦ください。

#概要

今回は pub/sub のインスタンスを各環境ごとに 100 セットずつ用意するケースについてやってみようと思います。
Ansible の並列実行のサンプルなのでタスク数を増やすためにあえて 1 つのテンプレートから 100 ファイル生成するようにしています。

#ディレクトリ構成

[前回の記事](https://qiita.com/tatsugogo55/items/2216dcbb5b399d8d8b59)から追加で修正するテンプレートファイルは以下の２つになります。

- pub/sub 用のテンプレート ( pubsub.tf.j2 ) を新規で追加
- dev.yml, stg.yml, prd.yml のベースとなるテンプレート ( env.yml.j2 ) の追記

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

```
.
├── 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
│           ├── pubsub-001.tf
│           ├── pubsub-002.tf
│           ├──  ........
│           └── pubsub-100.tf
├── stg
│   └── gcp
│       └── terraform (stg.ymlを実行すると生成される設定)
│           ├── cloudsql.tf
│           ├── credential.json
│           ├── providor.tf
│           ├── pubsub-001.tf
│           ├── pubsub-002.tf
│           ├──  ........
│           └── pubsub-100.tf
├── prd
│   └── gcp
│       └── terraform (prd.ymlを実行すると生成される設定)
│           ├── cloudsql.tf
│           ├── credential.json
│           ├── providor.tf
│           ├── pubsub-001.tf
│           ├── pubsub-002.tf
│           ├──  ........
│           └── pubsub-100.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 を使うための設定)
                └── pubsub.tf.j2   (Terraform で pubsub を生成するための設定)
```


# インストール
後で yq, datamash, parallel というツールを使いますので先に入れておきましょう。

- yq は Ansible Playbook の YAML ファイルからタグの属性を抽出するために使用します。
- datamash は CSV の行と列を入れ替えるために使用します。
- parallel は Ansible Playbook を並列実行するために使用します。

Mac の場合は以下でサクッと入ります。

```
$ brew install yq datamash parallel
```

# Terraform の設定を生成するためのテンプレートの作成
Terraform のテンプレートは以下になります。指定の場所に配置してください。

```template/gcp/terraform/share/pubsub.tf.j2
resource "google_pubsub_topic" "sample_topic_{{index}}" {
  name = "sample_topic_{{index}}"
}

resource "google_pubsub_subscription" "sample_subscription_{{index}}" {
  name  = "sample_subscription_{{index}}"
  topic = "sample_topic_{{index}}"
}
```



# Terraform の設定を生成するための Ansible のタスクを追加

Ansible Playbook のテンプレートに pub/sub を生成するタスクを追加します。
以下のコマンドで追加します。

```
$ for i in `seq -w 1 100` ; do
cat << EOF
    - name: {{env}}/gcp/terraform/pubsub-${i}.tf
      tags: {{env}}/gcp/terraform/pubsub-${i}.tf
      template:
        src: ./template/gcp/terraform/share/pubsub.tf.j2
        dest: ./{{env}}/gcp/terraform/pubsub-${i}.tf
      vars:
        index: ${i}

EOF
done >> ./template/env.yml.j2
```

追加後は env.yml.j2 に以下のように追記されているハズです。

```template/env.yml.j2
 ... 省略 ...
    - name: {{env}}/gcp/terraform/pubsub-001.tf
      tags: {{env}}/gcp/terraform/pubsub-001.tf
      template:
        src: ./template/gcp/terraform/share/pubsub.tf.j2
        dest: ./{{env}}/gcp/terraform/pubsub-001.tf
      vars:
        index: 001

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

 ... 省略 ...
```

# Ansible Playbook の生成

Ansible Playbook の pre.yml を実行して各環境の Playbook (dev.yml, stg.yml, prd.yml) を生成します。

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

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

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

# Ansible で Terraform の設定を 1 つずつ生成

それでは Ansible Playbook の dev.yml を実行して Terraform の設定ファイルを 1 つずつ生成してみます。参考までに私のノートPCのスペックも記載しておきます。

```
MacBook Pro 13-inch 2017
CPU    : 2.3GHzデュアルコアIntel Core i5
Memory : 16 GB
```

Playbook を実行して dev 環境の設定を生成してみましたが 100 ファイル生成するのに 43 秒ほどかかっています。少し短縮したいところですね。

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

TASK [dev/gcp/terraform/pubsub-001.tf] ****************************************************************************
ok: [localhost]

.....

TASK [dev/gcp/terraform/pubsub-100.tf] ****************************************************************************
ok: [localhost]

PLAY RECAP ********************************************************************************************************
localhost                  : ok=103  changed=0    unreachable=0    failed=0

30.18s user 11.98s system 97% cpu 43.449 total
```

# Ansible で Terraform の設定を 4 並列で生成

それでは先程実行したタスクを Ansible のタグの機能を使って分割実行してみます。
若干ややこしいのですが流れとしては以下のとおりです。

1. Ansible の Playbook からタグを抽出
2. タグをカンマ区切りで4つ並べたら改行する (4 列の CSV に変換する)
3. 行と列を入れ替える (4行の CSV ファイルに変換する)
4. CSVファイルを1行ずつ ansible のコマンドに引数に入れたものを parallel コマンドで並列実行する

上記の説明では分かりづらいので実際にコマンドを実行して確認しつつ進めます。

## Ansible の Playbook からタグを抽出

最初にインストールした yq コマンドを使って tag を抽出します。

```
$ yq r dev.yaml [0].tasks[*].tags
- dev/gcp/terraform/credential.tf
- dev/gcp/terraform/providor.tf
- dev/gcp/terraform/cloudsql.tf
- dev/gcp/terraform/pubsub-001.tf
- dev/gcp/terraform/pubsub-002.tf
... 省略 ...
```

次に先頭のハイフンを取り除きます。(この辺のやり方は何でもいいのでお好みの方法でどうぞ)

```
$ yq r dev.yml [0].tasks[*].tags | grep pubsub | perl -pe 's/^- (.*)/$1/'
dev/gcp/terraform/pubsub-001.tf
dev/gcp/terraform/pubsub-002.tf
... 省略 ...
```

## タグをカンマ区切りで4つ並べたら改行する (4 列の CSV に変換する)

ワンライナーの中身については触れませんが以下を実行するとこんな感じで分割さます。

```
$ yq r dev.yml [0].tasks[*].tags | grep pubsub | perl -pe 's/^- (.*)/$1/' |\
  perl -pe "s/\n$/,/ if $.%4"
dev/gcp/terraform/pubsub-001.tf,dev/gcp/terraform/pubsub-002.tf,dev/gcp/terraform/pubsub-003.tf,dev/gcp/terraform/pubsub-004.tf
dev/gcp/terraform/pubsub-005.tf,dev/gcp/terraform/pubsub-006.tf,dev/gcp/terraform/pubsub-007.tf,dev/gcp/terraform/pubsub-008.tf
dev/gcp/terraform/pubsub-009.tf,dev/gcp/terraform/pubsub-010.tf,dev/gcp/terraform/pubsub-011.tf,dev/gcp/terraform/pubsub-012.tf
dev/gcp/terraform/pubsub-013.tf,dev/gcp/terraform/pubsub-014.tf,dev/gcp/terraform/pubsub-015.tf,dev/gcp/terraform/pubsub-016.tf
dev/gcp/terraform/pubsub-017.tf,dev/gcp/terraform/pubsub-018.tf,dev/gcp/terraform/pubsub-019.tf,dev/gcp/terraform/pubsub-020.tf
... 省略 ...
```

## 行と列を入れ替える (4行の CSV ファイルに変換する)

最終的に 4 並列でコマンドを実行したいので全部で、これから 4 行のテキストにします。
今、出力されている CSV のテキストは 4 列なので行と列を入れ替えて 4 行のテキストに変換します。
CSV の行と列を入れ替える場合は　datamash というツールが便利なのでこれを使います。
あと、行末のカンマは不要なので削除してしまいます。

実行すると以下のように出力されます。

```
$ yq r dev.yml [0].tasks[*].tags | grep pubsub | perl -pe 's/^- (.*)/$1/' |\
  perl -pe "s/\n$/,/ if $.%4" |\
  datamash --no-strict -t ',' transpose |\
  sed -e 's/,$//'
dev/gcp/terraform/pubsub-001.tf,dev/gcp/terraform/pubsub-005.tf,dev/gcp/terraform/pubsub-009.tf,dev/gcp/terraform/ ...(省略)...
dev/gcp/terraform/pubsub-002.tf,dev/gcp/terraform/pubsub-006.tf,dev/gcp/terraform/pubsub-010.tf,dev/gcp/terraform/ ...(省略)...
dev/gcp/terraform/pubsub-003.tf,dev/gcp/terraform/pubsub-007.tf,dev/gcp/terraform/pubsub-011.tf,dev/gcp/terraform/ ...(省略)...
dev/gcp/terraform/pubsub-004.tf,dev/gcp/terraform/pubsub-008.tf,dev/gcp/terraform/pubsub-012.tf,dev/gcp/terraform/ ...(省略)...
... 省略 ...
```

最後に抽出した４行のタグのリストを parallel コマンドの引数に入れて 4 並列で実行します。
約 24 秒程度かかっていますね。

```
$ time (\
  yq r dev.yml [0].tasks[*].tags | grep pubsub | perl -pe 's/^- (.*)/$1/' |\
  perl -pe "s/\n$/,/ if $.%4" |\
  datamash --no-strict -t ',' transpose |\
  sed -e 's/,$//' |\
  parallel -u -P 4 ansible-playbook -t {} dev.yml )

50.00s user 18.36s system 288% cpu 23.688 total
```

# まとめ

Ansible の実行時間を並列実行の有無で比較しますと概ね半分くらいの時間で処理が終わっていました。
今回、実行したマシンのコア数は 2 コアなので概ね期待通り高速化できています。
本来、Ansible のタグの機能は特定のタスクのみ実行する用途で使うために存在するのですが、今回のようにタスクの依存関係などが無い場合は Playbook の分割をしなくてもタスクを並列実行して高速化することができます。もしよければ参考にしてみてください。

並列実行しない場合

```
30.18s user 11.98s system 97% cpu 43.449 total
```

並列実行した場合

```
50.00s user 18.36s system 288% cpu 23.688 total
```

以上です。