Terraform と Ansible の CI/CD を効率化したかった

導入

こんにちは。市橋です。

弊社は IaC が全体的に用いられています。インフラ構築は Terraform 、インフラ設定は Ansible がデファクトスタンダードになっています。

ところで、IaC をやっているならば、当然コードに対して lint をかけたり、コードを元にデプロイしたり……といった作業を CI でやりたくなりますよね。
しかし、現状では各プロジェクトごとに CI ジョブを定義しています。どうせどこもやること大体 (重要) 同じなのに……

というわけで、これらを共通化したいい感じのジョブテンプレートとはどんなものなのかについて検討してみようと思います。

おことわり

この記事は何も解決しません。どうすればよいのかわからないというお悩み公開記事です。何も期待しないでください。でも IaC やっていくためには割と大事だと思う。

前提

  • CI ツールは GitLab CI/CD だけ想定します
  • 社内向けに構築した GitLab だけで利用します
  • Terraform については以下を行うジョブを手軽に定義できるようにします
    • terraform fmt
    • terraform validate
    • tflint
    • tfsec
    • terraform plan
    • terraform apply
    • スケジュール実行用 terraform plan
  • Ansible については以下を行うジョブを手軽に定義できるようにします
    • molecule lint
    • molecule test
    • ansible-playbook --check
    • ansible-playbook
    • スケジュール実行用 ansible-playbook

案1: いい感じのジョブテンプレートプロジェクトを提供し、それを参照してもらう

この案の方針

殆どの IaC プロジェクトは、前提にて述べたようなジョブパイプラインを定義するはずです。なので、それらをかんたんに定義できるようにテンプレートを提供します。

GitLab CI/CD の include には、他のプロジェクトからファイルをインクルードする機能 があるため、
これをつかってテンプレートを集約したプロジェクトからジョブテンプレートを参照してもらう戦略です。

これにより、同じようなジョブをいちから作る手間が省かれるはずです。

Terraform

terraform 関連コマンドの実行ディレクトリ指定

terraform のディレクトリ構成はある程度バリエーションがありますが、
ここでは役割ごとにある程度ディレクトリ分割されているようなケースを想定します。

例として、以下のようなディレクトリ構造があります。

/ --- production
    +-- network
    +-- iam
    +-- instance
    +-- storage
    +-- log
    +-- share
  +-- test
    +-- network
    +-- iam
    +-- instance
    +-- storage
    +-- log
    +-- share
  +-- share

ここで、 share は、各ディレクトリで共有するファイル (.terraform-version や provider 、変数 などの定義ファイル) とします。
また、環境分離は workspace ではなくディレクトリで行っているとします。

前提にて述べた terraform 関連ツールを実行したいディレクトリは、実際にリソースの作成を行うディレクトリだけです。そこで、実行対象ディレクトリを何らかの方法で指定する必要があります。
また、今回のような環境でディレクトリが分かれるような構造の場合、環境ごとに実行対象ディレクトリを指定したくなります。

よって、以下のように、環境ごとにディレクトリリストを定義した yaml を用意することとします。

production:
  - production/network
  - production/iam
  - production/instance
  - production/storage
  - production/log
test:
  - test/network
  - test/iam
  - test/instance
  - test/storage
  - test/log

この yaml ファイルはリポジトリルートに terraform-dir-structure.yml ファイルとして定義し、 CI/CD テンプレートが自動的に読み込み、利用するものとします。

CI/CD ステージ

以下のようなステージを予め定義しておき、 include して利用します。

stages:
  - terraform_fmt         # terraform fmt コマンドを各ディレクトリに実行する
  - terraform_validate    # terraform validate コマンドを各ディレクトリに実行する
  - terraform_tflint      # tflint コマンドを各ディレクトリに実行する
  - terraform_tfsec       # tfsec コマンドを各ディレクトリに実行する
  - terraform_plan        # terraform plan コマンドを各ディレクトリに実行する
  - terraform_apply       # terraform apply コマンドを各ディレクトリに実行する。直前の plan 結果を適用する
  - terraform_drift_check # スケジュール実行を前提とした、 terraform plan による構成ドリフトチェック

テンプレートを利用した各ステージのジョブ定義方法

これ以降の説明では、適切なジョブテンプレートはもう定義されているものとします。

terraform_fmt

