この記事は、Ansible Advent Calendar 2018 の 7 日目の記事です。
ソフトウェアは変更に弱く壊れやすいものです。Ansible の Playbook も例外ではありません。本記事では Rust の生みの親である Graydon Hoare 氏のこちらの記事を参考に、Configuration as Code の開発プロセスに CI を導入した結果について述べます。
The Not Rocket Science Rule Of Software Engineering:
automatically maintain a repository of code that always passes all the tests
tl;dr
- Ansible における Role のテストフレームワークである Molecule はローカル/CI環境問わず Playbook のテストにも有用です。
- Ansible 関連のパッケージを Pipenv で管理することで、チーム内の開発環境等の差異を吸収できます。
- Anisble 関連のパッケージは bot を使って定期的に更新すると Toil が減ります。
- 複数人での開発を行う場合は自動マージ Bot を導入することで、upstream の安全性を高め、Toil を減らすことが出来ます。
Intro
アプリ開発における CI/CD の導入は"当たり前"になりつつあります。自動化による Toil の撲滅、コードの安全性向上、開発スピードの高速化など理由は様々あると思います。だったらインフラのコードでも CI/CD をまわしていきたい!という思いから、CI の部分を設計してみました。(CD は割と行われているイメージです。)
Ansible には Check Mode を使った Playbook の動作をシミュレートするモードがあります。しかし、実際に変更を適用する訳ではないので、実行結果に依存した条件式が組み込まれた Playbook や Check モードに非対応のモジュールを使った Playbook はテストすることができません。そのため、別途 Playbook を開発環境や検証環境に適用して動作確認する必要があります。ローカルやクラウド上に開発環境や検証環境を自前で用意するのは Toil であり、排除すべき対象です。
最近流行りの CI 環境は、使い捨てのコンテナ (一部 VM) が実行環境です。使い捨ての環境と Ansible の親和性は高く、CI の実行環境上にコンテナを構築して Playbook を適用するというアプローチを取りました。そして、実行環境上で適用対象のノードの管理からテストの実行まで一貫して行うことができる Molecule をテストフレームワークとして導入しました。
What's Molecule
Ansible のロールの開発やテストを補助する目的で設計されています。コンテナ、VM、クラウドなど様々なインスタンスを構築し、実際に変更を適用することでテストします。Molecule の内部では Ansible の Playbook を使って環境を構築しているため、Ansible が公式でサポートしているプロバイダーにほぼ対応しています。元は個人プロジェクトでしたが現在は Ansible (Red Hat) 配下のプロジェクトです。
今回は Docker と Vagrant ドライバーしか取り上げませんが、AWS / Azure / GCP / OpenStack に対応しているため、CD も簡単に設計できます。
Molecule を導入した Ansible 公式の example はこちらにあります。Molecule を実行している様子をキャプチャした demo を用意しています。開発中のロールに Molecule を導入したサンプルはこちらにあります。
以降は Molecule を導入した example playbook を使って説明していきます。
Overview of Testing
Molecule が行うテストの概要は以下の通りです。
-
- YAML ファイルを静的に解析します。
-
.yamllint
で定義したルールに沿わない場合、warning/error でテストが失敗します。
-
- Playbook を静的に解析し、ベストプラクティスに沿っているかを評価します。
-
CCIE 上のコンテナに対する変更の適用
- Molecule の
test
コマンドが実行され、コンテナに対して実際に変更を適用していきます。Molecule の設定ファイル (molecule.yml) 内で、DIFF_ALWAYS を有効にすれば、変更の差分が表示されます。 - 同時に冪等性チェック (
idempotence
) が行われ、状態がchanged
となるタスクがあればエラー終了します。
- Molecule の
-
サーバーの自動テスト
- デフォルトは Testinfra ですが、ServerSpec と InSpec と Goss から選択できます。
- Testinfra は Serverspec の Python 版みたいなものです。
- Flake8 による静的解析が行われるため、Python のお作法が分からなくてもリンターに怒られながらコードを書くことができます。
- 使わない選択肢もありですが、Molecule に同梱されています。
Testing Flow
Molecule のテストはシナリオ単位で複数管理することができます。各シナリオで対象の環境や Playbook を個別に定義でき、テストの流れ (シークエンス) を設定することができます。
シークエンスは create
、check
、converge
、destroy
、test
が用意されており、独自に実行するタスクを定義可能です。
例えば、デフォルトの test
シークエンスは以下のタスクを順に実行していきます。
-
lint
- デフォルトでは Yamllint, Flake8, Ansible Lint の順番でリンターが実行されます。
- Linter はそれぞれ変更可能です。
- destroy
- 変更を適用した環境が残っていれば破壊し、お掃除します。
- dependency
- requirements.yml に定義された Ansible Galaxy のロールをダウンロードします。
- syntax
- ansible-playbook コマンドを
--syntax-check
オプションで実行します。
- ansible-playbook コマンドを
- create
- 変更を適用する対象の環境を構築します。docker の場合、Dockerfile.j2 に従ってDocker コンテナをビルドして起動します。(Dockerfile はカスタマイズ可能です。)
- prepare
- converge を実行する前に環境を整えたい場合に使用します。
- converge
- 対象の環境に Playbook を適用します。
- idempotence
- Playbook を再度適用し、冪等性チェックを行います。
- side_effect
- verify
- Testinfra が実行されます。
- destroy
Directory Structure
molecule init scenario -r <role name> [-s <scenario name>]
で既存の role に Molecule のテストシナリオを追加することができます。シナリオ名を指定しない場合は、default シナリオが作成され、以下のファイルが role 配下のディレクトリに作成されます。
├── .yamllint <- 追加された Yamllint の設定ファイル
└── molecule <- 追加された Molecule 関連のボイラーテンプレート
└── default <- デフォルトのテストシナリオ
├── Dockerfile.j2 <- Dockerfile をカスタマイズする場合に使います
├── INSTALL.rst <- Docker ドライバーを使用する場合のインストール方法が書かれたドキュメント
├── molecule.yml <- Molecule の設定ファイル
├── playbook.yml <- Molecule で実行する Playbook を記述します
└── tests
├── test_default.py <- Testinfra で使用するコードを記述します
└── test_default.pyc <- 上記のコードをコンパイルした結果
Configuration
Molecule の設定はシナリオごとに管理します。
Depencency
Role の依存関係の管理に関する設定です。デフォルトの dependency マネージャは Ansible Galaxy です。その他に Gilt と Shell を選択できます。
dependency:
name: galaxy
Driver
Molecule のテストを適用する対象のインスタンスの種類を指定します。コンテナ、クラウド、VM など複数の選択肢があります。詳細はこちらを確認して下さい。シナリオ内で複数の Driver を選択することはできません。
driver:
name: docker
Platforms
テストを適用する対象のインスタンスの設定です。複数のインスタンスを指定することが可能です。name
と groups
が Ansible における Hosts と Groups に対応しています。
Driver で docker
を指定した場合は、docker_container モジュールのオプションを指定することができます。コンテナではセキュリティ上の理由からデフォルトで init や systemd を制御することができません。capabilities
と /sbin/init
を指定することでサービスの状態確認/起動/停止を可能にしています。特権モードを付与したい場合は、privileged: true
を指定すれば良いです。
platforms:
- name: instance-1
groups:
- dbservers
image: centos:6
capabilities:
- SYS_ADMIN
command: /sbin/init
- name: instance-2
groups:
- webservers
image: centos:6
capabilities:
- SYS_ADMIN
command: /sbin/init
Provisioner
インスタンスに対して変更を適用するツールを指定します。指定できる Provisioner は Ansible のみです。
inventory
を指定することで、group_vars ディレクトリをリンクさせることもできます。
inventory:
links:
group_vars: ../../group_vars
config_options
を指定することで、ansible.cfg の設定を記載可能です。
config_options:
defaults:
timeout: 80
forks: 50
hash_behaviour: merge
host_key_checking: false
retry_files_enabled: false
nocows: true
diff:
always: yes
lint
はデフォルトで Ansible-lint が指定されています。options:
で Ansible-lint の設定が可能です。適用ルールの表示を強制させています。
lint:
name: ansible-lint
options:
L: true
force-color: true
Scenario
シナリオのシークエンスを上書きすることができます。複数のシナリオを管理することはできないので注意して下さい。デフォルトのシークエンスは、こちらをご確認ください。
check シークエンスはデフォルトリントチェックが走らないので設定を上書きしています。
check_sequence:
- lint
- destroy
- dependency
- syntax
- create
- prepare
- check
- destroy
Verifier
インフラ自動テストのツールを指定します。TestInfra がデフォルトで指定されています。インフラ自動テストを使用していないので、特に設定していません。
verifier:
name: testinfra
lint:
name: flake8
Strategy for CI testing
こちらのサンプルでは、次の CI サービスと連携しています。
CircleCI
CircleCI の設定ファイルは .circleci/config.yml にあります。
Executor
Executor Type として machine
を使用しています。CircleCI の docker
実行環境 (Executor) は特権モードでコンテナを起動できないなど制限があるからです。詳細はこちらを確認下さい。
machine: true
-
machine
の構築にはこちらのスクリプトが使用されています。必要なパッケージやツールがプリインストールされているかを確認しましょう。-
スクリプトの中を見ると分かるのですが、Python の仮想環境はすでに構築されています。
PIPENV_VENV_IN_PROJECT
を環境変数として定義しているのは、working_direcory
配下に新たに.env
というディレクトリを作成するためです。後述しますが、この.env
をキャッシュとして保存しています。environment: PIPENV_VENV_IN_PROJECT: true
-
Working Directory
working_directory
を指定しているのは、デフォルトだと ~/project
という名前になってしまうからです。Ansible のロールを Playbook からインポートするこちらで不整合が起きないよう明示的に指定しています。
Caching Strategy
CircleCI のキャッシュ機能を用いることで、テストの実行時間を短縮しています。
-
Python の仮想環境 .venv 配下のライブラリ等をキャッシュとして保存しています。
- キャッシュはキーと値のペアです。
- キャッシュは CircleCI のプロジェクト内で共有されます。
- CircleCi ではキャッシュを手動で削除することはできません。
- そのため、
v1
という prefix をキーの名前に付与しています。異なるキャッシュを使いたい場合は、明示的にv2
という風にバージョンを増やして行きます。
- そのため、
- Pipfile.lock のチェックサムに変更がない場合はキャッシュを保存しません。
- キャッシュは 30 日間保管されます。
steps: (...) - save_cache: key: 'sample-v1-{{ checksum "Pipfile.lock" }}' paths: - .venv
-
キャッシュしたディレクトリを同一のパスにリストアしています。
steps: (...) - restore_cache: key: 'sample-v1-{{ checksum "Pipfile.lock" }}'
Ansible Installation
Ansible 関連のパッケージをインストールしています。pipenv sync
は Pipfile.lock に記載された依存関係をその通りにインストールします。Pipfile と Pipenv.lock の間に差異があったとしても Pipenv.lock に記載された依存関係に従います。
YAML Anchors and Aliases
YAML の機能である Anchor (&名前
) と Alias (*名前
) による参照を活用し、DRY 原則に基づいて、重複した run
を定義しています。Anchor や Alias の名前は大文字のスネークケースで表しています。
Other CI Platforms
CircleCI の設定をできるだけ再現していますが、未完成です。
Package Management
Pipenv の詳細は省きます。Pipenv を使うことで、Ansible に関連するパッケージのバージョンを管理し、チーム内のメンバーの開発環境に差異が生じないようにしています。
Setup Automatic Updates
Pipenv ファイルに記載されたパッケージのバージョンを手作業で更新するのは Toil となるため、パッケージの更新を Bot に任せます。依存関係のパッケージやライブラリの更新サービスは、Renovate や Dependabot などがあります。今回は Pipfile に対応している Dependabot を使います。(執筆時には対応中でしたが、Renovate も対応完了したようです。)
ちなみに、どちらのサービスもコア機能を OSS として公開されており、GHE や GitLab で利用するためのセルフホストが可能です。Renovate はこちら、Dependabot はこちらです。Dependabot の場合は、サンプルの Ruby コードがこちらにあります。
Dependabot をデプロイすることで、レポジトリ内の依存関係に更新があると自動的に Pipfile と Pipfile.lock の両方をアップデートしたこちらの PR を作成してくれます。
Automate Merge Flow
PR に対するレビュアーの設定や master への安全なマージを自動化するために bors-ng を導入しています。bors の歴史とセルフホスト可能な代替 bot の popuko の詳細はこちらの記事を参照して下さい。popuko を GHE で動作させるにはコードに一部手を加える必要があります。popuko が内部で利用している go-github に BaseURL を設定しなければなりません。詳細はこちらの Issue をご覧ください。
自動マージ bot の重要性 (マージ待ちの PR が溜まった状態の危険性) について書きたかったのですが、文量が多くなったので今回は省きます。
Local Testing
ローカル環境でテストを実行するには、Docker と Pipenv をインストールする必要があります。
Installation
macOS
$ brew cask install docker pipenv
アプリケーション から Docker を起動するのを忘れないで下さい。
Ubuntu 18.04
Ubuntu 16.04 -> 18.04 に上げており、Python 2.7 系が既にインストールされた状態を想定しています。
- 参考
$ sudo apt-get python-pip
$ pip install pipenv
$ sudo apt-get install \
apt-transport-https \
ca-certificates \
curl \
software-properties-common
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo apt-key fingerprint 0EBFCD88
$ sudo add-apt-repository \
"deb [arch=amd64] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) \
stable"
$ sudo apt-get update
$ apt-cache madison docker-ce
$ sudo apt-get install docker-ce=118.03.1~ce~3-0~ubuntu
$ sudo gpasswd -a $USER docker
$ newgrp docker
Usage
ローカル環境で Vagrant + VirtualBox でテストを実行する場合は、Vagrant と VirtualBox をインストールし、以下のコマンドを実行して下さい。
$ git clone https://github.com/toVersus/sample-role-for-ci-testing.git
$ cd ./sample-role-for-ci-testing
$ pipenv sync
$ pipenv run test-local
コンテナ特有のエラーに苦しみたくない場合に有効です。例えば、Ubuntu ホスト上の CentOS コンテナで httpd がインストールできない問題やコンテナの IP アドレスが取得できない問題などです。コンテナ特有の問題を Molecule でどう回避したか、需要があれば別記事に書きます。
Conclusion
Molecule は CI 環境でコンテナに対して Playbook を適用することで、upstream のコードの安全性を高め、Toil を削減することができます。また、ローカル環境で VM (Vagrant + VirtualBox) に対して Playbook を適用することで、コンテナ特有の問題に依存しないテストを行なえ、心理的安全性を更に高めることができます。インフラコードにも CI/CD を導入していき、常に動作保証されたコードを本番環境へデプロイしていきたいです。