Posted at
AnsibleDay 15

Testinfra+GitLab-CIで、「テストされたAnsibleロール」を作ってみる

More than 1 year has passed since last update.

※少し前に、実戦投入目標で検証してたやつの振り返り用です。

サーバーのミドルウェア構成なんかを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拡張もインストールできるようにする


流れ



  1. ansible-galaxyでロールのソース一式を新規作成

  2. ansible反映先となるコンテナ用のDockerイメージを準備


  3. .gitlab-ci.ymlを用意して、ansible-playbook+testinfraが通るようにする

  4. 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サーバー」なイメージを用意します。


attakei/ansible-target

FROM jdeathe/centos-ssh:centos-7

# 「rootでログインはしない」「`yum install`は使う」のために設定
ADD sudoers /etc/sudoers.d/app-admin



3. .gitlab-ci.ymlを用意して、ansible-playbook+testinfraが通るようにする


.gitlab-ci.yml

---

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+テストコードを書きつつ、ロールを実装していきます。


.gitlab-ci.yml

# (一部略)

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



test_install_default.yml

---

- hosts: targets
roles:
- role: .


test_install_default.py

# -*- 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を用意することでローカルでも動作確認が使えるのが大きいです。


terminail

❯ 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


当然ながらテストに失敗します。


タスクを実装


tasks/main.yml

- 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にタスクを追記して、再度実行しましょう。


terminal

!!

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-pipeline.png


いい点


  • GitLab-CIが勝手にテストしてくれるので、「テストコードが正しくテストしてくれる」ことを前提に第三者に「これは正しく動く」ということを客観的に示すことができる

  • Ansibleロールとテストコードが同梱できるので、他の人も検証がしやすい


難点


  • アプリケーションレベルのテストコードにあるようなsetUp/tearDownが使いづらいので、各テストごとにコンテナを起動する必要がある

  • Shared Runnerしか使わない場合はリソースの取り合いになるので、更にテスト完了が遅くなりがち

  • デーモン起動系のテストが動かしづらい


まとめ

「このロールは少なくともこの部分を保証してくれる」というのがわかるのは、それだけでも大きいかなと。

そして「テストされたロール」を軸にPlaybookを組み立てることで、少ないリソースでも「ある程度品質が保証されたPlaybook」を作ることも可能かなと思いました。


参考