※少し前に、実戦投入目標で検証してたやつの振り返り用です。
サーバーのミドルウェア構成なんかをAnsibleで管理する機会がだいぶ増えてきました。
で、趣味ベースで「Galaxyには登録しないけど、ある程度内容の正しさを担保してあるロール」というのを作ってみたくなり、こんな組み合わせを構築してみました。
- Ansibleのロールのプロジェクトを作る
- pushするたびに、GitLab-CIする
- 一緒に起動したCentOSコンテナにAnsibleロールの反映
- その場で、testinfraを使って、CentOSコンテナ上のテスト
登場人物整理
- Ansible
- おなじみ構成管理ツール。
- GitLab.com, GitLab-CI
- おなじみOSSなGitHubライクサービス。GitLab-CIが何の手続きもなく使えるのがGood
- testinfra
- Ansibleと並ぶ今回の主役。インフラ系のテストをpy.testの仕組み上で出来るようにしたもの
お題
CentOS7のサーバーに対して、PHP7.1(mod_php)を入れるロール
オプションで以下の様なことができるようになること。
- remi-phpXXのバージョンを指定したら、そのバージョンのPHPをインストールできるようにする
- 選択式で、php-fpmをインストールできるようにする
- 追加のphp拡張もインストールできるようにする
流れ
-
ansible-galaxy
でロールのソース一式を新規作成 - ansible反映先となるコンテナ用のDockerイメージを準備
-
.gitlab-ci.yml
を用意して、ansible-playbook
+testinfra
が通るようにする - testinfraのテストコードを書きつつ、ロールを実装
作ってみる
1. ansible-galaxy
でロールのソース一式を新規作成
ansible-galaxy
コマンドにはinit
というサブコマンドがあり、defaults
, handlers
, meta
, tasks
, tests
, vars
といったロールに必要なフォルダとファイルの一式を用意してくれます。--offline
オプションを使えば、AnsibleGalaxyも見にいかないのでお手軽です。
今回は、.travis.ymlの存在は忘れてください。
testsフォルダ
デフォルトで作成されるtests
フォルダ内には、シンタクスチェックのみを目的としたインベントリとplaybookファイルがあります。
2. ansible反映先となるコンテナ用のDockerイメージを準備
GitLab-CIではservices
内にDockerイメージを記載することで、CI実行中にテスト実行用のコンテナとは別にコンテナを動かすことが出来ます。
今回はお題の条件をみたすために、「CentOS 7 + SSHサーバー」なイメージを用意します。
FROM jdeathe/centos-ssh:centos-7
# 「rootでログインはしない」「`yum install`は使う」のために設定
ADD sudoers /etc/sudoers.d/app-admin
3. .gitlab-ci.yml
を用意して、ansible-playbook
+testinfra
が通るようにする
---
image: williamyeh/ansible:alpine3
stages:
- lint
- test
services:
- attakei/ansible-target:centos-7-latest
before_script:
- cd tests
- cp hosts/insecure_private_key ~/
- chmod 400 ~/insecure_private_key
syntax_check:
stage: lint
script:
- ansible-playbook test.yml --syntax-check
ここまでがtestinfraの準備段階のgitlab-ci.ymlです。
AnsibleのシンタックスチェックとGitLab-CIの仕組みを利用して、文法ミスを見つけたらNGを出すようにしておくと効率が上がると思います。
4. testinfraのテストコードを書きつつ、ロールを実装
ここからテスト用にPlaybook+テストコードを書きつつ、ロールを実装していきます。
# (一部略)
before_script:
- pip install -q testinfra
# (抜粋)
test_install_default:
stage: test
script:
- ansible-playbook test_install_default.yml
- py.test --ssh-config=hosts/ssh_config --hosts=server test_install_default.py
---
- hosts: targets
roles:
- role: .
# -*- coding:utf8 -*-
def test_has_dependencies(host):
assert host.package("epel-release").is_installed
assert host.package("remi-release").is_installed
def test_installed_php(host):
assert host.package('php').is_installed
php_version = host.check_output("php -v")
assert 'PHP 7.1' in php_version
.gitlab-ci.yml
上にテスト用のジョブを用意していきます。
その際に、ジョブ名とジョブ実行時に使うPlaybookとそれを検証するテストファイルを同じ名前にしておくとわかりやすいです。
ここでテストを実行する
GitLab-CIを使う場合だと、gitlab-runnerを用意することでローカルでも動作確認が使えるのが大きいです。
❯ gitlab-runner exec docker test_install_default
Running with gitlab-ci-multi-runner 9.0.2 (fa8b86d3)
on ()
(略)
$ chmod 400 ~/insecure_private_key
$ ansible-playbook test_install_default.yml
PLAY [targets] *****************************************************************
TASK [Gathering Facts] *********************************************************
ok: [server]
PLAY RECAP *********************************************************************
server : ok=1 changed=0 unreachable=0 failed=0
$ py.test --ssh-config=hosts/ssh_config --hosts=server test_install_default.py
============================= test session starts ==============================
platform linux2 -- Python 2.7.13, pytest-3.3.1, py-1.5.2, pluggy-0.6.0
rootdir: /builds/project-0/tests, inifile:
plugins: testinfra-1.10.1
collected 2 items
test_install_default.py FF [100%]
=================================== FAILURES ===================================
___________________ test_has_dependencies[paramiko://server] ___________________
host = <testinfra.host.Host object at 0x7f75605f8550>
def test_has_dependencies(host):
assert host.package("epel-release").is_installed
> assert host.package("remi-release").is_installed
E AssertionError: assert False
E + where False = <package remi-release>.is_installed
E + where <package remi-release> = <class 'testinfra.modules.base.RpmPackage'>('remi-release')
E + where <class 'testinfra.modules.base.RpmPackage'> = <testinfra.host.Host object at 0x7f75605f8550>.package
test_install_default.py:6: AssertionError
------------------------------ Captured log call -------------------------------
(略)
=========================== 2 failed in 0.86 seconds ===========================
Running after script...
ERROR: Job failed: exit code 1
FATAL: exit code 1
当然ながらテストに失敗します。
タスクを実装
- name: Install EPEL(dependencies)
become: yes
yum: name=epel-release
- name: repo's package
become: yes
get_url:
url: https://rpms.remirepo.net/enterprise/remi-release-7.rpm
dest: /root/remi-release.rpm
- name: Install REMI repo
become: yes
yum: name=/root/remi-release.rpm
- name: Install php and basic packages
become: yes
yum: name={{ item }} enablerepo=remi,remi-php71
with_items:
- php
tasks/main.ymlにタスクを追記して、再度実行しましょう。
❯ !!
gitlab-runner exec docker test_install_default
Running with gitlab-ci-multi-runner 9.0.2 (fa8b86d3)
(略)
$ ansible-playbook test_install_default.yml
PLAY [targets] *****************************************************************
TASK [Gathering Facts] *********************************************************
ok: [server]
TASK [. : Install EPEL(dependencies)] ******************************************
ok: [server]
TASK [. : repo's package] ******************************************************
changed: [server]
TASK [. : Install REMI repo] ***************************************************
changed: [server]
TASK [. : Install php and basic packages] **************************************
changed: [server] => (item=[u'php'])
PLAY RECAP *********************************************************************
server : ok=5 changed=3 unreachable=0 failed=0
$ py.test --ssh-config=hosts/ssh_config --hosts=server test_install_default.py
============================= test session starts ==============================
platform linux2 -- Python 2.7.13, pytest-3.3.1, py-1.5.2, pluggy-0.6.0
rootdir: /builds/project-0/tests, inifile:
plugins: testinfra-1.10.1
collected 2 items
test_install_default.py .. [100%]
=========================== 2 passed in 1.00 seconds ===========================
Running after script...
Job succeeded
無事にテストが通りました。
実装を進める
テストケースとテスト用Playbookを増やしながら実装をしていきましょう。
pipelineもすべてがグリーンです。
いい点
- GitLab-CIが勝手にテストしてくれるので、「テストコードが正しくテストしてくれる」ことを前提に第三者に「これは正しく動く」ということを客観的に示すことができる
- Ansibleロールとテストコードが同梱できるので、他の人も検証がしやすい
難点
- アプリケーションレベルのテストコードにあるようなsetUp/tearDownが使いづらいので、各テストごとにコンテナを起動する必要がある
- Shared Runnerしか使わない場合はリソースの取り合いになるので、更にテスト完了が遅くなりがち
- デーモン起動系のテストが動かしづらい
まとめ
「このロールは少なくともこの部分を保証してくれる」というのがわかるのは、それだけでも大きいかなと。
そして「テストされたロール」を軸にPlaybookを組み立てることで、少ないリソースでも「ある程度品質が保証されたPlaybook」を作ることも可能かなと思いました。