Ansibleにおいてテストを行う理由

連載の第6回では、Ansibleのテストにおける考え方や方法論を解説していきます。
Ansibleにおいてテストは必要か
テストは、対象のプログラムが仕様通りに正しく動いていることの確認や、バグを発見するために必要な工程です。適切なテストをすることにより、対象物(システム)の品質を担保できます。では、Ansibleのような構成管理ツールではどうでしょうか。従来の構成管理の手法としてはシェルスクリプトなどを使い、ミドルウェアのインストールおよびサービスの起動を行っていました。シェルスクリプトはプログラムなので、仕様通りに構成管理が行われていることの確認としてテストをすることがあります。それに対してAnsibleでは、プログラムを使用せずにyaml形式のPlaybookという設定ファイルをユーザーが記述します。そして、そのPlaybookに基づいてAnsible内部のPythonで記述されたプログラムが実行され、構成管理が行われます。Ansibleは、このPlaybookに記述した通りにサーバーへ設定ができなかった際には、即座に実行が失敗して終了する設計になっています。このような設計をAnsibleでは「fail-fast」と呼んでいます。
Many times, people ask, "how can I best integrate testing with Ansible playbooks?" There are many options. Ansible is actually designed to be a "fail-fast" and ordered system, therefore it makes it easy to embed testing directly in Ansible playbooks.
(Ansible Documentation Testing Strategies本文より抜粋)
この「fail-fast」な設計により、Ansibleの実行が失敗しなかった場合はPlaybookに記述した通りに構築が行われたことが保証されます。そのため、Ansibleにはテストが必要ないという考えも存在します。しかし、人の手でコードを書いている限りは、設定の順序やパラメータの間違いなどのヒューマンエラーが発生する可能性はあります。そのような可能性を洗い出すためにも、テストをする必要性は出てくると考えます。
テスト対象
Ansibleにおいてテストは必要だと考えますが、「fail-fast」という設計によりテストすべき項目が減っていることは確かです。ここからは、Ansibleでどのようなテストを行うべきかを説明していきます。
一連のtask単位でテストを行う
以下のリストは、jenkinsのインストールからサービスの開始までの一連のtaskを記述したものです。
リスト1:jenkinsのインストールからサービスを開始するまでのタスク
---
- name: Install openjdk and openjdk-devel
yum: name={{ item }} state=present update_cache=yes
with_items: openjdk_items
tags:
- jenkins
- name: Get jenkins repository
get_url: url={{ jenkins_repository }} dest={{ yum_repository_path }}
tags:
- jenkins
- name: Import jenkins key
rpm_key: state=present key={{ jenkins_key }}
tags:
- jenkins
- name: Install jenkins
yum: name=jenkins state=latest update_cache=yes
tags:
- jenkins
- name: Start jenkins
service: name=jenkins state=started enabled=yes
tags:
- jenkins
この一連のtaskにおける最終的な目的は、jenkinsのサービスが正常に起動していることです。つまりテストするべき項目は「jenkinsのサービスが正常に動作していること」となります。また、Ansible上ではこの一連のタスクの実行が正常に完了したとしても、その後に何らかの要因でjenkinsのサービスが停止してしまう可能性もあります。そういった事態を考慮して、最終確認としてこのようなテストを行っておくべきだと考えます。
不安なtaskをテストする
先ほどの「一連のtask単位でテストを行う」は、最低限行うべきことだと思います。それ以外に、途中過程においても不安なtaskがあればテストするべきだと考えます。Playbookを、コードという形で人間が記述している限りはヒューマンエラーが存在するため、絶対的な安心はできないはずです。TDD(テスト駆動開発)でよく出てくるキーワードとして「不安をテストにする」というものがあるように、Playbookを書く人が不安に思う項目があればテストをするべきです。
シェルスクリプトをテストする
Ansibleには、commandモジュールやshellモジュールのようにシェルスクリプトをそのまま実行できるモジュールが存在しています。これらのモジュールは、シェルスクリプトが正常に終了したことを保証します。そのため、Ansibleの実行が正常に終了したにもかかわらず、期待した結果にならない可能性が高くなります。これらのモジュールを使用する際は、シェルスクリプトそのものが間違っている可能性を考慮して、期待通りの結果であることをテストしてあげるべきだと考えます。
ユーザーの期待する結果をテストする
例えば、ユーザーがjenkinsを利用したいという要件があったとします。その要件を満たすために、jenkinsが利用できる環境を構築するPlaybookを実行します。 その後、実際の利用を想定して外部から対象サーバーのIPとjenkinsのport番号を指定して期待したページが返ってくることや、適切なステータスコードが返ってくることをテストします。このテストによりユーザーの要件を満たしていることを確認します。
このテストは先ほどの「一連のtask単位でテストを行う」より一つ上のレイヤー(インフラ全体)をテストしています。そのため、Ansibleが実行した内容は全く気にしないので、Ansibleにおけるテストという枠を超えることにはなります。しかし、対象サーバーが正常に構築されていても、ネットワークの設定など何らかの外的要因により、目的を達成できない可能性があります。Ansibleの構成管理だけではなく、インフラ全体を意識した広い視野で見れば、このテストも必要となってきます。
テストツール
テストツールを利用するメリット
正しいPlaybookを作成する過程で、下図のようにAnsibleの実行と構築の確認を繰り返し行うことがあります。
Playbookの設定を間違えた際、対象サーバーにログインし、コマンドを駆使して構築が正常に行われていることを確認するのは非効率な方法です。それに対してテストツールを利用すれば、このような手動での確認は不要になり、効率的に構築結果を確認できます。加えてCI(継続的インテグレーション)ツールを利用することにより、Playbookの修正、Ansible実行、テストコードの実行という一連の流れを自動化することもできます。
テストツールの紹介
serverspec
serverspecはRSpecがベースとなっているテストツールで、sshでテスト対象のサーバーにログインしてサーバーの中からテストを行います。Ansibleを実行した際も同じようにsshで対象のサーバーにログインしてから実行します。そのためAnsibleの実行で行える項目とserverspecでテストできる項目は、重複している部分が多くあります。Ansibleに対応したテストは、容易に行うことができます。serverspecの記法は、Rspecとほぼ同じです。
以下の例ではhttpdのサービスが起動していることをテストしています。
リスト2:serverspecの記述例
require 'spec_helper'
set :request_pty, true
describe package('httpd') do
it { should be_running }
end
serverspecでテストできる主な項目は、以下の通りです。
- 指定したサービスが動いているか
- 指定したポートが開いているか
- 指定したファイルが存在しているか
AnsibleSpec
AnsbileSpecはAnsible専用テストツールで、serverspecがベースとなっています。テストコードをPlaybookのディレクトリ内に組み込むことや、inventoryファイルの設定をもとにテストを実行することができます。また、テストできる項目や記述方法はserverspecと同じです。詳細は「Ansible専用のテストツールAnsibleSpecの特徴および使い方」にて解説されています。
infrataster
infratasterはRSpecがベースとなっているテストツールで、対象サーバーの外からテストを行うツールです。対象サーバーの内部は気にせず、外部から確認を行うため、Ansibleにおけるテストだけではなく、外的要因も洗い出すことができます。infratasterの記法は、Rspecとほぼ同じです。
以下の例では、指定のURLにリクエストした際にステータスコードの200番を返してくることをテストしています。
リスト3:infratasterの記述例
require 'spec_helper'
describe server(:app) do
describe http('http://ip:port') do
it 'returns 200' do
expect(response.status).to eq(200)
end
end
end
infratasterでテストできる主な項目は、以下の通りです。
- 指定したステータスコードが返ってくるか
- 指定したページを返すか
Ansibleのassertモジュール
Ansible標準のモジュールを用いて、Playbookにテストコードを記述できます。 that句にテスト項目を文字列で指定します。渡す文字列は、when句に記述できる内容と同じになります。
以下の例では、OSのfamilyが「Debian」であることをテストしています。
リスト4:assertモジュールを用いたテストの例
- assert: { that: "ansible_os_family == 'Debian'" }
テストツールを使用した実例
ここまでの話を踏まえて、実際にAnsibleにおけるテストを行っていきます。
構成および実行の流れ
今回は、jenkinsをインストールしてサービスを起動するPlaybookをテスト対象とします。全体の構成図および実行の流れを下図に示します。
実行の流れとしては、まず①で10.255.197.175のjenkins実行用サーバーに対してjenkinsのインストールおよびサービスを起動するPlaybookをAnsibleで実行します。Ansibleの実行が終了したら、次に②で正常に構築されていることを確認するためにテストを実行します。使用するテストツールは、serverspecおよびinfratasterとします。
テスト対象となるPlaybook
ディレクトリ構造は、以下のようになっています。
リスト5:ディレクトリ構造
├── group_vars
│ └── all
├── hosts
├── jenkins.yml
└── roles
└── jenkins
├── tasks
│ ├── deploy.yml
│ └── main.yml
└── vars
└── main.yml
hosts(inventoryファイル)の内容を以下に示します。
リスト6:hostsファイル
[all] *.*.*.*
以下のPlaybookは、group_varsディレクトリのallです。
リスト7:Playbook(group_vars/all)
user: "root" proxy_env: http_proxy: "" https_proxy: ""
以下のPlaybookは、jenkins.ymlです。
リスト8:Playbook(jenkins.yml)
---
- name: Ansible-Jenkins-Test
hosts: jenkins
become: yes
become_user: "{{ user }}"
roles:
- common
- jenkins
environment: "{{ proxy_env }}"
以下のPlaybookは、roles/jenkins/varsディレクトリのmain.ymlです。
リスト9:Playbook(roles/jenkins/vars/main.yml)
---
openjdk_items:
- java-1.7.0-openjdk
- java-1.7.0-openjdk-devel
yum_repository_path: /etc/yum.repos.d
jenkins_repository: http://pkg.jenkins-ci.org/redhat/jenkins.repo
jenkins_key: http://pkg.jenkins-ci.org/redhat/jenkins-ci.org.key
jenkins_host_name: localhost
jenkins_port: 8080
jenkins_updates_json_path: /var/lib/jenkins/updates/default.json
jenkins_cli_dest: ~/jenkins-cli.jar
jenkins_cli_url: http://{{ jenkins_host_name }}:{{ jenkins_port }}/jnlpJars/jenkins-cli.jar
以下のPlaybookは、roles/jenkins/tasksディレクトリのmain.ymlです。
リスト10:Playbook(roles/jenkins/tasks/main.yml)
--- - include: deploy.yml
以下のPlaybookは、roles/jenkins/tasksディレクトリのdeploy.ymlです。
リスト11:Playbook(roles/jenkins/tasks/deploy.yml)
---
- name: Install openjdk and openjdk-devel
yum: name={{ item }} state=present update_cache=yes
with_items: openjdk_items
tags:
- jenkins
- name: Get jenkins repository
get_url: url={{ jenkins_repository }} dest={{ yum_repository_path }}
tags:
- jenkins
- name: Import jenkins key
rpm_key: state=present key={{ jenkins_key }}
tags:
- jenkins
- name: Install jenkins
yum: name=jenkins state=latest update_cache=yes
tags:
- jenkins
- name: Start jenkins
service: name=jenkins state=started enabled=yes
tags:
- jenkins
テストコード
まず、serverspecのテストコードを以下に示します。
リスト12:serverspecのテストコード
require 'spec_helper'
set :request_pty, true
describe package('jenkins') do
it { should be_installed }
end
describe service('jenkins') do
it { should be_enabled }
it { should be_running }
end
describe port(8080) do
it { should be_listening }
end
上述のテストコードでは、以下の項目がテストされています。
- jenkinsのパッケージがインストールされているか
- jenkinsのサービスがenableになっているか
- jenkinsのサービスが起動しているか
- 8080番ポートがlisten状態か
テスト結果(serverspec)
テストが成功した場合は、以下のような出力になります。
リスト13:テスト成功時の出力(serverspec)
.... Finished in 3.34 seconds (files took 0.5838 seconds to load) 4 examples, 0 failures
次に、infratasterによるテストコードを示します。
リスト14:infratasterのテストコード
require 'spec_helper'
describe server(:app) do
describe http('http://10.255.197.175:8080') do
it 'returns 200 status code' do
expect(response.status).to eq(200)
end
it 'responds as \'text/html;charset=UTF-8\'' do
expect(response.headers['content-type']).to eq('text/html;charset=UTF-8')
end
it 'responds content including \'Jenkinsへようこそ!\'' do
expect(response.body.force_encoding('UTF-8')).to include('Jenkinsへようこそ!')
end
end
end
上述のテストコードでは、以下の項目がテストされています。
- 対象のページに対してリクエストした際にステータスコード200番が返ってくるか
- 対象のページに対してリクエストした際にヘッダーのcontent-typeが「text/html;charset=UTF-8」か
- 対象のページに対してリクエストした際にbodyの中に「Jenkinsへようこそ」という文字列が含まれているか
テスト結果(infrataster)
テストが成功した場合は以下のような出力になります。
リスト15:テスト成功時の出力(infrataster)
... Finished in 0.08792 seconds (files took 1.2 seconds to load) 3 examples, 0 failures
まとめ
第6回では、Ansibleにおけるテストをどのように行うべきかを解説しました。また、その考えをもとに実例としてテストを行いました。 Ansibleにおけるテストが必要であるかという問題に対して、議論されることは多々あります。必ずテストが必要であると言い切ることは難しいですが、テストを行うメリットはあります。そしてテストのメリットを引き出すためには、そのPlaybookが何のために作成されているかを意識することが大切です。
次回は、実践編として複数のアプリケーションをデプロイする実例を紹介します。




