BFT名古屋 TECH BLOG

日々の業務で得た知識を所属するエンジニアたちがアウトプットしていきます。

【Asnible・Serverspec】Ansible_specを利用して構築と単体テストを一緒に行おう!

はじめに


こんにちは!
自動化大好き、株式会社BFT名古屋支店・インフラ女子(?)のやまぐちです。

Ansibleで自動構築をしていると、テストで画面キャプチャを取得したり、ログを眺めたりするのが少しもったいなく感じます。構築と一緒に設定値確認や一部の動作確認を同時にできれば単体テスト仕様書の作成から実施までの多くの工数を削減できそうですよね。

テスト仕様書を作るのって、結構時間がかかります。構築を自動化してるなら、テストしなくてもいいよねという気持ちもありますが、プロジェクト側では今までとやり方を変えるのに難しい場合も少なくありません。

というわけで、今回はAnsible_specを利用して、構築&テストをノンストップで実行していく構成を作ってみました。かなりムネアツな構成です。

Ansible_specを利用して構築と単体テストを一緒に行う


Ansible_specって?

そもそもAnsible_specって何なんでしょう?これは(恐らく)Ansibleで構築したものをServerspecでテストしたいという有志の方が公開してくださっているプリセットのファイル群、です。

Ansibleで構築したものをServerspecでテストしたいと思った時に困るのが以下の点です。

  • Ansibleで使った変数を使えない(ServerspecはServerspecで変数を用意する)
  • Ansibleだとロールベースで書いているのにServerspecは(デフォルトだと)対象のホストベースでテストを書くので、構築とテストの紐づけができない

これを解決するためにRakefileやspec_helper.rbなどをあらかじめAnsibleのロールベースでできるようにしてくれているのが、Ansible_specです。

公式はGithubで公開されています。制限もありますが、それさえ認識しておけばとても有用でありがたいものです。

GitHub - volanja/ansible_spec: It's ruby gem that connect Ansible & Serverspec for Test Driven Server Configuration(or TDD).

前提条件

AnsibleとServerspecは既にインストールされている前提で進めます。その他Rubyのバージョンは現在は「3.2系」が最新ですが「3.1系」までが動作確認済みのようです。(3.2だと動かなかったので、3.1に落として使っています)

またこの記事は以下の構成で動作を確認しました。

※すみません、AnsibleとRspecは確認するのを忘れました…

その後RHELのUBIコンテナイメージでも確認したので以下の構成でも動作確認済みです。

なおAnsibleで実行するPlaybook「site.yml」の内容は以下の通りです。testロールがlocalhostで実行される簡単なものです。この「-name」から始まる書き方をすることでServerspecでもロール単位でテストができます(Serverspecではここに記載されている「-name」を「rake serverspec::<nameに記載の文字列>」と実行時に指定してテストします)。

- name: test
  hosts: localhost
  roles:
    - test

環境構築

Ansible_spec

Ansible_specを入れる前のディレクトリ構成は以下の通りです。基本的にAnsibleのことだけ考えていればOKです。

Ansibleのルートは「/home/ansibleuser/work/ansible」として、直下にsite.yml、インベントリとしてのhosts、testのロール、という構成です。

/home/ansibleuser/work/
      |--- ansible
                |--- site.yml
                |--- hosts
                |--- roles
                    |--- test
                          |--- tasks
                          |     |--- main.yml
                          |--- vars
                                |--- main.yml

Ansibleは「ansibleuser」で実行したいため、Rubyもansibleuserで実行できるように「~/.bash_profile」には以下の行を追加しています。

export PATH="$HOME/.rbenv/bin:$PATH"
eval "$(rbenv init -)"

Ansible_specは以下のコマンドでインストールします。

gem install ansible_spec

インストール後は以下のコマンドで初期化します。このコマンドを実行すると、直下にこの構成に必要なServerspec系のディレクトリやファイルが作成されます。

ansiblespec-init