TERRAFORM_ENVIRONMENT にて指定した環境に登録されているディレクトリに対して、 terraform fmt -recursive -check を実行します。

利用方法の例は以下のようになります。

production_terraform_fmt:
  extends: .terraform_fmt_template
  variables:
    TERRAFORM_ENVIRONMENT: production
terraform_validate

TERRAFORM_ENVIRONMENT にて指定した環境に登録されているディレクトリに対して、 terraform fmt -recursive -check を実行します。

利用方法の例は以下のようになります。

production_terraform_validate:
  extends: .terraform_validate_template
  variables:
    TERRAFORM_ENV: production
    TERRAFORM_INIT_ARGS: -backend-config ${CI_BUILDS_DIR}/production/share/backend_config.tfvars
    TERRAFORM_VALIDATE_ARGS: -var-file ${CI_BUILDS_DIR}/production/share/default_vars.tfvars

TERRAFORM_INIT_ARGSterraform init を行う際の -backend-config などをしていする際に利用する環境変数です。

TERRAFORM_VALIDATE_ARGSterraform validate を行う際の -var-file などを指定する際に利用する環境変数です。

terraform_tflint

TERRAFORM_ENVIRONMENT にて指定した環境に登録されているディレクトリに対して、 tflint を実行します。

production_terraform_tflint:
  extends: .terraform_tflint_template
  variables:
    TERRAFORM_ENVIRONMENT: production
    TERRAFORM_TFLINT_ARGS: -var-file ${CI_BUILDS_DIR}/production/share/default_vars.tfvars

TERRAFORM_TFLINT_ARGS は、 tflint を行う際の -var-file などを指定する際に利用する環境変数です。

terraform_tfsec

TERRAFORM_ENVIRONMENT にて指定した環境に登録されているディレクトリに対して、 tfsec を実行します。

production_terraform_tfsec:
  extends: .terraform_tfsec_template
  variables:
    TERRAFORM_ENVIRONMENT: production
terraform_plan

TERRAFORM_ENVIRONMENT にて指定した環境に登録されているディレクトリに対して、 terraform plan を実行します。 また、 plan 結果を artifact として apply に受け渡せるようにします。

production_terraform_plan:
  extends: .terraform_plan_template
  variables:
    TERRAFORM_ENVIRONMENT: production
    TERRAFORM_INIT_ARGS: -backend-config ${CI_BUILDS_DIR}/production/share/backend_config.tfvars
    TERRAFORM_PLAN_ARGS: -var-file ${CI_BUILDS_DIR}/production/share/default_vars.tfvars
terraform_apply

TERRAFORM_ENVIRONMENT にて指定した環境に登録されているディレクトリに対して、 terraform apply を実行します。 plan 結果は直前の plan ジョブが生成した artifact から取得します。

production_terraform_apply:
  extends: .terraform_apply_template
  needs:
    - job: production_terraform_plan
      artifacts: true
  variables:
    TERRAFORM_ENVIRONMENT: production
    TERRAFORM_INIT_ARGS: -backend-config ${CI_BUILDS_DIR}/production/share/backend_config.tfvars
    TERRAFORM_APPLY_ARGS: -var-file ${CI_BUILDS_DIR}/production/share/default_vars.tfvars
terraform_drift_check

TERRAFORM_ENVIRONMENT にて指定した環境に登録されているディレクトリに対して、 terraform plan -detailed-exitcode を実行します。

このジョブはスケジュールパイプラインにて実行される想定です。

production_terraform_drift_check:
  extends: .terraform_drift_check_template
  variables:
    TERRAFORM_ENVIRONMENT: production
    TERRAFORM_INIT_ARGS: -backend-config ${CI_BUILDS_DIR}/production/share/backend_config.tfvars
    TERRAFORM_DRIFT_CHECK_ARGS: -var-file ${CI_BUILDS_DIR}/production/share/default_vars.tfvars

Ansible

想定するディレクトリ構造

以下のようなディレクトリ構造とします。いわゆる Ansible のベストプラクティス構成に、 molecule ディレクトリが追加されたものです。

/ --- molecule/
  +-- group_vars/
  +-- inventory/
  +-- roles/
  +-- site.yml

他にも色々ありますが、上記のものが最低限あると想定します。

CI/CD ステージ

以下のようなステージを予め定義しておき、 include して利用します。

