ansible_specでAnsibleとServerspecを連携させる
- 環境
- 実施内容
- まとめ
- 参考文献
サーバ構築の際に、Ansibleで設定、Serverspecで試験を実施する場合、以下のような課題が出てくる。
- 実行対象のサーバとロールについて、AnsibleとServerspecそれぞれでファイル管理する必要がある。
(Ansibleはインベントリファイルとプレイブック、ServerspecはRakefileや独自の設定ファイルなど。) - Serverspecの初期のディレクトリ構成、Rakefile、spec_helper.rbは複数サーバへの実行がしづらいため、導入する際は使い方を検討して、それなりに修正が必要になる。
→ansible_specというツールを使うと、Ansibleのインベントリから対象サーバを、プレイブックから対象ロールを読み込み、ServerspecのRakeタスクを生成できるらしい。
複数サーバ、複数ロールの実行にも対応しており、上記課題を解決できそうなので試してみる。
※Ansibleで構築した環境に試験が必要なのかという点について
Ansibleは宣言的な記述ができ、記述された状態になることを保障してくれるので、そもそも別ツールでの試験が必要なのか、と思ったりもする。
しかし、以下の理由で必要だと考えている。
- プレイブックの記載ミスや、Ansible自体の不具合、そもそも設定内容が誤っているといった事態があり得るので、別のツールで試験することに意味はある。
- Ansibleだけで動作確認しようとすると、プレイブックが複雑になってくる(単に設定するだけではなく、その設定で想定通りの挙動になるかの確認コマンドの実行などを盛り込んでいく必要が出てくる)。
- 実際の開発では、「実績のあるAnsibleプレイブックで構築してるのでテストしなくて大丈夫!」で偉い人や顧客を説得できないこともあると思う。
なんだかんだ別のテストツールでの試験エビデンスがあった方が、みんな安心。
環境
バージョン
- DockerホストのOS:RockyLinux8.6
- 管理対象サーバ(Serverspecの実行先)のOS:RockyLinux8.6
- Docker:20.10.18
- ansible:7.3.0
- ansible-core:2.14.3
- ansiible_spec:0.3.2
- Serverspec:2.42.2
※Ansibleの実行環境は以下記事で構築したものを使う。 tk-ch.hatenablog.com
Ansible実行対象のサーバ
商用環境
- test-prd-001 (192.168.10.1)
- test-prd-002 (192.168.10.2)
- test-prd-003 (192.168.10.3)
ステージング環境
- test-stg-001 (192.168.20.1)
実施内容
Ansibleのファイルを準備する
ansible_specを試すために、Ansibleのプレイブックを用意する。
今回は、以下記事で使用したものを使うことにした。
tk-ch.hatenablog.com
ディレクトリ構成は以下の通り。
ansible |-- ansible.cfg # Ansibleの設定ファイル |-- inventories # インベントリと変数定義の格納場所 | |-- production # 商用環境用 | | |-- group_vars # グループ単位の変数定義の格納場所 | | | |-- all.yml # 全グループ共通の変数定義 | | | |-- test_group_a.yml # グループごとの変数定義(ファイル名はグループ名.yml) | | | |-- test_group_ab.yml | | | `-- test_group_b.yml | | |-- host_vars # ホスト単位の変数定義の格納場所 | | | |-- test-prd-001.yml # ホストごとの変数定義(ファイル名はホスト名.yml) | | | |-- test-prd-002.yml | | | `-- test-prd-003.yml | | `-- hosts # インベントリ(Ansibleの実行対象ホスト、グループの定義) | `-- staging # ステージング環境用 | |-- group_vars | | |-- all.yml | | |-- test_group_a.yml | | |-- test_group_ab.yml | | `-- test_group_b.yml | |-- host_vars | | `-- test-stg-001.yml | `-- hosts |-- roles # タスク定義の格納場所 | |-- role00 # ロール単位でタスク定義をまとめている | | |-- defaults | | | `-- main.yml # ロールのデフォルト変数定義 | | `-- tasks | | `-- main.yml # タスクで実行したい処理をここに書く | |-- role01 | | |-- defaults | | | `-- main.yml | | |-- tasks | | | `-- main.yml | | |-- templates | | | `-- variable.txt.j2 # templateモジュールで使うテンプレートファイル | | `-- vars | | `-- main.yml # ロール変数定義 | `-- role02 | |-- defaults | | `-- main.yml | |-- tasks | | `-- main.yml | |-- templates | | `-- variable.txt.j2 | `-- vars | `-- main.yml |-- site.yml # マスタープレイブック |-- test_group_a.yml # 実行対象のグループとロールを記載したプレイブック |-- test_group_ab.yml `-- test_group_b.yml
Ansibleを商用環境とステージング環境に実行し、エラー無く実行されることを確認する。
# ansible-playbook -i inventories/production/hosts site.yml # ansible-playbook -i inventories/staging/hosts site.yml
ちなみに、実行される処理は以下の2つ(詳細はこの記事を参照)。
ansible_spec実行用のDockerイメージをビルドする
ansible_specはどこでも簡単に構築できるよう、Dockerコンテナを使って実行することにする。
まずはDockerfileを作成する。
Dockerfile_ansiblespec
# ベースイメージ FROM rockylinux:8.6 # 変数定義 ARG ANSIBLESPEC_VERSION # 必要なパッケージのインストール RUN dnf module -y enable ruby:3.1 && \ dnf -y install \ gcc-c++ \ make \ openssh-clients \ redhat-rpm-config \ ruby \ ruby-devel && \ dnf clean all && \ rm -rf /var/cache/dnf/* # Serverspecのインストール RUN gem install ansible_spec -v $ANSIBLESPEC_VERSION RUN gem install rake RUN gem install highline # ロケールを日本語に設定 RUN dnf -y install glibc-locale-source glibc-langpack-en && \ dnf clean all && \ rm -rf /var/cache/dnf/* RUN localedef -f UTF-8 -i ja_JP ja_JP.utf8 RUN echo 'LANG="ja_JP.UTF-8"' > /etc/locale.conf # タイムゾーンをJSTに設定 RUN echo 'ZONE="Asia/Tokyo"' > /etc/sysconfig/clock RUN rm -f /etc/localtime RUN ln -fs /usr/share/zoneinfo/Asia/Tokyo /etc/localtime # コンテナログイン時のカレントディレクトリを設定 WORKDIR /work/ansible
→Dockerfileの解説
- ベースイメージがRockyLinux8.6なのは使い慣れているからで、特に意味は無い。他のOSで作っても良い。
- ansible_specのインストール方法は、GitのREADMEを参照。
- ansible_specをインストールすると、Serverspecも一緒にインストールされる。
- openssh-clientsは、Serverspecから管理対象サーバへ接続するために必要なのでインストールした。
- インストールするansible_specのバージョンを制御できるよう、ARGにて変数ANSIBLESPEC_VERSIONを定義し、ansible_specインストール時に指定している。
変数に設定する値は、後ほど「docker build」実行時に--build-argオプションで指定する。
詳細はDockerの公式ドキュメントのここを参照。 - dnf実行時の書き方については、RedHatの「How to build tiny container images」を参考にした。
- ロケールとタイムゾーンの設定は必須ではないが、自前のコンテナイメージ作成時はいつもやっているので入れている。
ansible_specのGitリポジトリのReleasesを見ると、0.3.2が現在(2023年2月)時点の最新版の模様。 ansible_specのバージョンを0.3.2と指定し、コンテナイメージをビルドする。
インストールしたいansible_specのバージョンを環境変数に格納 # export ANSIBLESPEC_VERSION=0.3.2 Dockerイメージをビルド # docker build --no-cache=true -f Dockerfile_ansiblespec -t ansiblespec:$ANSIBLESPEC_VERSION --build-arg ANSIBLESPEC_VERSION=$ANSIBLESPEC_VERSION . ビルドされたイメージを確認 # docker images | grep ansiblespec | grep $ANSIBLESPEC_VERSION ansiblespec 0.3.2 9b5927bf46bd 15 seconds ago 509MB ビルドされたイメージを起動し、インストールされたバージョンを確認 # docker run --rm -it ansiblespec:$ANSIBLESPEC_VERSION gem list ansible_spec serverspec rake highline *** LOCAL GEMS *** ansible_spec (0.3.2) *** LOCAL GEMS *** serverspec (2.42.2) *** LOCAL GEMS *** rake (13.0.6) *** LOCAL GEMS *** highline (2.1.0)
ansiblespec-initの実行
先ほどビルドしたコンテナイメージを指定し、ansible_specを実行してみる。
まずは、ansiblespec-initを実行して必要なファイルを生成する。
ansiblespec-initはAnsibleプレイブックがあるディレクトリで実行するので、ansible_specコンテナ起動時に該当のディレクトリをマウントしている。
Ansibleのファイルが置かれたDockerホストのディレクトリを確認 # ls -l /root/ansible 合計 16 -rw-r--r-- 1 root root 37 3月 4 11:54 ansible.cfg drwxr-xr-x 4 root root 39 3月 4 13:53 inventories drwxr-xr-x 5 root root 48 3月 4 12:24 roles -rw-r--r-- 1 root root 76 3月 4 11:54 site.yml -rw-r--r-- 1 root root 132 3月 4 11:54 test_group_a.yml -rw-r--r-- 1 root root 132 3月 4 11:54 test_group_b.yml 使用するansible_specのバージョンを指定 # export ANSIBLESPEC_VERSION=0.3.2 ansiblespecコンテナから、DockerホストのAnsible用ディレクトリがマウントできていることを確認 # docker run --rm -it -v /root/ansible:/work/ansible ansiblespec:$ANSIBLESPEC_VERSION ls -l total 16 -rw-r--r-- 1 root root 37 Mar 4 20:54 ansible.cfg drwxr-xr-x 4 root root 39 Mar 4 22:53 inventories drwxr-xr-x 5 root root 48 Mar 4 21:24 roles -rw-r--r-- 1 root root 76 Mar 4 20:54 site.yml -rw-r--r-- 1 root root 132 Mar 4 20:54 test_group_a.yml -rw-r--r-- 1 root root 132 Mar 4 20:54 test_group_b.yml ansiblespec-initを実行して、ansible_specで使うファイルを生成する。 # docker run --rm -it -v /root/ansible:/work/ansible ansiblespec:$ANSIBLESPEC_VERSION ansiblespec-init create spec create spec/spec_helper.rb create Rakefile create .ansiblespec create .rspec
作成されたファイルの中身は以下の通り。
Rakefile
require 'rake' require 'rspec/core/rake_task' require 'yaml' require 'ansible_spec' properties = AnsibleSpec.get_properties # {"name"=>"Ansible-Sample-TDD", "hosts"=>["192.168.0.103","192.168.0.103"], "user"=>"root", "roles"=>["nginx", "mariadb"]} # {"name"=>"Ansible-Sample-TDD", "hosts"=>[{"name" => "192.168.0.103:22","uri"=>"192.168.0.103","port"=>22, "private_key"=> "~/.ssh/id_rsa"}], "user"=>"root", "roles"=>["nginx", "mariadb"]} cfg = AnsibleSpec::AnsibleCfg.new desc "Run serverspec to all test" task :all => "serverspec:all" namespace :serverspec do properties = properties.compact.reject{|e| e["hosts"].length == 0} task :all => properties.map {|v| 'serverspec:' + v["name"] } properties.each_with_index.map do |property, index| property["hosts"].each do |host| desc "Run serverspec for #{property["name"]}" RSpec::Core::RakeTask.new(property["name"].to_sym) do |t| puts "Run serverspec for #{property["name"]} to #{host}" ENV['TARGET_HOSTS'] = host["hosts"] ENV['TARGET_HOST'] = host["uri"] ENV['TARGET_PORT'] = host["port"].to_s ENV['TARGET_GROUP_INDEX'] = index.to_s ENV['TARGET_PRIVATE_KEY'] = host["private_key"] unless host["user"].nil? ENV['TARGET_USER'] = host["user"] else ENV['TARGET_USER'] = property["user"] end ENV['TARGET_PASSWORD'] = host["pass"] ENV['TARGET_CONNECTION'] = host["connection"] roles = property["roles"] for role in property["roles"] for rolepath in cfg.roles_path deps = AnsibleSpec.load_dependencies(role, rolepath) if deps != [] roles += deps break end end end t.pattern = '{' + cfg.roles_path.join(',') + '}/{' + roles.join(',') + '}/spec/*_spec.rb' end end end end
→Ansibleのファイルから実行対象のホストとロールの情報を取得し、実行するホストとspecファイルを決定している。
spec/spec_helper.rb
require 'serverspec' require 'net/ssh' require 'ansible_spec' require 'winrm' # # Set ansible variables to serverspec property # host = ENV['TARGET_HOST'] hosts = ENV["TARGET_HOSTS"] group_idx = ENV['TARGET_GROUP_INDEX'].to_i vars = AnsibleSpec.get_variables(host, group_idx,hosts) ssh_config_file = AnsibleSpec.get_ssh_config_file set_property vars connection = ENV['TARGET_CONNECTION'] case connection when 'ssh' # # OS type: UN*X # set :backend, :ssh # Ansible use `BECOME`, But Serverspec use `SUDO`. if ENV['ASK_BECOME_PASSWORD'] begin require 'highline/import' rescue LoadError fail "highline is not available. Try installing it." end set :sudo_password, ask("Enter become password: ") { |q| q.echo = false } else set :sudo_password, ENV['BECOME_PASSWORD'] end options = Net::SSH::Config.for(host) options[:user] = ENV['TARGET_USER'] || options[:user] options[:port] = ENV['TARGET_PORT'] || options[:port] options[:keys] = ENV['TARGET_PRIVATE_KEY'] || options[:keys] if ssh_config_file from_config_file = Net::SSH::Config.for(host,files=[ssh_config_file]) options.merge!(from_config_file) end set :host, options[:host_name] || host set :ssh_options, options # Disable become (Serverspec use sudo) # set :disable_sudo, true # Set environment variables # set :env, :LANG => 'C', :LC_MESSAGES => 'C' # Set PATH # set :path, '/sbin:/usr/local/sbin:$PATH' when 'winrm' # # OS type: Windows # set :backend, :winrm set :os, :family => 'windows' user = ENV['TARGET_USER'] port = ENV['TARGET_PORT'] pass = ENV['TARGET_PASSWORD'] if user.nil? begin require 'highline/import' rescue LoadError fail "highline is not available. Try installing it." end user = ask("\nEnter #{host}'s login user: ") { |q| q.echo = true } end if pass.nil? begin require 'highline/import' rescue LoadError fail "highline is not available. Try installing it." end pass = ask("\nEnter #{user}@#{host}'s login password: ") { |q| q.echo = false } end endpoint = "http://#{host}:#{port}/wsman" winrm = ::WinRM::WinRMWebService.new(endpoint, :ssl, :user => user, :pass => pass, :basic_auth_only => true) winrm.set_timeout 300 # 5 minutes max timeout for any operation Specinfra.configuration.winrm = winrm when 'local' # # local connection # set :backend, :exec end
→Ansibleのファイルからの変数定義読み込みと、実行先ホストへのSSH接続の設定が書いてある。
.ansiblespec
--- - playbook: site.yml inventory: hosts
→ansible_specの設定ファイル。
記載内容はこちらを参照。
.rspec
--color --format documentation
→rspec実行時の設定ファイル。
タスクを確認する
Rakefileにより作成されるRakeタスクを確認してみる。
# export ANSIBLESPEC_VERSION=0.3.2 # docker run --rm -it -v /root/ansible:/work/ansible ansiblespec:$ANSIBLESPEC_VERSION rake INVENTORY=inventories/production/hosts -T rake all # Run serverspec to all test rake serverspec:test_group_a # Run serverspec for test_group_a rake serverspec:test_group_b # Run serverspec for test_group_b
→表示されたRakeタスクを確認すると、「rake all」という全対象に対するタスクと、Ansibleのインベントリに記載しているグループ「test_group_a」、「test_group_b」に対するタスクが作成されている。
「rake serverspec: 」に続く文字列はプレイブックの「name:」に指定された文字列になる。
Rakefile内でansible_specの機能が使用されて、Ansibleのプレイブック、インベントリファイルから情報が読み込まれている模様。
注意:Ansibleのディレクトリ構成がansible_specのデフォルト設定と異なる場合
最初以下のように実行したら、エラーになった。
# export ANSIBLESPEC_VERSION=0.3.2 # docker run --rm -it -v /root/ansible:/work/ansible ansiblespec:$ANSIBLESPEC_VERSION rake -T Error: hosts is not Found. create hosts or ./.ansiblespec See https://github.com/volanja/ansible_spec
→デフォルトでは「.ansiblespec」の記載内容に従い、プレイブックとしてsite.yml、インベントリ ファイルとしてhostsが使用される。
今回のディレクトリ構成だとインベントリファイルは「inventories/環境名/hosts」になるので、「Error: hosts is not Found. 」となって実行に失敗している。
「.ansiblespec」を変更するか、ansible_specのREADMEにあるように環境変数で変更できる。
環境変数INVENTORYにインベントリファイルの配置パスを設定して実行したところ、エラーが解消した。
(ちなみに、プレイブックをデフォルトから変更したい場合は環境変数PLAYBOOKに設定する。)
注意:プレイブックの「name:」の値について
プレイブックの「name:」の値は、Serverspecタスクの「rake serverspec: 」に続く文字列に使われる。
そのため、プレイブックに「name:」が定義されていないと以下のように「Please insert name on playbook」というエラーになる。
# rake INVENTORY=inventories/production/hosts VARS_DIRS_PATH=inventories/production -T rake aborted! Please insert name on playbook 'site.yml' /usr/local/share/gems/gems/ansible_spec-0.3.2/lib/ansible_spec/load_ansible.rb:273:in `load_playbook' /usr/local/share/gems/gems/ansible_spec-0.3.2/lib/ansible_spec/load_ansible.rb:426:in `get_properties' /work/ansible/Rakefile:6:in `<top (required)>' /usr/local/share/gems/gems/rake-13.0.6/exe/rake:27:in `<top (required)>' (See full trace by running task with --trace)
また、「name:」の値にはスペースを含めることができ、Ansible実行時には特に問題にならない。
しかし、Serverspec側で以下の「rake serverspec:test group a」のようなタスク定義になってしまい、そのままコマンドとして実行してもエラーになってしまうので、避けるべき。
タスク定義を表示 # rake INVENTORY=inventories/production/hosts VARS_DIRS_PATH=inventories/production -T rake all # Run serverspec to all test rake serverspec:test group a # Run serverspec for test group a rake serverspec:test_group_b # Run serverspec for test_group_b 以下はエラーになる # rake INVENTORY=inventories/production/hosts VARS_DIRS_PATH=inventories/production serverspec:test group a rake aborted! Don't know how to build task 'serverspec:test' (See the list of available tasks with `rake --tasks`) Did you mean? serverspec:all /usr/local/share/gems/gems/rake-13.0.6/exe/rake:27:in `<top (required)>' (See full trace by running task with --trace) 以下のように二重引用符で囲めばエラーにならないが、分かりづらい # rake INVENTORY=inventories/production/hosts VARS_DIRS_PATH=inventories/production "serverspec:test group a"
また、「name:」の値が仮に被っていると、同じnameのプレイブックはまとめて一つのタスクにされてしまい、個別の実行が出来なくなる。
例えば、test_group_a.ymlもtest_group_b.ymlも「name: test_group」としている場合、タスク定義は「rake serverspec:test_group」にまとめられてしまう。
このタスクを実行するとtest_group_aとtest_group_b両方のグループにServerspecが実行されることになり、グループ単位での実行ができない。
# rake INVENTORY=inventories/production/hosts VARS_DIRS_PATH=inventories/production -T rake all # Run serverspec to all test rake serverspec:test_group # Run serverspec for test_group
以上のことから、「name:」の値はスペースなど特殊な文字は含めず、他のプレイブックのnameと重複していない、実行内容が分かりやすいものがよい。
基本的には、本記事で使っているプレイブックのように、「name:」の値は「hosts:」の値と同じにするのが無難だと思う。
テストコードを作成する
テストを実行するため、テストコードを作成する。
テストコードの記載におけるポイントは以下の通り。
- 書式など、記載方法は普通にServerspecを使うときと同じ。
- テストコードの配置場所は、ansible_specのREADMEに従い、テスト対象のロールの「roles/ロール名/spec/*_spec.rb」にする。
- ansible_specを使うと、テストコード内でAnsibleで設定している変数を使用できる。
変数が上書きされる優先度もAnsibleと同じなので、テストコードに対応するロールと同様の値を得られることになる。
ただし、インベントリファイルとロールのtasks/main.ymlに定義された変数、Fasts変数に対応していないので注意すること。
※詳細はansible_specのREADME参照。
各ロールのテストコードを以下のように作成した。role00
roles/role00/spec/main_spec.rb
require 'spec_helper' puts '###### roles/role00のテスト' property['role00_services_list'].each do |service| describe service("#{service}"), :if => os[:family] == 'redhat' do it { should be_enabled } end end
→role00は「role00_services_listに定義したサービス(chronyd、crond、sshd)の自動起動を有効にする」というロール。
対応するテストとして、serviceリソースを使用して、「サービスの自動起動が有効であること」を確認する内容にする。
対象のサービスはAnsibleで以下のように定義しているので、「property['role00_services_list']」でリストを取り出して、Ansibleのタスクと同様繰り返し処理にしている。
roles/role00/defaults/main.yml
--- role00_services_list: - chronyd - crond - sshd
role01
roles/role01/spec/main_spec.rb
require 'spec_helper' puts '###### roles/role01のテスト' describe file('/tmp/role01.txt'), :if => os[:family] == 'redhat' do it { should be_file } end variable_file = <<"EOF" test_var_role_default: #{property['test_var_role_default']} test_var_group_all : #{property['test_var_group_all']} test_var_group_parent: #{property['test_var_group_parent']} test_var_group : #{property['test_var_group']} test_var_host : #{property['test_var_host']} test_var_role : #{property['test_var_role']} EOF describe file('/tmp/role01.txt'), :if => os[:family] == 'redhat' do its(:content) { should match variable_file } end
→role01は「/tmp/ロール名.txtに、Ansibleの各種変数定義ファイルにで定義した変数の値を出力する」というロール。
対応するテストとして、fileリソースを使用して、「ファイルが存在していること、内容が想定通りであること」を確認する内容にする。
its(:content)マッチャ―で1行ずつ確認することもできるが、それだと各文字列の順番が正しいかまでは確認できないので、ヒアドキュメントを使ってファイルの内容全体を比較している。
注意:Ansibleのマジック変数について
今回用意したAnsibleのプレイブックでは、ロール名.txtというファイルを作成する際に、role_nameというAnsibleのマジック変数を使用している。
そのため、テストコードでもファイル名を指定する部分「describe file('/tmp/role01.txt')」は、「describe file('/tmp/' + property['role_name'] + '.txt')」としたかったが、ansible_spec実行時にrole_nameの展開はされたが値が空で上手くいかなかった。
恐らく、role_nameはansible-playbook実行時にしか値が設定されないのだと思う。
そのため、事実上ansible_specではこういったAnsibleマジック変数は使用できないと考えられる。
role02
roles/role02/spec/main_spec.rb
require 'spec_helper' puts '###### roles/role02のテスト' describe file('/tmp/role02.txt'), :if => os[:family] == 'redhat' do it { should be_file } end variable_file = <<"EOF" test_var_role_default: #{property['test_var_role_default']} test_var_group_all : #{property['test_var_group_all']} test_var_group_parent: #{property['test_var_group_parent']} test_var_group : #{property['test_var_group']} test_var_host : #{property['test_var_host']} test_var_role : #{property['test_var_role']} EOF describe file('/tmp/role02.txt'), :if => os[:family] == 'redhat' do its(:content) { should match variable_file } end
→role02はファイル名と変数の値以外role01と同じロールなので、テストコードもファイル名以外はrole01と同じ内容。
Serverspecを実行する
商用環境のtest_group_aグループに対してテストを実行する
実行におけるポイントは以下の通り。
- 実行先の環境面は、環境変数INVENTORYに指定するインベントリファイルの選択で制御する。
- 環境変数INVENTORYにインベントリファイルのパスを指定する(理由は前述の補足参照)。
- 環境変数VARS_DIRS_PATHにgroup_vars、host_varsが存在するディレクトリパスを指定する(理由は後述)。
- グループ単位に実行したい場合はrakeコマンドの引数に「serverspec:グループ名」と指定する。全ホストに実行する場合は「all」を指定する。
- 今回、実行先サーバへのSSHはパスワード認証なので、実行するときパスワード入力を求められるが、手動で入力する。
※Ansibleプレイブックにはansible_ssh_passでパスワードを設定済みだが、こちらに記載の通りansible_specでは非対応なため、読み込まれない。
# export ANSIBLESPEC_VERSION=0.3.2 # docker run --rm -it -v /root/ansible:/work/ansible ansiblespec:$ANSIBLESPEC_VERSION rake INVENTORY=inventories/production/hosts serverspec:test_group_a Run serverspec for test_group_a to {"name"=>"192.168.10.1", "port"=>22, "connection"=>"ssh", "uri"=>"192.168.10.1"} /usr/bin/ruby -I/usr/local/share/gems/gems/rspec-support-3.12.0/lib:/usr/local/share/gems/gems/rspec-core-3.12.1/lib /usr/local/share/gems/gems/rspec-core-3.12.1/exe/rspec --pattern \{roles\}/\{role00,role01\}/spec/\*_spec.rb ###### roles/role00のテスト /usr/local/share/gems/gems/specinfra-2.85.0/lib/specinfra/backend/ssh.rb:82:in `create_ssh': Passing nil, or [nil] to Net::SSH.start is deprecated for keys: user, keys root@192.168.10.1's password: ###### roles/role01のテスト Service "chronyd" is expected to be enabled Service "crond" is expected to be enabled Service "sshd" is expected to be enabled File "/tmp/role01.txt" is expected to be file File "/tmp/role01.txt" content is expected to match "test_var_role_default: roles/role01/defaults/main.yml\ntest_var_group_all : roles/role01/defaults/main.yml\ntest_var_group_parent: roles/role01/defaults/main.yml\ntest_var_group : roles/role01/defaults/main.yml\ntest_var_host : roles/role01/defaults/main.yml\ntest_var_role : roles/role01/vars/main.yml\n" (FAILED - 1) Failures: 1) File "/tmp/role01.txt" content is expected to match "test_var_role_default: roles/role01/defaults/main.yml\ntest_var_group_all : roles/role01/defaults/main.yml\ntest_var_group_parent: roles/role01/defaults/main.yml\ntest_var_group : roles/role01/defaults/main.yml\ntest_var_host : roles/role01/defaults/main.yml\ntest_var_role : roles/role01/vars/main.yml\n" On host `192.168.10.1' Failure/Error: its(:content) { should match variable_file } expected "test_var_role_default: roles/role01/defaults/main.yml\ntest_var_group_all : inventories/production...entories/production/host_vars/test-prd-001.yml\ntest_var_role : roles/role01/vars/main.yml\n" to match "test_var_role_default: roles/role01/defaults/main.yml\ntest_var_group_all : roles/role01/defaults/main.yml\ntest_var_group_parent: roles/role01/defaults/main.yml\ntest_var_group : roles/role01/defaults/main.yml\ntest_var_host : roles/role01/defaults/main.yml\ntest_var_role : roles/role01/vars/main.yml\n" Diff: @@ -1,7 +1,7 @@ test_var_role_default: roles/role01/defaults/main.yml -test_var_group_all : roles/role01/defaults/main.yml -test_var_group_parent: roles/role01/defaults/main.yml -test_var_group : roles/role01/defaults/main.yml -test_var_host : roles/role01/defaults/main.yml +test_var_group_all : inventories/production/group_vars/all.yml +test_var_group_parent: inventories/production/group_vars/test_group_ab.yml +test_var_group : inventories/production/group_vars/test_group_a.yml +test_var_host : inventories/production/host_vars/test-prd-001.yml test_var_role : roles/role01/vars/main.yml sudo -p 'Password: ' /bin/sh -c cat\ /tmp/role01.txt\ 2\>\ /dev/null\ \|\|\ echo\ -n test_var_role_default: roles/role01/defaults/main.yml test_var_group_all : inventories/production/group_vars/all.yml test_var_group_parent: inventories/production/group_vars/test_group_ab.yml test_var_group : inventories/production/group_vars/test_group_a.yml test_var_host : inventories/production/host_vars/test-prd-001.yml test_var_role : roles/role01/vars/main.yml # ./roles/role01/spec/main_spec.rb:19:in `block (2 levels) in <top (required)>' Finished in 0.3952 seconds (files took 5.37 seconds to load) 5 examples, 1 failure Failed examples: rspec ./roles/role01/spec/main_spec.rb:19 # File "/tmp/role01.txt" content is expected to match "test_var_role_default: roles/role01/defaults/main.yml\ntest_var_group_all : roles/role01/defaults/main.yml\ntest_var_group_parent: roles/role01/defaults/main.yml\ntest_var_group : roles/role01/defaults/main.yml\ntest_var_host : roles/role01/defaults/main.yml\ntest_var_role : roles/role01/vars/main.yml\n" /usr/bin/ruby -I/usr/local/share/gems/gems/rspec-support-3.12.0/lib:/usr/local/share/gems/gems/rspec-core-3.12.1/lib /usr/local/share/gems/gems/rspec-core-3.12.1/exe/rspec --pattern \{roles\}/\{role00,role01\}/spec/\*_spec.rb failed
→Failureとなった点があるので確認すると、4つの変数test_var_group_all、test_var_group_parent、test_var_group、test_var_hostの値がプレイブックで作成されたものと異なる。
この4つはinventories/{group_vars,host_vars}ディレクトリ配下で定義された値で上書きされてほしかったが、上書きされていない。
これは、ansible_specはデフォルトでは直下にあるgroup_varsディレクトリとhost_varsディレクトリの配下しか読み込まないため。
その他の場所を読み込ませるには環境変数VARS_DIRS_PATHにgroup_varsディレクトリとhost_varsディレクトリのパスを設定する必要がある。
この点を修正して再度実行してみる。
# export ANSIBLESPEC_VERSION=0.3.2 # docker run --rm -it -v /root/ansible:/work/ansible ansiblespec:$ANSIBLESPEC_VERSION rake INVENTORY=inventories/production/hosts VARS_DIRS_PATH=inventories/production serverspec:test_group_a Run serverspec for test_group_a to {"name"=>"192.168.10.1", "port"=>22, "connection"=>"ssh", "uri"=>"192.168.10.1"} /usr/bin/ruby -I/usr/local/share/gems/gems/rspec-support-3.12.0/lib:/usr/local/share/gems/gems/rspec-core-3.12.1/lib /usr/local/share/gems/gems/rspec-core-3.12.1/exe/rspec --pattern \{roles\}/\{role00,role01\}/spec/\*_spec.rb ###### roles/role00のテスト /usr/local/share/gems/gems/specinfra-2.85.0/lib/specinfra/backend/ssh.rb:82:in `create_ssh': Passing nil, or [nil] to Net::SSH.start is deprecated for keys: user, keys root@192.168.10.1's password: ###### roles/role01のテスト Service "chronyd" is expected to be enabled Service "crond" is expected to be enabled Service "sshd" is expected to be enabled File "/tmp/role01.txt" is expected to be file File "/tmp/role01.txt" content is expected to match "test_var_role_default: roles/role01/defaults/main.yml\ntest_var_group_all : inventories/production/group_vars/all.yml\ntest_var_group_parent: inventories/production/group_vars/test_group_ab.yml\ntest_var_group : inventories/production/group_vars/test_group_a.yml\ntest_var_host : inventories/production/group_vars/test_group_a.yml\ntest_var_role : roles/role01/vars/main.yml\n" (FAILED - 1) Failures: 1) File "/tmp/role01.txt" content is expected to match "test_var_role_default: roles/role01/defaults/main.yml\ntest_var_group_all : inventories/production/group_vars/all.yml\ntest_var_group_parent: inventories/production/group_vars/test_group_ab.yml\ntest_var_group : inventories/production/group_vars/test_group_a.yml\ntest_var_host : inventories/production/group_vars/test_group_a.yml\ntest_var_role : roles/role01/vars/main.yml\n" On host `192.168.10.1' Failure/Error: its(:content) { should match variable_file } expected "test_var_role_default: roles/role01/defaults/main.yml\ntest_var_group_all : inventories/production...entories/production/host_vars/test-prd-001.yml\ntest_var_role : roles/role01/vars/main.yml\n" to match "test_var_role_default: roles/role01/defaults/main.yml\ntest_var_group_all : inventories/production/group_vars/all.yml\ntest_var_group_parent: inventories/production/group_vars/test_group_ab.yml\ntest_var_group : inventories/production/group_vars/test_group_a.yml\ntest_var_host : inventories/production/group_vars/test_group_a.yml\ntest_var_role : roles/role01/vars/main.yml\n" Diff: @@ -2,6 +2,6 @@ test_var_group_all : inventories/production/group_vars/all.yml test_var_group_parent: inventories/production/group_vars/test_group_ab.yml test_var_group : inventories/production/group_vars/test_group_a.yml -test_var_host : inventories/production/group_vars/test_group_a.yml +test_var_host : inventories/production/host_vars/test-prd-001.yml test_var_role : roles/role01/vars/main.yml sudo -p 'Password: ' /bin/sh -c cat\ /tmp/role01.txt\ 2\>\ /dev/null\ \|\|\ echo\ -n test_var_role_default: roles/role01/defaults/main.yml test_var_group_all : inventories/production/group_vars/all.yml test_var_group_parent: inventories/production/group_vars/test_group_ab.yml test_var_group : inventories/production/group_vars/test_group_a.yml test_var_host : inventories/production/host_vars/test-prd-001.yml test_var_role : roles/role01/vars/main.yml # ./roles/role01/spec/main_spec.rb:19:in `block (2 levels) in <top (required)>' Finished in 0.4168 seconds (files took 5.81 seconds to load) 5 examples, 1 failure Failed examples: rspec ./roles/role01/spec/main_spec.rb:19 # File "/tmp/role01.txt" content is expected to match "test_var_role_default: roles/role01/defaults/main.yml\ntest_var_group_all : inventories/production/group_vars/all.yml\ntest_var_group_parent: inventories/production/group_vars/test_group_ab.yml\ntest_var_group : inventories/production/group_vars/test_group_a.yml\ntest_var_host : inventories/production/group_vars/test_group_a.yml\ntest_var_role : roles/role01/vars/main.yml\n" /usr/bin/ruby -I/usr/local/share/gems/gems/rspec-support-3.12.0/lib:/usr/local/share/gems/gems/rspec-core-3.12.1/lib /usr/local/share/gems/gems/rspec-core-3.12.1/exe/rspec --pattern \{roles\}/\{role00,role01\}/spec/\*_spec.rb failed
→test_var_group_all、test_var_group_parent、test_var_groupについてはgroup_vars配下で定義された値が読み込まれるようになった。
しかし、test_var_host変数だけはまだNGのまま。
host_vars配下の値が読み込まれていないように見える。
これは、結論から言うとhost_vars配下のファイル名を以下のようにホスト名.ymlからIPアドレス.ymlに変更したら解消した。
※host_vars配下のファイルは、ファイル名がhostsファイルに定義するホスト名と一致しないと読み込まれないので、hostsファイル内の定義も併せて修正している。
<修正前>
# ls -l inventories/*/host_vars inventories/production/host_vars: 合計 12 -rw-r--r-- 1 root root 138 3月 4 12:36 test-prd-001.yml -rw-r--r-- 1 root root 138 3月 4 12:36 test-prd-002.yml -rw-r--r-- 1 root root 138 3月 4 12:36 test-prd-003.yml inventories/staging/host_vars: 合計 4 -rw-r--r-- 1 root root 132 3月 4 12:39 test-stg-001.yml # cat inventories/production/hosts [test_group_a] test-prd-001 ansible_host=192.168.10.1 test-prd-002 ansible_host=192.168.10.2 [test_group_b] test-prd-003 ansible_host=192.168.10.3
<修正後>
# ls -l inventories/*/host_vars inventories/production/host_vars: 合計 12 -rw-r--r-- 1 root root 138 3月 4 12:36 192.168.10.3.yml -rw-r--r-- 1 root root 138 3月 4 12:36 192.168.10.1.yml -rw-r--r-- 1 root root 138 3月 4 12:36 192.168.10.2.yml inventories/staging/host_vars: 合計 4 -rw-r--r-- 1 root root 132 3月 4 12:39 192.168.20.1.yml # cat inventories/production/hosts [test_group_a] #test-prd-001 192.168.10.1 #test-prd-002 192.168.10.2 [test_group_b] #test-prd-003 192.168.10.3
再度試してみる。
# docker run --rm -it -v /root/ansible:/work/ansible ansiblespec:$ANSIBLESPEC_VERSION rake INVENTORY=inventories/production/hosts VARS_DIRS_PATH=inventories/production serverspec:test_group_a Run serverspec for test_group_a to {"name"=>"192.168.10.1", "port"=>22, "connection"=>"ssh", "uri"=>"192.168.10.1"} /usr/bin/ruby -I/usr/local/share/gems/gems/rspec-support-3.12.0/lib:/usr/local/share/gems/gems/rspec-core-3.12.1/lib /usr/local/share/gems/gems/rspec-core-3.12.1/exe/rspec --pattern \{roles\}/\{role00,role01\}/spec/\*_spec.rb ###### roles/role00のテスト /usr/local/share/gems/gems/specinfra-2.85.0/lib/specinfra/backend/ssh.rb:82:in `create_ssh': Passing nil, or [nil] to Net::SSH.start is deprecated for keys: user, keys root@192.168.10.1's password: ###### roles/role01のテスト Service "chronyd" is expected to be enabled Service "crond" is expected to be enabled Service "sshd" is expected to be enabled File "/tmp/role01.txt" is expected to be file File "/tmp/role01.txt" content is expected to match "test_var_role_default: roles/role01/defaults/main.yml\ntest_var_group_all : inventories/production/group_vars/all.yml\ntest_var_group_parent: inventories/production/group_vars/test_group_ab.yml\ntest_var_group : inventories/production/group_vars/test_group_a.yml\ntest_var_host : inventories/production/host_vars/test-prd-001.yml\ntest_var_role : roles/role01/vars/main.yml\n" Finished in 0.39271 seconds (files took 6.47 seconds to load) 5 examples, 0 failures Run serverspec for test_group_a to {"name"=>"192.168.10.2", "port"=>22, "connection"=>"ssh", "uri"=>"192.168.10.2"} /usr/bin/ruby -I/usr/local/share/gems/gems/rspec-support-3.12.0/lib:/usr/local/share/gems/gems/rspec-core-3.12.1/lib /usr/local/share/gems/gems/rspec-core-3.12.1/exe/rspec --pattern \{roles\}/\{role00,role01\}/spec/\*_spec.rb ###### roles/role00のテスト /usr/local/share/gems/gems/specinfra-2.85.0/lib/specinfra/backend/ssh.rb:82:in `create_ssh': Passing nil, or [nil] to Net::SSH.start is deprecated for keys: user, keys root@192.168.10.2's password: ###### roles/role01のテスト Service "chronyd" is expected to be enabled Service "crond" is expected to be enabled Service "sshd" is expected to be enabled File "/tmp/role01.txt" is expected to be file File "/tmp/role01.txt" content is expected to match "test_var_role_default: roles/role01/defaults/main.yml\ntest_var_group_all : inventories/production/group_vars/all.yml\ntest_var_group_parent: inventories/production/group_vars/test_group_ab.yml\ntest_var_group : inventories/production/group_vars/test_group_a.yml\ntest_var_host : inventories/production/host_vars/test-prd-002.yml\ntest_var_role : roles/role01/vars/main.yml\n" Finished in 0.4198 seconds (files took 5.39 seconds to load) 5 examples, 0 failures
→全てのテストをパスした。
注意:host_vars/ホスト名.ymlが読み込まれない
host_vars/ホスト名.ymlで上手くいかず、host_vars/IPアドレス.ymlだと上手くいかなかったのは、インベントリファイルで以下のようにホスト名を登録してIPをansible_hostに設定する方式にしていた(登録された名前と実際のSSH時の宛先が異なることになる)からだと考えている。
inventories/production/hosts
[test_group_a] test-prd-001 ansible_host=192.168.10.1 test-prd-002 ansible_host=192.168.10.2 [test_group_b] test-prd-003 ansible_host=192.168.10.3
→恐らく、ansible_specでは「sshする際に使用する実行先(今回はIPアドレス)の名前.yml」が読み込まれるのだと思う。
実際、IPではなくホスト名指定でSSHできるように環境を変更し、インベントリファイルにもホスト名だけ記載すれば、ホスト名.ymlのファイルを読み込んでくれた。
商用環境の全ホストに対してテストを実行する
テストが通るようになったので、商用環境の全ホストに対して改めてテストを実行してみる。
# docker run --rm -it -v /root/ansible:/work/ansible ansiblespec:$ANSIBLESPEC_VERSION rake INVENTORY=inventories/production/hosts VARS_DIRS_PATH=inventories/production all Run serverspec for test_group_a to {"name"=>"192.168.10.1", "port"=>22, "connection"=>"ssh", "uri"=>"192.168.10.1"} /usr/bin/ruby -I/usr/local/share/gems/gems/rspec-support-3.12.0/lib:/usr/local/share/gems/gems/rspec-core-3.12.1/lib /usr/local/share/gems/gems/rspec-core-3.12.1/exe/rspec --pattern \{roles\}/\{role00,role01\}/spec/\*_spec.rb ###### roles/role00のテスト /usr/local/share/gems/gems/specinfra-2.85.0/lib/specinfra/backend/ssh.rb:82:in `create_ssh': Passing nil, or [nil] to Net::SSH.start is deprecated for keys: user, keys root@192.168.10.1's password: ###### roles/role01のテスト Service "chronyd" is expected to be enabled Service "crond" is expected to be enabled Service "sshd" is expected to be enabled File "/tmp/role01.txt" is expected to be file File "/tmp/role01.txt" content is expected to match "test_var_role_default: roles/role01/defaults/main.yml\ntest_var_group_all : inventories/production/group_vars/all.yml\ntest_var_group_parent: inventories/production/group_vars/test_group_ab.yml\ntest_var_group : inventories/production/group_vars/test_group_a.yml\ntest_var_host : inventories/production/host_vars/test-prd-001.yml\ntest_var_role : roles/role01/vars/main.yml\n" Finished in 0.40632 seconds (files took 5.39 seconds to load) 5 examples, 0 failures Run serverspec for test_group_a to {"name"=>"192.168.10.2", "port"=>22, "connection"=>"ssh", "uri"=>"192.168.10.2"} /usr/bin/ruby -I/usr/local/share/gems/gems/rspec-support-3.12.0/lib:/usr/local/share/gems/gems/rspec-core-3.12.1/lib /usr/local/share/gems/gems/rspec-core-3.12.1/exe/rspec --pattern \{roles\}/\{role00,role01\}/spec/\*_spec.rb ###### roles/role00のテスト /usr/local/share/gems/gems/specinfra-2.85.0/lib/specinfra/backend/ssh.rb:82:in `create_ssh': Passing nil, or [nil] to Net::SSH.start is deprecated for keys: user, keys root@192.168.10.2's password: ###### roles/role01のテスト Service "chronyd" is expected to be enabled Service "crond" is expected to be enabled Service "sshd" is expected to be enabled File "/tmp/role01.txt" is expected to be file File "/tmp/role01.txt" content is expected to match "test_var_role_default: roles/role01/defaults/main.yml\ntest_var_group_all : inventories/production/group_vars/all.yml\ntest_var_group_parent: inventories/production/group_vars/test_group_ab.yml\ntest_var_group : inventories/production/group_vars/test_group_a.yml\ntest_var_host : inventories/production/host_vars/test-prd-002.yml\ntest_var_role : roles/role01/vars/main.yml\n" Finished in 0.44318 seconds (files took 5.66 seconds to load) 5 examples, 0 failures Run serverspec for test_group_b to {"name"=>"192.168.10.3", "port"=>22, "connection"=>"ssh", "uri"=>"192.168.10.3"} /usr/bin/ruby -I/usr/local/share/gems/gems/rspec-support-3.12.0/lib:/usr/local/share/gems/gems/rspec-core-3.12.1/lib /usr/local/share/gems/gems/rspec-core-3.12.1/exe/rspec --pattern \{roles\}/\{role00,role02\}/spec/\*_spec.rb ###### roles/role00のテスト /usr/local/share/gems/gems/specinfra-2.85.0/lib/specinfra/backend/ssh.rb:82:in `create_ssh': Passing nil, or [nil] to Net::SSH.start is deprecated for keys: user, keys root@192.168.10.3's password: ###### roles/role02のテスト Service "chronyd" is expected to be enabled Service "crond" is expected to be enabled Service "sshd" is expected to be enabled File "/tmp/role02.txt" is expected to be file File "/tmp/role02.txt" content is expected to match "test_var_role_default: roles/role02/defaults/main.yml\ntest_var_group_all : inventories/production/group_vars/all.yml\ntest_var_group_parent: inventories/production/group_vars/test_group_ab.yml\ntest_var_group : inventories/production/group_vars/test_group_b.yml\ntest_var_host : inventories/production/host_vars/test-prd-003.yml\ntest_var_role : roles/role02/vars/main.yml\n" Finished in 1.13 seconds (files took 9.97 seconds to load) 5 examples, 0 failures
→問題なく実行され、テストもすべてパスした。
以下のことが分かる。
- 192.168.10.1、192.168.10.2に対してはrole00とrole01のテスト、192.168.10.3に対してはrole00とrole02のテストというように、Ansibleと同じホスト、ロールの組み合わせでテストが実行できている。
- 「File "/tmp/ロール名.txt"」のテストをパスできているため、同じ変数がAnsibleのファイルの複数個所に定義されていても、ansible_specがちゃんとAnsibleと同じ優先順で値を上書きしていることが分かる。
ステージング環境の全ホストに対してテストを実行する
次に、ステージング環境の全ホストに対してテストを実行する。
環境変数INVENTORY、VARS_DIRS_PATHをステージング環境のものに変更して実行すればよい。
# docker run --rm -it -v /root/ansible:/work/ansible ansiblespec:$ANSIBLESPEC_VERSION rake INVENTORY=inventories/staging/hosts VARS_DIRS_PATH=inventories/staging all Run serverspec for test_group_a to {"name"=>"192.168.20.1", "port"=>22, "connection"=>"ssh", "uri"=>"192.168.20.1"} /usr/bin/ruby -I/usr/local/share/gems/gems/rspec-support-3.12.0/lib:/usr/local/share/gems/gems/rspec-core-3.12.1/lib /usr/local/share/gems/gems/rspec-core-3.12.1/exe/rspec --pattern \{roles\}/\{role00,role01\}/spec/\*_spec.rb ###### roles/role00のテスト /usr/local/share/gems/gems/specinfra-2.85.0/lib/specinfra/backend/ssh.rb:82:in `create_ssh': Passing nil, or [nil] to Net::SSH.start is deprecated for keys: user, keys root@192.168.20.1's password: ###### roles/role01のテスト Service "chronyd" is expected to be enabled Service "crond" is expected to be enabled Service "sshd" is expected to be enabled File "/tmp/role01.txt" is expected to be file File "/tmp/role01.txt" content is expected to match "test_var_role_default: roles/role01/defaults/main.yml\ntest_var_group_all : inventories/staging/group_vars/all.yml\ntest_var_group_parent: inventories/staging/group_vars/test_group_ab.yml\ntest_var_group : inventories/staging/group_vars/test_group_a.yml\ntest_var_host : inventories/staging/host_vars/test-stg-001.yml\ntest_var_role : roles/role01/vars/main.yml\n" Finished in 0.27998 seconds (files took 4.84 seconds to load) 5 examples, 0 failures
→環境変数INVENTORY、VARS_DIRS_PATHを変更することで、テスト実行先の環境面と読み込まれる変数定義を切り替えることが出来た。
注意:親グループをhostsに指定した際の挙動が、Ansibleとansible_specで異なる
先ほどまで使用していたsite.ymlでは、test_group_a.yml、test_group_b.ymlでhostsとしてtest_group_aグループ、test_group_bグループを指定している。
この2グループはインベントリで以下のようにtest_group_abグループの子として設定しているため、group_vars配下のtest_group_a.yml、test_group_b.ymlに加え、test_group_ab.ymlの変数定義も読み込まれる。
これは、Ansibleでもansible_specでも同様の挙動になっている。
inventories/production/hosts
# cat inventories/production/hosts [test_group_a] #test-prd-001 192.168.10.1 #test-prd-002 192.168.10.2 [test_group_b] #test-prd-003 192.168.10.3 [test_group_ab:children] test_group_a test_group_b
しかし、以下のプレイブックのようにhostsに親グループを指定したときは、Ansibleとansible_specで挙動が異なる。
test_group_ab.yml
--- - name: test_group_ab hosts: test_group_ab roles: - role: role00 tags: role00 - role: role01 tags: role01
試しに、上記プレイブックを使ってAnsibleとServerspecを実行してみる。
まずAnsibleを実行する
# ansible-playbook -i inventories/production/hosts test_group_ab.yml
次に、Serverspecを実行する。
# docker run --rm -it -v /root/ansible:/work/ansible ansiblespec:$ANSIBLESPEC_VERSION rake INVENTORY=inventories/production/hosts VARS_DIRS_PATH=inventories/production PLAYBOOK=test_group_ab.yml -T rake all # Run serverspec to all test rake serverspec:test_group_ab # Run serverspec for test_group_ab # docker run --rm -it -v /root/ansible:/work/ansible ansiblespec:$ANSIBLESPEC_VERSION rake INVENTORY=inventories/production/hosts VARS_DIRS_PATH=inventories/production PLAYBOOK=test_group_ab.yml serverspec:test_group_ab Run serverspec for test_group_ab to {"name"=>"192.168.10.1", "port"=>22, "connection"=>"ssh", "uri"=>"192.168.10.1"} /usr/bin/ruby -I/usr/local/share/gems/gems/rspec-support-3.12.0/lib:/usr/local/share/gems/gems/rspec-core-3.12.1/lib /usr/local/share/gems/gems/rspec-core-3.12.1/exe/rspec --pattern \{roles\}/\{role00,role01\}/spec/\*_spec.rb ###### roles/role00のテスト /usr/local/share/gems/gems/specinfra-2.85.0/lib/specinfra/backend/ssh.rb:82:in `create_ssh': Passing nil, or [nil] to Net::SSH.start is deprecated for keys: user, keys root@192.168.10.1's password: ###### roles/role01のテスト Service "chronyd" is expected to be enabled Service "crond" is expected to be enabled Service "sshd" is expected to be enabled File "/tmp/role01.txt" is expected to be file File "/tmp/role01.txt" content is expected to match "test_var_role_default: roles/role01/defaults/main.yml\ntest_var_group_all : inventories/production/group_vars/all.yml\ntest_var_group_parent: inventories/production/group_vars/test_group_ab.yml\ntest_var_group : inventories/production/group_vars/test_group_ab.yml\ntest_var_host : inventories/production/host_vars/test-prd-001.yml\ntest_var_role : roles/role01/vars/main.yml\n" (FAILED - 1) Failures: 1) File "/tmp/role01.txt" content is expected to match "test_var_role_default: roles/role01/defaults/main.yml\ntest_var_group_all : inventories/production/group_vars/all.yml\ntest_var_group_parent: inventories/production/group_vars/test_group_ab.yml\ntest_var_group : inventories/production/group_vars/test_group_ab.yml\ntest_var_host : inventories/production/host_vars/test-prd-001.yml\ntest_var_role : roles/role01/vars/main.yml\n" On host `192.168.10.1' Failure/Error: its(:content) { should match variable_file } expected "test_var_role_default: roles/role01/defaults/main.yml\ntest_var_group_all : inventories/production...entories/production/host_vars/test-prd-001.yml\ntest_var_role : roles/role01/vars/main.yml\n" to match "test_var_role_default: roles/role01/defaults/main.yml\ntest_var_group_all : inventories/production/group_vars/all.yml\ntest_var_group_parent: inventories/production/group_vars/test_group_ab.yml\ntest_var_group : inventories/production/group_vars/test_group_ab.yml\ntest_var_host : inventories/production/host_vars/test-prd-001.yml\ntest_var_role : roles/role01/vars/main.yml\n" Diff: @@ -1,7 +1,7 @@ test_var_role_default: roles/role01/defaults/main.yml test_var_group_all : inventories/production/group_vars/all.yml test_var_group_parent: inventories/production/group_vars/test_group_ab.yml -test_var_group : inventories/production/group_vars/test_group_ab.yml +test_var_group : inventories/production/group_vars/test_group_a.yml test_var_host : inventories/production/host_vars/test-prd-001.yml test_var_role : roles/role01/vars/main.yml sudo -p 'Password: ' /bin/sh -c cat\ /tmp/role01.txt\ 2\>\ /dev/null\ \|\|\ echo\ -n test_var_role_default: roles/role01/defaults/main.yml test_var_group_all : inventories/production/group_vars/all.yml test_var_group_parent: inventories/production/group_vars/test_group_ab.yml test_var_group : inventories/production/group_vars/test_group_a.yml test_var_host : inventories/production/host_vars/test-prd-001.yml test_var_role : roles/role01/vars/main.yml # ./roles/role01/spec/main_spec.rb:19:in `block (2 levels) in <top (required)>' Finished in 0.40189 seconds (files took 5.97 seconds to load) 5 examples, 1 failure Failed examples: rspec ./roles/role01/spec/main_spec.rb:19 # File "/tmp/role01.txt" content is expected to match "test_var_role_default: roles/role01/defaults/main.yml\ntest_var_group_all : inventories/production/group_vars/all.yml\ntest_var_group_parent: inventories/production/group_vars/test_group_ab.yml\ntest_var_group : inventories/production/group_vars/test_group_ab.yml\ntest_var_host : inventories/production/host_vars/test-prd-001.yml\ntest_var_role : roles/role01/vars/main.yml\n" /usr/bin/ruby -I/usr/local/share/gems/gems/rspec-support-3.12.0/lib:/usr/local/share/gems/gems/rspec-core-3.12.1/lib /usr/local/share/gems/gems/rspec-core-3.12.1/exe/rspec --pattern \{roles\}/\{role00,role01\}/spec/\*_spec.rb failed
→Ansibleでは変数test_var_groupの値がtest_group_a.yml、test_group_b.ymlに定義された値で上書きされている。
しかし、Serverspec(ansible_spec)では変数test_var_groupの値がtest_group_ab.ymlに定義された値のままで、test_group_a.yml、test_group_b.ymlが読み込まれていないことが分かる。
整理すると以下のようになる。
- hostsに子グループを指定した場合
- ansible:group_vars配下の子グループ.ymlに加え、親グループ.ymlも読み込まれる。
- ansible_spec:group_vars配下の子グループ.ymlに加え、親グループ.ymlも読み込まれる。
- hostsに親グループを指定した場合
- ansible:group_vars配下の親グループ.ymlに加え、子グループ.ymlも読み込まれる。
- ansible_spec:group_vars配下の親グループ.ymlだけが読み込まれ、子グループ.ymlは読み込まれない。
上記の挙動であるため、ansible_specを使う場合は、プレイブックのhostsには親グループではなく子グループを指定した方がよい。
親グループを指定してしまうと、挙動のずれによってServerspecでテストできなくなる。
aliasの設定
ansible_spec実行時に毎回「docker run ~」と入力するのは手間なので、エイリアスを使って簡単に実行できるようにする。
Dockerホストの「~/.bashrc」に以下の2行を追記する。
export ANSIBLESPEC_VERSION=0.3.2 alias rake='docker run --rm -it -v /root/ansible:/work/ansible ansiblespec:$ANSIBLESPEC_VERSION rake'
試してみる。
変更を反映 # source ~/.bashrc エイリアスを指定してansoble_specを実行する。 # rake INVENTORY=inventories/production/hosts VARS_DIRS_PATH=inventories/production -T rake all # Run serverspec to all test rake serverspec:test_group_a # Run serverspec for test_group_a rake serverspec:test_group_b # Run serverspec for test_group_b
→「rake ~」と実行するだけでDockerコンテナを使ってansible_specを実行できるようになった。
別バージョンのansible_specを使いたい場合は、該当バージョンのansible_specがインストールされたDockerイメージを用意し、環境変数「ANSIBLESPEC_VERSION」に指定する値を変えることで使用するバージョンを制御できる。
ansible_specのカスタマイズ
実行してみていくつか気になった点があるので、設定を変更して解消する。
全てansible_specというよりServerspecの話なので、Serverspec単体で使っている場合も同様の対処になる。
SSHの認証パスワードを自動入力するよう設定する
ansibleは、実行対象サーバにはパスワード認証ではなく鍵認証を使って接続できるよう設定しておくことを推奨しており、ansible_specもそれに倣っている。
そのため、SSHがパスワード認証になっているサーバにServerspecを実行した場合は、パスワードを手動入力する必要がある。
しかし、パスワード認証を使いたいケースもあると思うため、パスワードを自動入力するようにしてみる。
SSHがパスワード認証のサーバに対しServerspecを実行する際の設定は、以下に記載がある。
- Serverspec公式ドキュメントのTipsの「How to use SSH password login」
- 上記Tipsの元記事「パスワード認証でログインするサーバに対してserverspecでテストを実行する」
→spec_helper.rbを修正して、環境変数「LOGIN_PASSWORD」にSSHパスワードを格納してServerspecを実行すればパスワードの手動入力が不要になる。
上記ドキュメント通りに、spec/spec_helper.rbに以下2点を追記する(追記場所は後述)。
require 'highline/import'
if ENV['ASK_LOGIN_PASSWORD'] options[:password] = ask("\nEnter login password: ") { |q| q.echo = false } else options[:password] = ENV['LOGIN_PASSWORD'] end
以下のように実行すると、SSHの認証パスワードの入力を求められずにServerspecを実行できる。
# rake INVENTORY=inventories/production/hosts VARS_DIRS_PATH=inventories/production LOGIN_PASSWORD="SSHの認証パスワード" all
※ちなみに、環境変数LOGIN_PASSWORDを使わずに、以下のようにパスワードを直接spec_helper.rbに書いても同様の挙動になる。
環境変数に設定するのが面倒ならこの方法もありだが、パスワードをファイルに平文で記載するのはセキュリティ的には良くないので注意すること。
require 'highline/import'
LOGIN_PASSWORD="SSHの認証パスワード" ## 変更点 if ENV['ASK_LOGIN_PASSWORD'] options[:password] = ask("\nEnter login password: ") { |q| q.echo = false } else options[:password] = LOGIN_PASSWORD ## 変更点 end
テストが失敗しても処理が終了しないようにする
デフォルトの挙動だと、テスト項目がFAILEDになるとそこでテストが終了し、その後のテスト項目、ホストへのテスト実行はされない。
FAILEDになってもとりあえず全テストを終わらせて、後でまとめて結果を見たいので、テストが失敗しても処理が終了しないようにする。
こちらを参考にさせていただいて、Rakefileに以下処理を追記した(追記場所は後述)。
# 環境変数IGNORE_FAILが1の場合、途中でfailしてもテストを続ける if ENV['IGNORE_FAIL'] == "1" t.fail_on_error = false end
これで、以下のように環境変数IGNORE_FAILに1を設定してServerspecを実行すると、テストが失敗しても処理が継続される。
# rake INVENTORY=inventories/production/hosts VARS_DIRS_PATH=inventories/production LOGIN_PASSWORD="SSHの認証パスワード" IGNORE_FAIL=1 all
出力を見やすくする
どのロールに対応するテストか分かるように、specファイルに以下のようにputsを書いている。
roles/role00/spec/main_spec.rb
require 'spec_helper' puts '###### roles/role00のテスト' property['role00_services_list'].each do |service| describe service("#{service}"), :if => os[:family] == 'redhat' do it { should be_enabled } end end
roles/role01/spec/main_spec.rb
require 'spec_helper' puts '###### roles/role01のテスト' describe file('/tmp/role01.txt'), :if => os[:family] == 'redhat' do it { should be_file } end variable_file = <<"EOF" test_var_role_default: #{property['test_var_role_default']} test_var_group_all : #{property['test_var_group_all']} test_var_group_parent: #{property['test_var_group_parent']} test_var_group : #{property['test_var_group']} test_var_host : #{property['test_var_host']} test_var_role : #{property['test_var_role']} EOF describe file('/tmp/role01.txt'), :if => os[:family] == 'redhat' do its(:content) { should match variable_file } end
これは、以下のような表示になることを期待して書いた。
###### roles/role00のテスト (roles/role00のテスト結果) ###### roles/role01のテスト (roles/role01のテスト結果)
しかし、実際には以下のようになり、どこがrole00とrole01の結果の境界なのか分からなくなっている。
###### roles/role00のテスト ###### roles/role01のテスト (roles/role00のテスト結果) (roles/role01のテスト結果)
これは恐らく、Rakefileのホストごとのfor分の中で、以下のようにスペックファイルが一気に読み込まれ、同時実行されているためだと思う。
Rakefileの一部
t.pattern = '{' + cfg.roles_path.join(',') + '}/{' + roles.join(',') + '}/spec/*_spec.rb'
これを修正するのは影響が大きそうなので、テストコードを以下のように変更することで、ロールとテスト内容を表示し、テストコードとテスト結果を紐づけ易くした。
- 全体をdescribeでグループ化し、説明を「Role "ロール名"」とする。
- テスト単位でdescribeでグループ化し、説明を「Test "テストの説明"」とする。
roles/role00/spec/main_spec.rb
require 'spec_helper' describe 'Role "roles/role00"' do describe 'Test "check_service_enabled"' do property['role00_services_list'].each do |service| describe service("#{service}"), :if => os[:family] == 'redhat' do it { should be_enabled } end end end end
roles/role01/spec/main_spec.rb
require 'spec_helper' describe 'Role "roles/role01"' do describe 'Test "variable_file_exist"' do describe file('/tmp/role01.txt'), :if => os[:family] == 'redhat' do it { should be_file } end end describe 'Test "variable_file_contents"' do variable_file = <<"EOF" test_var_role_default: #{property['test_var_role_default']} test_var_group_all : #{property['test_var_group_all']} test_var_group_parent: #{property['test_var_group_parent']} test_var_group : #{property['test_var_group']} test_var_host : #{property['test_var_host']} test_var_role : #{property['test_var_role']} EOF describe file('/tmp/role01.txt'), :if => os[:family] == 'redhat' do its(:content) { should match variable_file } end end end
また現状の出力だと、実行対象ホスト間の境界や、ホストに実行される対象ロールが分かりづらいので、Rakefileに以下を追記する(追記場所は後述)。
Rakefileの一部
# 実行ホスト名、実行ロールを表示する puts "#####################################################################" puts " Target host : #{host["name"]}" puts " Target role : {#{cfg.roles_path.join(',')}}/{#{roles.join(',')}}" puts "##################################################3##################"
上記変更後のServerspecの出力は以下のようになる。
# rake INVENTORY=inventories/production/hosts VARS_DIRS_PATH=inventories/production LOGIN_PASSWORD="SSHの認証パスワード" IGNORE_FAIL=1 all Run serverspec for test_group_a to {"name"=>"192.168.10.1", "port"=>22, "connection"=>"ssh", "uri"=>"192.168.10.1"} ##################################################################### Target host : 192.168.10.1 Target role : {roles}/{role00,role01} ##################################################3################## /usr/bin/ruby -I/usr/local/share/gems/gems/rspec-support-3.12.0/lib:/usr/local/share/gems/gems/rspec-core-3.12.1/lib /usr/local/share/gems/gems/rspec-core-3.12.1/exe/rspec --pattern \{roles\}/\{role00,role01\}/spec/\*_spec.rb /usr/local/share/gems/gems/specinfra-2.85.0/lib/specinfra/backend/ssh.rb:82:in `create_ssh': Passing nil, or [nil] to Net::SSH.start is deprecated for keys: user, keys Role "roles/role00" Test "check_service_enabled" Service "chronyd" is expected to be enabled Service "crond" is expected to be enabled Service "sshd" is expected to be enabled Role "roles/role01" Test "variable_file_exist" File "/tmp/role01.txt" is expected to be file Test "variable_file_contents" File "/tmp/role01.txt" content is expected to match "test_var_role_default: roles/role01/defaults/main.yml\ntest_var_group_all : inventories/production/group_vars/all.yml\ntest_var_group_parent: inventories/production/group_vars/test_group_ab.yml\ntest_var_group : inventories/production/group_vars/test_group_a.yml\ntest_var_host : inventories/production/host_vars/test-prd-001.yml\ntest_var_role : roles/role01/vars/main.yml\n" Finished in 0.41207 seconds (files took 3.38 seconds to load) 5 examples, 0 failures Run serverspec for test_group_a to {"name"=>"192.168.10.2", "port"=>22, "connection"=>"ssh", "uri"=>"192.168.10.2"} ##################################################################### Target host : 192.168.10.2 Target role : {roles}/{role00,role01} ##################################################3################## /usr/bin/ruby -I/usr/local/share/gems/gems/rspec-support-3.12.0/lib:/usr/local/share/gems/gems/rspec-core-3.12.1/lib /usr/local/share/gems/gems/rspec-core-3.12.1/exe/rspec --pattern \{roles\}/\{role00,role01\}/spec/\*_spec.rb /usr/local/share/gems/gems/specinfra-2.85.0/lib/specinfra/backend/ssh.rb:82:in `create_ssh': Passing nil, or [nil] to Net::SSH.start is deprecated for keys: user, keys Role "roles/role00" Test "check_service_enabled" Service "chronyd" is expected to be enabled Service "crond" is expected to be enabled Service "sshd" is expected to be enabled Role "roles/role01" Test "variable_file_exist" File "/tmp/role01.txt" is expected to be file Test "variable_file_contents" File "/tmp/role01.txt" content is expected to match "test_var_role_default: roles/role01/defaults/main.yml\ntest_var_group_all : inventories/production/group_vars/all.yml\ntest_var_group_parent: inventories/production/group_vars/test_group_ab.yml\ntest_var_group : inventories/production/group_vars/test_group_a.yml\ntest_var_host : inventories/production/host_vars/test-prd-002.yml\ntest_var_role : roles/role01/vars/main.yml\n" Finished in 0.43605 seconds (files took 3.28 seconds to load) 5 examples, 0 failures Run serverspec for test_group_b to {"name"=>"192.168.10.3", "port"=>22, "connection"=>"ssh", "uri"=>"192.168.10.3"} ##################################################################### Target host : 192.168.10.3 Target role : {roles}/{role00,role02} ##################################################3################## /usr/bin/ruby -I/usr/local/share/gems/gems/rspec-support-3.12.0/lib:/usr/local/share/gems/gems/rspec-core-3.12.1/lib /usr/local/share/gems/gems/rspec-core-3.12.1/exe/rspec --pattern \{roles\}/\{role00,role02\}/spec/\*_spec.rb /usr/local/share/gems/gems/specinfra-2.85.0/lib/specinfra/backend/ssh.rb:82:in `create_ssh': Passing nil, or [nil] to Net::SSH.start is deprecated for keys: user, keys Role "roles/role00" Test "check_service_enabled" Service "chronyd" is expected to be enabled Service "crond" is expected to be enabled Service "sshd" is expected to be enabled Role "roles/role02" Test "variable_file_exist" File "/tmp/role02.txt" is expected to be file Test "variable_file_contents" File "/tmp/role02.txt" content is expected to match "test_var_role_default: roles/role02/defaults/main.yml\ntest_var_group_all : inventories/production/group_vars/all.yml\ntest_var_group_parent: inventories/production/group_vars/test_group_ab.yml\ntest_var_group : inventories/production/group_vars/test_group_b.yml\ntest_var_host : inventories/production/host_vars/test-prd-003.yml\ntest_var_role : roles/role02/vars/main.yml\n" Finished in 1.19 seconds (files took 7.31 seconds to load) 5 examples, 0 failures
→出力が変わり、ホスト、ロール、テスト項目を探しやすくなったと思う。
テスト結果をファイル出力する
ここまで、テスト結果はコンソールに表示されたものを確認してきたが、実際の開発ではエビデンスが必要になる。
そのため、テスト結果をファイル出力する方法をいくつか試す。
方法1:普通にリダイレクトする
rakeの実行結果をファイルにリダイレクトする。
コンソールに表示されていた結果をそのまま保存するだけでいいならこれで十分。
ただ、対象のホストやテスト項目が大量だと、見づらいという問題はある。
# mkdir -p audit/documentation # rake INVENTORY=inventories/production/hosts VARS_DIRS_PATH=inventories/production LOGIN_PASSWORD="SSHの認証パスワード" IGNORE_FAIL=1 all > ./audit/documentation/$(date +%Y%m%d-%H%M%S)-production-all.log # ls -l ./audit/documentation/ 合計 8 -rw-r--r-- 1 root root 5042 3月 7 05:26 20230307-052630-production-all.log
方法2:ホスト単位でファイル出力する
こちらを参考にさせていただいて、Rakefileを以下のように変更する。
Rakefileに以下を追記する(追記場所は後述)。
これで、環境変数LOG_DIRを指定すると、指定先に実行結果がファイル出力される。
# 実行結果をファイル出力する if ENV['LOG_DIR'] t.rspec_opts = "--format documentation -o #{ENV['LOG_DIR']}/#{ENV['TARGET_HOST']}.log" end
LOG_DIRを指定して実行する。
rakeコマンドの実行結果以外は変わらずコンソールに出力される。
# mkdir -p audit/documentation # rake INVENTORY=inventories/production/hosts VARS_DIRS_PATH=inventories/production LOGIN_PASSWORD="SSHの認証パスワード" IGNORE_FAIL=1 LOG_DIR=audit/documentation/ all Run serverspec for test_group_a to {"name"=>"192.168.10.1", "port"=>22, "connection"=>"ssh", "uri"=>"192.168.10.1"} ##################################################################### Target host : 192.168.10.1 Target role : {roles}/{role00,role01} ##################################################3################## /usr/bin/ruby -I/usr/local/share/gems/gems/rspec-support-3.12.0/lib:/usr/local/share/gems/gems/rspec-core-3.12.1/lib /usr/local/share/gems/gems/rspec-core-3.12.1/exe/rspec --pattern \{roles\}/\{role00,role01\}/spec/\*_spec.rb --format documentation -o audit/documentation//$(date +%Y%m%d-%H%M%S)-192.168.10.1.log /usr/local/share/gems/gems/specinfra-2.85.0/lib/specinfra/backend/ssh.rb:82:in `create_ssh': Passing nil, or [nil] to Net::SSH.start is deprecated for keys: user, keys Run serverspec for test_group_a to {"name"=>"192.168.10.2", "port"=>22, "connection"=>"ssh", "uri"=>"192.168.10.2"} ##################################################################### Target host : 192.168.10.2 Target role : {roles}/{role00,role01} ##################################################3################## /usr/bin/ruby -I/usr/local/share/gems/gems/rspec-support-3.12.0/lib:/usr/local/share/gems/gems/rspec-core-3.12.1/lib /usr/local/share/gems/gems/rspec-core-3.12.1/exe/rspec --pattern \{roles\}/\{role00,role01\}/spec/\*_spec.rb --format documentation -o audit/documentation//$(date +%Y%m%d-%H%M%S)-192.168.10.2.log /usr/local/share/gems/gems/specinfra-2.85.0/lib/specinfra/backend/ssh.rb:82:in `create_ssh': Passing nil, or [nil] to Net::SSH.start is deprecated for keys: user, keys Run serverspec for test_group_b to {"name"=>"192.168.10.3", "port"=>22, "connection"=>"ssh", "uri"=>"192.168.10.3"} ##################################################################### Target host : 192.168.10.3 Target role : {roles}/{role00,role02} ##################################################3################## /usr/bin/ruby -I/usr/local/share/gems/gems/rspec-support-3.12.0/lib:/usr/local/share/gems/gems/rspec-core-3.12.1/lib /usr/local/share/gems/gems/rspec-core-3.12.1/exe/rspec --pattern \{roles\}/\{role00,role02\}/spec/\*_spec.rb --format documentation -o audit/documentation//$(date +%Y%m%d-%H%M%S)-192.168.10.3.log /usr/local/share/gems/gems/specinfra-2.85.0/lib/specinfra/backend/ssh.rb:82:in `create_ssh': Passing nil, or [nil] to Net::SSH.start is deprecated for keys: user, keys
ログを確認すると、ホスト単位で出力されている。
# ls -l audit/documentation/ 合計 12 -rw-r--r-- 1 root root 879 3月 7 06:12 192.168.10.1.log -rw-r--r-- 1 root root 882 3月 7 06:11 192.168.10.2.log -rw-r--r-- 1 root root 882 3月 7 06:12 192.168.10.3.log
ホスト単位でどれくらいfailしたのか、以下のように見られるので、比較的結果が確認しやすそう。
# grep ".*examples,.*failures" audit/documentation/*.log audit/documentation/192.168.10.1.log:5 examples, 0 failures audit/documentation/192.168.10.2.log:5 examples, 0 failures audit/documentation/192.168.10.3.log:5 examples, 0 failures
ちなみに、formatを変えればJSON形式でも出力できる。
Rakefileを以下のように変更する。
# 実行結果をファイル出力する if ENV['LOG_DIR'] t.rspec_opts = "--format json -o #{ENV['LOG_DIR']}/#{ENV['TARGET_HOST']}.json" end
実行する。
# mkdir -p audit/json # rake INVENTORY=inventories/production/hosts VARS_DIRS_PATH=inventories/production LOGIN_PASSWORD="SSHの認証パスワード" IGNORE_FAIL=1 LOG_DIR=audit/json/ all (出力は省略) # ls -l audit/json/ 合計 12 -rw-r--r-- 1 root root 2560 3月 7 11:29 20230307-202931-192.168.10.1.json -rw-r--r-- 1 root root 2560 3月 7 11:29 20230307-202933-192.168.10.2.json -rw-r--r-- 1 root root 2559 3月 7 11:29 20230307-202936-192.168.10.3.json
中を見ると、JSON形式になっている。
プログラムに読み込ませて処理を行う場合などに使えそう。
# cat audit/json/20230307-202931-192.168.10.1.json | jq '.' { "version": "3.12.1", "examples": [ { "id": "./roles/role00/spec/main_spec.rb[1:1:1:1]", "description": "is expected to be enabled", "full_description": "Role \"roles/role00\" Test \"check_service_enabled\" Service \"chronyd\" is expected to be enabled", "status": "passed", "file_path": "./roles/role00/spec/main_spec.rb", "line_number": 8, "run_time": 0.090414959, "pending_message": null }, { "id": "./roles/role00/spec/main_spec.rb[1:1:2:1]", "description": "is expected to be enabled", "full_description": "Role \"roles/role00\" Test \"check_service_enabled\" Service \"crond\" is expected to be enabled", "status": "passed", "file_path": "./roles/role00/spec/main_spec.rb", "line_number": 8, "run_time": 0.086973199, "pending_message": null }, { "id": "./roles/role00/spec/main_spec.rb[1:1:3:1]", "description": "is expected to be enabled", "full_description": "Role \"roles/role00\" Test \"check_service_enabled\" Service \"sshd\" is expected to be enabled", "status": "passed", "file_path": "./roles/role00/spec/main_spec.rb", "line_number": 8, "run_time": 0.092661662, "pending_message": null }, { "id": "./roles/role01/spec/main_spec.rb[1:1:1:1]", "description": "is expected to be file", "full_description": "Role \"roles/role01\" Test \"variable_file_exist\" File \"/tmp/role01.txt\" is expected to be file", "status": "passed", "file_path": "./roles/role01/spec/main_spec.rb", "line_number": 7, "run_time": 0.07561173, "pending_message": null }, { "id": "./roles/role01/spec/main_spec.rb[1:2:1:1:1]", "description": "is expected to match \"test_var_role_default: roles/role01/defaults/main.yml\\ntest_var_group_all : inventories/production/group_vars/all.yml\\ntest_var_group_parent: inventories/production/group_vars/test_group_ab.yml\\ntest_var_group : inventories/production/group_vars/test_group_a.yml\\ntest_var_host : inventories/production/host_vars/test-prd-001.yml\\ntest_var_role : roles/role01/vars/main.yml\\n\"", "full_description": "Role \"roles/role01\" Test \"variable_file_contents\" File \"/tmp/role01.txt\" content is expected to match \"test_var_role_default: roles/role01/defaults/main.yml\\ntest_var_group_all : inventories/production/group_vars/all.yml\\ntest_var_group_parent: inventories/production/group_vars/test_group_ab.yml\\ntest_var_group : inventories/production/group_vars/test_group_a.yml\\ntest_var_host : inventories/production/host_vars/test-prd-001.yml\\ntest_var_role : roles/role01/vars/main.yml\\n\"", "status": "passed", "file_path": "./roles/role01/spec/main_spec.rb", "line_number": 22, "run_time": 0.076268357, "pending_message": null } ], "summary": { "duration": 0.424786489, "example_count": 5, "failure_count": 0, "pending_count": 0, "errors_outside_of_examples_count": 0 }, "summary_line": "5 examples, 0 failures" }
方法3:formatterを使ってCSV形式で出力する
rspec-coreのドキュメントを見ると、rspecの「--formatオプション」に指定できるのはdefault、documentation、json、html。
これら以外の形式にしたい場合は、Custom Formattersを定義して使用することになる。
試しに、こちらを参考にCSV出力するCustom Formattersを作成する。
serverspec_audit_formatter.rb
require 'rspec/core/formatters/documentation_formatter' require 'specinfra' require 'serverspec/version' require 'serverspec/type/base' require 'serverspec/type/command' require 'fileutils' require 'csv' class ServerspecAuditFormatter < RSpec::Core::Formatters::DocumentationFormatter RSpec::Core::Formatters.register self, :example_group_started, :example_passed, :example_pending, :example_failed def initialize(output) super @seq = 0 end def example_group_started(notification) @seq = 0 if @group_level == 0 super end def example_passed(notification) save_evidence(notification.example) super end def example_pending(notification) save_evidence(notification.example) super end def example_failed(notification) save_evidence(notification.example, notification.exception) super end # CSVファイルへの出力 def save_evidence(example, exception=nil) #test = example.metadata host = ENV['TARGET_HOST'] result_status = example.metadata[:execution_result].status location = example.metadata[:location].to_s full_description = example.metadata[:full_description] csv_data = [host, result_status, location, full_description] output_file = ENV['LOG_DIR'] + '/' + ENV['LOG_FILE'] CSV.open(output_file, "a") do |csv| csv << csv_data end end end
Rakefileに以下を追記する(追記場所は後述)。
これで、環境変数LOG_DIRとLOG_FILEを指定すると、「LOG_DIR/LOG_FILE」にCSV形式のファイルが出力されるようになる。
if ENV['LOG_DIR'] t.rspec_opts = "--format ServerspecAuditFormatter --require ./formatters/serverspec_audit_formatter.rb" end
環境変数LOG_DIRとLOG_FILEを指定して実行する。
# mkdir -p audit/csv # rake INVENTORY=inventories/production/hosts VARS_DIRS_PATH=inventories/production LOGIN_PASSWORD="SSHの認証パスワード" IGNORE_FAIL=1 LOG_DIR=audit/csv/ LOG_FILE=$(date +%Y%m%d-%H%M%S)-production-all.log all (コンソールにはdocumentation形式で結果が出力されるが省略) # ls -l audit/csv/ 合計 4 -rw-r--r-- 1 root root 3333 3月 7 15:14 20230307-151401-production-all.log
中身が想定通りCSV形式になっている。
# head -n 3 audit/csv/20230307-151401-production-all.log 192.168.10.1,passed,./roles/role00/spec/main_spec.rb:8,"Role ""roles/role00"" Test ""check_service_enabled"" Service ""chronyd"" is expected to be enabled" 192.168.10.1,passed,./roles/role00/spec/main_spec.rb:8,"Role ""roles/role00"" Test ""check_service_enabled"" Service ""crond"" is expected to be enabled" 192.168.10.1,passed,./roles/role00/spec/main_spec.rb:8,"Role ""roles/role00"" Test ""check_service_enabled"" Service ""sshd"" is expected to be enabled"
メッセージ「Passing nil, or [nil] to Net::SSH.start is deprecated for keys: user, keys」を抑制する(未解決)
Serverspecの実行結果の中で、ホストに対してSSHする箇所で毎回以下メッセージが表示されている。
邪魔なので出力されないようにしたいのだが、方法が分からなかったので一旦放置している。
/usr/local/share/gems/gems/specinfra-2.85.0/lib/specinfra/backend/ssh.rb:82:in `create_ssh': Passing nil, or [nil] to Net::SSH.start is deprecated for keys: user, keys
最終的なディレクトリ構成・ファイル
最終的なディレクトリ構成・ファイルは以下の通り。
ここに置いてある。
ディレクトリ構成
ansible |-- .ansiblespec # ansiblespec-initで作成される |-- .rspec # ansiblespec-initで作成される |-- Rakefile # ansiblespec-initで作成される |-- ansible.cfg |-- audit # Serverspecの結果ファイル出力先 | |-- csv | |-- documentation | `-- json |-- formatters # ServerspecのCustom Formatters置き場 | `-- serverspec_audit_formatter.rb |-- inventories | |-- production | | |-- group_vars | | | |-- all.yml | | | |-- test_group_a.yml | | | |-- test_group_ab.yml | | | `-- test_group_b.yml | | |-- host_vars | | | |-- 192.168.10.1.yml | | | |-- 192.168.10.2.yml | | | `-- 192.168.10.3.yml | | `-- hosts | `-- staging | |-- group_vars | | |-- all.yml | | |-- test_group_a.yml | | |-- test_group_ab.yml | | `-- test_group_b.yml | |-- host_vars | | `-- 192.168.20.1.yml | `-- hosts |-- roles | |-- role00 | | |-- defaults | | | `-- main.yml | | |-- spec # specファイル置き場 | | | `-- main_spec.rb | | `-- tasks | | `-- main.yml | |-- role01 | | |-- defaults | | | `-- main.yml | | |-- spec | | | `-- main_spec.rb | | |-- tasks | | | `-- main.yml | | |-- templates | | | `-- variable.txt.j2 | | `-- vars | | `-- main.yml | `-- role02 | |-- defaults | | `-- main.yml | |-- spec | | `-- main_spec.rb | |-- tasks | | `-- main.yml | |-- templates | | `-- variable.txt.j2 | `-- vars | `-- main.yml |-- site.yml |-- spec # ansiblespec-initで作成される | `-- spec_helper.rb # ansiblespec-initで作成される |-- test_group_a.yml |-- test_group_ab.yml `-- test_group_b.yml
ファイル
ここにはRakefiletとspec_helper.rbのみ記載する。
他のファイルはここに格納しているもの参照。
Rakefile
require 'rake' require 'rspec/core/rake_task' require 'yaml' require 'ansible_spec' properties = AnsibleSpec.get_properties # {"name"=>"Ansible-Sample-TDD", "hosts"=>["192.168.0.103","192.168.0.103"], "user"=>"root", "roles"=>["nginx", "mariadb"]} # {"name"=>"Ansible-Sample-TDD", "hosts"=>[{"name" => "192.168.0.103:22","uri"=>"192.168.0.103","port"=>22, "private_key"=> "~/.ssh/id_rsa"}], "user"=>"root", "roles"=>["nginx", "mariadb"]} cfg = AnsibleSpec::AnsibleCfg.new desc "Run serverspec to all test" task :all => "serverspec:all" namespace :serverspec do properties = properties.compact.reject{|e| e["hosts"].length == 0} task :all => properties.map {|v| 'serverspec:' + v["name"] } properties.each_with_index.map do |property, index| property["hosts"].each do |host| desc "Run serverspec for #{property["name"]}" RSpec::Core::RakeTask.new(property["name"].to_sym) do |t| puts "Run serverspec for #{property["name"]} to #{host}" ENV['TARGET_HOSTS'] = host["hosts"] ENV['TARGET_HOST'] = host["uri"] ENV['TARGET_PORT'] = host["port"].to_s ENV['TARGET_GROUP_INDEX'] = index.to_s ENV['TARGET_PRIVATE_KEY'] = host["private_key"] unless host["user"].nil? ENV['TARGET_USER'] = host["user"] else ENV['TARGET_USER'] = property["user"] end ENV['TARGET_PASSWORD'] = host["pass"] ENV['TARGET_CONNECTION'] = host["connection"] roles = property["roles"] for role in property["roles"] for rolepath in cfg.roles_path deps = AnsibleSpec.load_dependencies(role, rolepath) if deps != [] roles += deps break end end end # 実行ホスト名、実行ロールを表示する puts "#####################################################################" puts " Target host : #{host["name"]}" puts " Target role : {#{cfg.roles_path.join(',')}}/{#{roles.join(',')}}" puts "##################################################3##################" t.pattern = '{' + cfg.roles_path.join(',') + '}/{' + roles.join(',') + '}/spec/*_spec.rb' # 実行結果をファイル出力する。使いたいフォーマットのコメントアウトを外すこと if ENV['LOG_DIR'] # Documentation t.rspec_opts = "--format documentation -o #{ENV['LOG_DIR']}/$(date +%Y%m%d-%H%M%S)-#{ENV['TARGET_HOST']}.log" # JSON #t.rspec_opts = "--format json -o #{ENV['LOG_DIR']}/$(date +%Y%m%d-%H%M%S)-#{ENV['TARGET_HOST']}.json" # CSV #t.rspec_opts = "--format ServerspecAuditFormatter --require ./formatters/serverspec_audit_formatter.rb" end # 環境変数IGNORE_FAILが1の場合、途中でfailしてもテストを続ける if ENV['IGNORE_FAIL'] == "1" t.fail_on_error = false end end end end end
require 'serverspec' require 'net/ssh' require 'ansible_spec' require 'winrm' require 'highline/import' # SSHのパスワード認証を使うため追加 # # Set ansible variables to serverspec property # host = ENV['TARGET_HOST'] hosts = ENV["TARGET_HOSTS"] group_idx = ENV['TARGET_GROUP_INDEX'].to_i vars = AnsibleSpec.get_variables(host, group_idx,hosts) ssh_config_file = AnsibleSpec.get_ssh_config_file set_property vars connection = ENV['TARGET_CONNECTION'] case connection when 'ssh' # # OS type: UN*X # set :backend, :ssh # Ansible use `BECOME`, But Serverspec use `SUDO`. if ENV['ASK_BECOME_PASSWORD'] begin require 'highline/import' rescue LoadError fail "highline is not available. Try installing it." end set :sudo_password, ask("Enter become password: ") { |q| q.echo = false } else set :sudo_password, ENV['BECOME_PASSWORD'] end options = Net::SSH::Config.for(host) options[:user] = ENV['TARGET_USER'] || options[:user] options[:port] = ENV['TARGET_PORT'] || options[:port] options[:keys] = ENV['TARGET_PRIVATE_KEY'] || options[:keys] if ssh_config_file from_config_file = Net::SSH::Config.for(host,files=[ssh_config_file]) options.merge!(from_config_file) end # SSHのパスワード認証を使うため追加 if ENV['ASK_LOGIN_PASSWORD'] options[:password] = ask("\nEnter login password: ") { |q| q.echo = false } else options[:password] = ENV['LOGIN_PASSWORD'] end set :host, options[:host_name] || host set :ssh_options, options # Disable become (Serverspec use sudo) # set :disable_sudo, true # Set environment variables # set :env, :LANG => 'C', :LC_MESSAGES => 'C' # Set PATH # set :path, '/sbin:/usr/local/sbin:$PATH' when 'winrm' # # OS type: Windows # set :backend, :winrm set :os, :family => 'windows' user = ENV['TARGET_USER'] port = ENV['TARGET_PORT'] pass = ENV['TARGET_PASSWORD'] if user.nil? begin require 'highline/import' rescue LoadError fail "highline is not available. Try installing it." end user = ask("\nEnter #{host}'s login user: ") { |q| q.echo = true } end if pass.nil? begin require 'highline/import' rescue LoadError fail "highline is not available. Try installing it." end pass = ask("\nEnter #{user}@#{host}'s login password: ") { |q| q.echo = false } end endpoint = "http://#{host}:#{port}/wsman" winrm = ::WinRM::WinRMWebService.new(endpoint, :ssl, :user => user, :pass => pass, :basic_auth_only => true) winrm.set_timeout 300 # 5 minutes max timeout for any operation Specinfra.configuration.winrm = winrm when 'local' # # local connection # set :backend, :exec end
補足
注意:インベントリファイルでグループのネスト(親子グループ)を使用している場合は記載順に気を付ける
Ansibleで、以下のようにグループのネストをしている場合があると思う。
inventories/production/hosts
[test_group_ab:children] test_group_a test_group_b [test_group_a] #test-prd-001 192.168.10.1 #test-prd-002 192.168.10.2 [test_group_b] #test-prd-003 192.168.10.3
test_group_ab.yml
--- - name: test_group_ab hosts: test_group_ab roles: - role: role00 tags: role00 - role: role01 tags: role01
この記載でAnsibleは問題なく実行できる。
しかし、Serverspecを実行すると、以下のように「TypeError: no implicit conversion of nil into Array」というエラーになる。
# rake INVENTORY=inventories/production/hosts VARS_DIRS_PATH=inventories/production PLAYBOOK=test_group_ab.yml -T rake aborted! TypeError: no implicit conversion of nil into Array /usr/local/share/gems/gems/ansible_spec-0.3.2/lib/ansible_spec/load_ansible.rb:112:in `+' /usr/local/share/gems/gems/ansible_spec-0.3.2/lib/ansible_spec/load_ansible.rb:112:in `block in get_parent' /usr/local/share/gems/gems/ansible_spec-0.3.2/lib/ansible_spec/load_ansible.rb:111:in `each' /usr/local/share/gems/gems/ansible_spec-0.3.2/lib/ansible_spec/load_ansible.rb:111:in `get_parent' /usr/local/share/gems/gems/ansible_spec-0.3.2/lib/ansible_spec/load_ansible.rb:86:in `block in load_targets' /usr/local/share/gems/gems/ansible_spec-0.3.2/lib/ansible_spec/load_ansible.rb:80:in `each' /usr/local/share/gems/gems/ansible_spec-0.3.2/lib/ansible_spec/load_ansible.rb:80:in `load_targets' /usr/local/share/gems/gems/ansible_spec-0.3.2/lib/ansible_spec/load_ansible.rb:425:in `get_properties' /work/ansible/Rakefile:6:in `<top (required)>' /usr/local/share/gems/gems/rake-13.0.6/exe/rake:27:in `<top (required)>' (See full trace by running task with --trace)
→エラーメッセージを見ると、ansible_specがAnsibleのから親子グループのあたりの情報を取得する部分で失敗しているように見える。
このエラーは、親グループの定義が子グループの後になるよう、インベントリファイルの記載順を以下のように修正したら解消した。
inventories/production/hosts
[test_group_a] #test-prd-001 192.168.10.1 #test-prd-002 192.168.10.2 [test_group_b] #test-prd-003 192.168.10.3 [test_group_ab:children] test_group_a test_group_b
Serverspecが正常に実行できるようになる。
# rake INVENTORY=inventories/production/hosts VARS_DIRS_PATH=inventories/product ion PLAYBOOK=test_group_ab.yml -T rake all # Run serverspec to all test rake serverspec:test_group_ab # Run serverspec for test_group_ab
親グループの定義を子グループより後に記載しないと、ansible_specが動作しないことが分かった。
Ansibleだけを使っているときは意識していない点だと思うので、気を付ける。
注意:ansible.cfgファイルのコメントに日本語を使用するとエラーになる
ansible.cfgで以下のように日本語のコメントを入れている場合、Ansibleは問題なく実行できる。
ansible.cfg
[defaults] # 鍵のチェックを無効 host_key_checking = False
test_group_ab.yml
--- - name: test_group_ab hosts: test_group_ab roles: - role: role00 tags: role00 - role: role01 tags: role01
しかし、Serverspecを実行すると、以下のように「ArgumentError: invalid byte sequence in US-ASCII」というエラーになる。
# rake INVENTORY=inventories/production/hosts VARS_DIRS_PATH=inventories/production -T rake aborted! ArgumentError: invalid byte sequence in US-ASCII /usr/local/share/gems/gems/inifile-3.0.0/lib/inifile.rb:522:in `===' /usr/local/share/gems/gems/inifile-3.0.0/lib/inifile.rb:522:in `block in parse' /usr/local/share/gems/gems/inifile-3.0.0/lib/inifile.rb:515:in `each_line' /usr/local/share/gems/gems/inifile-3.0.0/lib/inifile.rb:515:in `parse' /usr/local/share/gems/gems/inifile-3.0.0/lib/inifile.rb:400:in `parse' /usr/local/share/gems/gems/inifile-3.0.0/lib/inifile.rb:128:in `block in read' /usr/local/share/gems/gems/inifile-3.0.0/lib/inifile.rb:128:in `open' /usr/local/share/gems/gems/inifile-3.0.0/lib/inifile.rb:128:in `read' /usr/local/share/gems/gems/inifile-3.0.0/lib/inifile.rb:80:in `initialize' /usr/local/share/gems/gems/ansible_spec-0.3.2/lib/ansible_spec/load_ansible.rb:608:in `new' /usr/local/share/gems/gems/ansible_spec-0.3.2/lib/ansible_spec/load_ansible.rb:608:in `block in load_ansible_cfg' /usr/local/share/gems/gems/ansible_spec-0.3.2/lib/ansible_spec/load_ansible.rb:607:in `each' /usr/local/share/gems/gems/ansible_spec-0.3.2/lib/ansible_spec/load_ansible.rb:607:in `load_ansible_cfg' /usr/local/share/gems/gems/ansible_spec-0.3.2/lib/ansible_spec/load_ansible.rb:585:in `initialize' /work/ansible/Rakefile:9:in `new' /work/ansible/Rakefile:9:in `<top (required)>' /usr/local/share/gems/gems/rake-13.0.6/exe/rake:27:in `<top (required)>' (See full trace by running task with --trace)
→ansible_specがansible.cfgを読み込む際に、日本語(たぶんUS-ASCIIに無い文字全て)が含まれているとエラーになる模様。
日本語が書かれている場合は消しておくこと。
(ちなみにRakefile、spec_helper.rb、.rspec、.ansiblespecやAnsibleのyamlには日本語コメントを入れても特にエラーにならなかった。)
まとめ
- ansible_specを導入することで、Ansibleのホスト、ロール、変数情報をServerspecに連携することができ、情報の2重管理の手間がなくなり、ディレクトリ構成も分かりやすいものに出来た。
- ansible_specの使用にあたり、以下に気を付けること。
- Ansibleのディレクトリ構成に合わせて、INVENTORY、PLAYBOOK、VARS_DIRS_PATHといったansible_specの環境変数を適切に設定する。
- プレイブックの「name:」の値は必ず設定し、値は「hosts:」の値と同じにする。
少なくとも、スペースなど特殊な文字や、他のプレイブックのnameと重複する値は避ける。 - Ansibleのマジック変数は使用できない可能性が高い。
- Ansibleとansible_specで変数の読み込まれ方を一致させるため、hash_behaviourは同じ設定にする。
- ホスト変数(host_vars配下のyaml)は、ansible_specでは最終的にServerspecがSSH接続する際に指定する文字列(環境によりFQDNだったりIPアドレスだったりする)に一致する名前のyamlしか読み込まれないので、インベントリファイルでansible_host変数を使っている場合は注意する。
- インベントリファイルでグループのネスト(親子グループ)を使用している場合は、親グループの定義を子グループより後に記載しないとエラーになるので、記載順に注意する。
- ansible.cfgに日本語などUS-ASCIIに無い文字が含まれるとエラーになるため、コメント記載時などは注意する。