この構成でServerspec(Rspec?)を実行するにはいくつかの制限があります。実行するPlaybookは「site.yml」で実行先のホストは「hosts」が同じディレクトリにあることです。その設定は「ansiblespec-init」コマンドを実行した際に作成される「.ansiblespec」に記載されています。

---
-
  playbook: site.yml
  inventory: hosts

ここでServerspecが参照するPlaybookとインベントリファイルを指定してます。実行する環境に合わせて必要に応じて変更してください。
今回localhostで実行するにあたり、site.ymlに記載していた「hosts: localhost」ではうまくServerspecの実行ができなかったため、site.ymlからコピーした「site2.yml」を作成し「hosts: all」と修正、 「.ansiblespec」のplaybookをsite2.ymlに修正しました。

---
-
  playbook: site2.yml
  inventory: hosts

hostsは以下の通りです。

localhost ansible_connection=local

Serverspecではansible_connectionでlocalhostを指定しないとローカルにSSHで接続しようとします。

Ansible側の工夫

さて、ここからがポイントです。Ansibleでの構築とServerspecでのテストを一緒に行うには以下の実装を行います。

  1. テストするロール配下にspecとhandlersディレクトリを作成する
  2. テストするロール配下のspecディレクトリに「<ロール名>_spec.rb」でテストを記載する(ロール名部分は任意でもよい)
  3. テストするロール配下のhandlersディレクトリにテストを実行するコマンドを記載する
  4. テストするロール配下のtasks/main.ymlには必要な部分にnotifyを追加し、変更があった時だけServerspecのテストを実行できるようにする

ひとつひとつ見ていきましょう。

「テストするロール配下にspecとhandlersディレクトリを作成する」
対応済みのディレクトリ構成は以下の通りです。普通に「spec」「handlers」ディレクトリをmkdirしてください。

/home/ansibleuser/work/
      |--- ansible
                |--- site.yml
                |--- hosts
                |--- roles
                         |--- test
                               |--- tasks
                               |   |--- main.yml
                               |--- vars
                               |    |--- main.yml
                               |--- spec       # 追加
                               | --- handlers  # 追加
                              

特に何も気にすることはありません。このspecディレクトリにはテストを、handlersディレクトリにはそのテストを実行するコマンドをこの後の手順で書きます。

「テストするロール配下のspecディレクトリに「<ロール名>_spec.rb」でテストを記載する」
例えばtasks/main.ymlがファイルに文字列を追加する内容だとします。

- name: Add string to test.txt
  lineinfile:
    path: /tmp/test.txt
    line: "{{ test_string }}"

変数はvars/main.ymlで設定しています。

test_string: abcde

Ansibleを実行すると「/tmp/test.txt」の末行に「abcde」と文字列を追記します。

このテストを「test_spec.rb」に記載します。「/tmp/test.txt」に「test_string」が含まれていること、をテストするだけです。

require 'spec_helper'