stages:
  - molecule_lint       # molecule lint コマンドを各シナリオについて実行する
  - molecule_test       # molecule test コマンドを各シナリオについて実行する
  - ansible_dryrun      # ansible-playbook --check --diff コマンドを各プレイブックについて実行する
  - ansible_deploy      # ansible-playbook --diff コマンドを各プレイブックについて実行する
  - ansible_drift_check # スケジュール実行を前提とした、 ansible-playbook --check --diff による構成ドリフトチェック

テンプレートを利用した各ステージのジョブ定義方法

molecule_lint

molecule ディレクトリに存在する各シナリオに molecule lint -s <scenario> を実行します。

molecule_lint:
  extends: .molecule_lint_template
molecule_test

molecule ディレクトリに存在する各シナリオに molecule test -s <scenario> を実行します。

molecule_test:
  extends: .molecule_test_template
ansible_dryrun

ANSIBLE_CHECK_ARGS にて指定された引数で、 ansible-playbook --check --diff を実行します。

test_ansible_dryrun:
  extends: .ansible_dryrun_template
  variables:
    ANSIBLE_CHECK_ARGS: -i inventory/test/ -u centos site.yml
ansible_deploy

ANSIBLE_DEPLOY_ARGS にて指定された引数で、 ansible-playbook --diff を実行します。

test_ansible_deploy:
  extends: .ansible_deploy_template
  variables:
    ANSIBLE_DEPLOY_ARGS: -i inventory/test/ -u centos site.yml
ansible_drict_check

ANSIBLE_DRIFT_CHECK_ARGS にて指定された引数で、 ansible-playbook --check -diff を実行します。また、実行結果の changed が 0 でなかった場合にジョブが fail します。

このジョブはスケジュールパイプラインにて実行される想定です。

test_ansible_dryrun:
  extends: .ansible_drift_check_template
  variables:
    ANSIBLE_DRIFT_CHECK_ARGS: -i inventories/test/ -u centos site.yml

案1 感想

CI のステージに合わせたジョブを定義していくだけで、適切なパイプラインが構築できる構成になりました。
詳細をすべてテンプレートによって提供するので、実装の手間が大幅に削減できそうです。

しかし以下のような問題がありそうです。

  • 環境分ジョブ定義が必要
    • 今回の例なら production と test 分必要
    • 環境数 * 環境ごとのジョブ数 分のジョブ定義はまあまあめんどくさそうです
  • terraform の想定構成から外れた場合の適応能力
    • workspace を利用する場合に適応できません。その場合は workspace 対応版テンプレートが必要です。
    • ただ、workspace を使わないとしてしまえば問題はないです
  • ansible のジョブテンプレートの効果の低さ
    • 正直、 ansible_dryrun, ansible_deploy, ansible_drift_check はあまり意味を感じません。
    • これは、環境ごとに利用する秘密鍵を変更したい、利用するプレイブックを変更したい、ユーザー名を変更したい、などの需要を想定した結果こうなっています。
    • たしかに 引数をすべて指定できれば柔軟性は確保できますが、ならば自分で scriptsansible-playbook <各種引数> というコマンドを書いても大して変わりません。
    • さらに、 AWS リソースにアクセスするためのプロファイルの設定や、踏み台経由で ssh する際の設定などもすべてユーザー任せです。
    • molecule も、一部シナリオの除外や、シナリオごとの実行ができません。除外は ignore ファイルを定義すれば対応できそうですが……
    • 総じて、あまり効果を感じません。

案2: ジョブ生成ツールを開発してしまう

案1の方法で、たしかに同じようなジョブの再定義は防がれました。

しかし、結局環境数分のジョブを定義しなければいけないことに変わりはありません。
また、molecule, ansible のジョブはほとんどテンプレートの恩恵を受けていないように思えます。

そこで、 molecule, ansible のジョブを自動生成するツールを開発することとしましょう。

molecule

想定する実行コマンド

molecule/ ディレクトリ以下で、以下のようなコマンドを実行することとします。

$ molecule-ci-job-gen

想定する実行結果

molecule/ は以下のような構造であるとします。

default/ hoge/ fuga/

すると、以下のような結果が標準出力で得られます。

molecule_lint_hoge:
  extends: .molecule_lint_template
  variables:
    MOLECULE_SCENARIO: hoge

