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

2016年8月9日(火)
坂本 諒太
構成管理ツール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作成時には実行と確認が繰り返される

Playbook作成時には実行と確認が繰り返される

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が何のために作成されているかを意識することが大切です。

次回は、実践編として複数のアプリケーションをデプロイする実例を紹介します。

TIS株式会社

戦略技術センター
2015年新卒入社。
デザイン指向クラウドオーケストレーションソフトウェア「CloudConductor」(http://cloudconductor.org/)のプロジェクトに従事。
開発の傍らDockerやAnsibleなど、OSSの検証を行っている。
最近興味を持っている技術はIoTとElixir言語。

連載バックナンバー

運用・管理技術解説
第10回

Ansible Towerのクラウド連携機能による効率化を目指す

2017/9/26
Ansible Towerが備えるクラウド連携機能を用いることで、Playbookやユーザーの管理を効率良く行えることを紹介する。
運用・管理技術解説
第9回

Ansible Towerによる権限の管理

2017/7/27
複数のユーザーによる使用を前提としたAnsible Towerでの権限管理の方法を紹介する。
運用・管理
第8回

Ansibleの機能を拡張するAnsible Tower

2017/4/18
好評の連載「注目の構成管理ツールAnsibleを徹底活用する」の追加コンテンツとして、Ansible Towerを取り上げる。

Think ITメルマガ会員登録受付中

Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

Think ITメルマガ会員のサービス内容を見る

他にもこの記事が読まれています