describe file('/tmp/test.txt') do
    its(:content) { should match /#{property['test_string']}/ }
end

prperty['<変数>'] と書くことでServerspec側でもAnsibleの変数を使えますが、今回は文字列マッチのため#{property['test_string']}と記載しています。変数をAnsibleでもServerspecでも使えるのは素晴らしいですね。

「テストするロール配下のhandlersディレクトリにテストを実行するコマンドを記載する」
Ansibleは冪等性を保つ書き方にすることが大事です。変更があった時だけテストを走らせたいので、handlersを利用します。

- name: Add string to test.txt
  lineinfile:
    path: /tmp/test.txt
    line: "{{ test_string }}"
  notify: '"{{ role_name }}" Exec unit test'   # 追加
  

handlers/main.yml内にnotifyの値と同じ文字列を「name」で指定することで、changedの際にそのタスクを実行します。handlers/main.ymlは以下のように記載します。

---

- name: '"{{ role_name }}" Exec unit test'
  shell:
    cmd: "rake serverspec:{{ role_name }} >> /tmp/test.log"

「role_name」はマジック変数であり、元々Ansible側で予約されている変数です。ここをrole_nameとすることでロールが増えてもhandlers/main.ymlもnotifyでの指定もコピペで済みます(テストのためログ出力先は適当なパス&ファイル名です)。

ここまでの構成をまとめると以下の図の通りです。

これで準備は完了です。

テスト

それではAnsibleでの構築&テストを実施してみましょう。
まずは構文チェックです。

[ansibleuser@3e4de8a86b38 src]$ ansible-playbook site.yml --syntax-check
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit
localhost does not match 'all'

playbook: site.yml

WARNINGが出ていますが、これはhostsにlocalhost以外の記載がないためです。続いて実行するタスクの確認です。

[ansibleuser@3e4de8a86b38 src]$ ansible-playbook site.yml --list-tasks
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit
localhost does not match 'all'

playbook: site.yml

  play #1 (localhost): test     TAGS: []
    tasks:
      test : Add string to test.txt     TAGS: []
[ansibleuser@3e4de8a86b38 src]$ 

タスクとして「Add string to test.txt」を実行することを確認できました。続いてドライランです。

[ansibleuser@3e4de8a86b38 src]$ ansible-playbook site.yml -C
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit
localhost does not match 'all'

PLAY [test] ******************************************************************************************

TASK [Gathering Facts] *******************************************************************************
ok: [localhost]

TASK [test : Add string to test.txt] *****************************************************************
changed: [localhost]

RUNNING HANDLER [test : "test" Exec unit test] *******************************************************
skipping: [localhost]

PLAY RECAP *******************************************************************************************
localhost                  : ok=2    changed=1    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0   

[ansibleuser@3e4de8a86b38 src]$

ドライランでは実際に実行はしないので、テストの部分(TASK [test : Add string to test.txt] )がスキップされています。特に問題なさそうなので本実行しましょう。

[ansibleuser@3e4de8a86b38 src]$ ansible-playbook site.yml
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit
localhost does not match 'all'

PLAY [test] ******************************************************************************************

TASK [Gathering Facts] *******************************************************************************
ok: [localhost]

TASK [test : Add string to test.txt] *****************************************************************
changed: [localhost]

RUNNING HANDLER [test : "test" Exec unit test] *******************************************************
changed: [localhost]

PLAY RECAP *******************************************************************************************
localhost                  : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

[ansibleuser@3e4de8a86b38 src]$ 

changed=2 でうまく実行できたようです。Serverspecのログ(/tmp/test.log)も見てみましょう。

Run serverspec for test to {"name"=>"localhost ansible_connection=local", "port"=>22, "connection"=>"local", "uri"=>"localhost"}
/home/ansibleuser/.rbenv/versions/3.1.3/bin/ruby -I/home/ansibleuser/.rbenv/versions/3.1.3/lib/ruby/gems/3.1.0/gems/rspec-support-3.12.0/lib:/home/ansibleuser/.rbenv/versions/3.1.3/lib/ruby/gems/3.1.0/gems/rspec-core-3.12.1/lib /home/ansibleuser/.rbenv/versions/3.1.3/lib/ruby/gems/3.1.0/gems/rspec-core-3.12.1/exe/rspec --pattern \{roles\}/\{test\}/spec/\*_spec.rb

File "/tmp/test.txt"
  content
    is expected to match /abcde/

Finished in 0.11089 seconds (files took 0.97542 seconds to load)
1 example, 0 failures

[ansibleuser@3e4de8a86b38 src]$ 

「1 example, 0 failures」と、うまく実行できてますね!

終わりに


どうでしょうか。この構成はAnsibleを使う時はデフォルトにしたらいいんじゃないかと思います。かなりムネアツな構成だということがわかっていただければ嬉しいです。

次はServerspecの実行結果をCSVに出力し、実行結果を簡単に確認できるやり方をご紹介したいと思います。 以上、ここまで読んでいただきありがとうございました~ ^ ^