molecule_lint_fuga:
  extends: .molecule_lint_template
  variables:
    MOLECULE_SCENARIO: fuga

molecule_test_hoge:
  extends: .molecule_test_template
  variables:
    MOLECULE_SCENARIO: hoge

molecule_test_fuga:
  extends: .molecule_test_template
  variables:
    MOLECULE_SCENARIO: fuga

これらのジョブは、 MOLECULE_SCENARIO で指定したシナリオに対し、 lint test を実行するものです。これを、必要なもののみユーザーが .gitlab-ci.yml に組み込むとします。

ansible

想定する実行コマンド

ansible-playbook コマンドに、設定ファイルにて指定した引数を自動で適用してくれるツールを作ることとします。名前を ansible-compose とします。

ansible-playbook を実行する場合は以下のようにします。

$ ansible-compose run hoge

CI ジョブを生成する場合は以下のようにします。

$ ansible-compose ci

想定する設定ファイル

ansible-compose は、実行ディレクトリにある ansible-compose.yml を読み込みます。
この ansible-compose.yml に、実行セット名と、その実行セットで ansible-playbook コマンドに与えるオプションや環境変数のリストを指定します。

例として、以下のような ansible-compose.yml があったとします。

production:
  playbook: site.yml
  private_key: ./production/private_key
  inventory: inventory/production/
  user: centos
  ssh_option: -o ProxyCommand="ssh -W %h:%p centos@xxx.yyy.zzz.www -i ./production/private_key"

この状態で、 ansible-compose run production を実行すると、 ANSIBLE_CONFIG=/tmp/production_ansible_config_auto_generated ansible-playbook -i inventory/production/ --private-key ./production/private_key -u centos を実行したことになります。

また、 ansible-compose ci を実行すると、以下のようなジョブ定義が生成されます。

production_ansible_check:
  extends: .ansible_check_template
  scripts:
    - ansible-compose check production

production_ansible_deploy:
  extends: .ansible_deploy_template
  scripts:
    - ansible-compose run production

production_ansible_drict_check:
  extends: .ansible_drift_check_template
  scripts:
    - ansible-compose check production

案2 感想

これらのツールがあれば、たしかにジョブの生成は楽になりそうです。
案 1 の恩恵の薄かった ansible を対象としたツールを考案しましたが、ジョブ生成部分については terraform にもあって良いかもしれません。

しかし、これを誰が開発、保守し、どうやって使い方を広めるのかという問題があります。 ジョブを定義するめんどくささを、ツールの利用法を覚えるめんどくささに転嫁しただけにも思えます。

案3: サンプルを真似してもらう

案2は案1にあった「結局ジョブを定義するところがめんどくさい」という問題を解決する案でした。

しかし、そのツールを誰が開発・保守するのかという問題が当然ながらあります。

また、案1の別の不安として、テンプレートが通用しない状況に出会ってしまったらいちから自分でジョブを定義するしかない、ということです。

正直なところ、 Terraform や Ansible の CI ジョブ定義はめんどくさいだけで、難しいことは何もないです。
多くの場合、 クラウドサービスの認証情報や、 ssh に関する設定をちゃんと行い、その上でジョブを作り込む、という点にあります。

ならば、それらがすでに行われているサンプルプロジェクトを作成し、必要な部分のみ改変してもらったほうが、平均的な作業量を減らせそうです。

あとがき: 皆さんどうしてます?

今回は、 Terraform と Ansible の CI/CD を効率化しようとしました。

結論は定まらず、以下の3つの案が出ました。

  1. テンプレートを使ってもらう
  2. ジョブ生成ツールを作る
  3. 参考になるサンプルを作る

1 は 手軽ですが、テンプレートが対応できない範囲については各自がジョブを作り込む必要がでてきます。

2 はよさそうですが開発・保守が大変そうです。そして実際作ってみてよくなかったら悲劇です。

3 はサンプル開発側の手間は小さく、あらゆる状況に対応できますが、手間の削減効果は小さそうです。

というわけで、身も蓋もない感じの 案3 が出てしまい、身の回りでは「案3が無難じゃないか」という方向になっています。
でもこれが最適解とは個人的には思えません。もっとうまいやり方があるように思います。

そこで、世の IaC を実践しており、 CI で静的解析からデプロイまで行っている皆さんに質問です。

インフラの CI/CD パイプラインの構築、どうやってやってますか?