- 導入
- おことわり
- 前提
- 案1: いい感じのジョブテンプレートプロジェクトを提供し、それを参照してもらう
- 案2: ジョブ生成ツールを開発してしまう
- 案3: サンプルを真似してもらう
- あとがき: 皆さんどうしてます?
導入
こんにちは。市橋です。
弊社は IaC が全体的に用いられています。インフラ構築は Terraform 、インフラ設定は Ansible がデファクトスタンダードになっています。
ところで、IaC をやっているならば、当然コードに対して lint をかけたり、コードを元にデプロイしたり……といった作業を CI でやりたくなりますよね。
しかし、現状では各プロジェクトごとに CI ジョブを定義しています。どうせどこもやること大体 (重要) 同じなのに……
というわけで、これらを共通化したいい感じのジョブテンプレートとはどんなものなのかについて検討してみようと思います。
おことわり
この記事は何も解決しません。どうすればよいのかわからないというお悩み公開記事です。何も期待しないでください。でも IaC やっていくためには割と大事だと思う。
前提
- CI ツールは GitLab CI/CD だけ想定します
- 社内向けに構築した GitLab だけで利用します
- Terraform については以下を行うジョブを手軽に定義できるようにします
- 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_ARGS
は terraform init
を行う際の -backend-config
などをしていする際に利用する環境変数です。
TERRAFORM_VALIDATE_ARGS
は terraform 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
はあまり意味を感じません。 - これは、環境ごとに利用する秘密鍵を変更したい、利用するプレイブックを変更したい、ユーザー名を変更したい、などの需要を想定した結果こうなっています。
- たしかに 引数をすべて指定できれば柔軟性は確保できますが、ならば自分で
scripts
にansible-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 はサンプル開発側の手間は小さく、あらゆる状況に対応できますが、手間の削減効果は小さそうです。
というわけで、身も蓋もない感じの 案3 が出てしまい、身の回りでは「案3が無難じゃないか」という方向になっています。
でもこれが最適解とは個人的には思えません。もっとうまいやり方があるように思います。
そこで、世の IaC を実践しており、 CI で静的解析からデプロイまで行っている皆さんに質問です。
インフラの CI/CD パイプラインの構築、どうやってやってますか?