はじめに
Ansibleで設定したLinuxサーバ群に対して、serverspecでテストする際に抱えていた以下のような問題を解決するために、Ansibleとserverspecを魔改造(?)してみました。
- Ansibleとserverspecそれぞれで変数ファイルを用意するのが面倒
- Ansibleとserverspecそれぞれで対象ノードを記述したインベントリファイルを用意するのが面倒
-
serverspec-init
で提供される初期設定では、テストコードをテスト対象サーバ名のついたディレクトリ毎に用意する必要があり面倒
Ansibleとserverspecを連携させて活用する際の入門編・初級編として、参考になさってください。
ゴール
以下が実現できるように実装します。
- Ansible のインベントリを serverspecでも使い回す
- Ansible Playbook実行時の変数を使って、serverspecでテストする
- Ansibleの変数は指定方法によって優先度が決められているので、最終的にノードに対してPlaybookが実行された際に使われた変数で、serverspecでテストする
- 複数ノードに対して、Ansibleしてserverspecする
- serverspecはロールごとにテストコードを分ける
実装例
環境
Ansible Control Node
- CentOS 7.6
- ansible 2.9.0
- serverspec 2.41.5
- ruby 2.6.3p62
- Python 2.7.5
Ansible Managed Node
- CentOS 7.6 × 2 Node
ディレクトリ構成
$ tree -aF /autotools
/autotools
|-- .ssh/
| `-- aws_key.pem # ManagedNodeのSSH秘密鍵
|-- ansible/
| |-- ansible.cfg
| |-- group_vars/ # グループ用変数ディレクトリ
| |-- host_vars/ # ホスト用変数ディレクトリ
| |-- inventory/ # Ansible向けインベントリ配置ディレクトリ
| `-- centos.yml # Playbook
`-- serverspec/
|-- .rspec
|-- Rakefile
|-- spec/
| |-- base/ # baseロール向けのテストコート配置ディレクトリ
| | `-- sample_spec.rb # テストコード
| `-- spec_helper.rb
`-- spec_hosts/ # serverspec向け変数配置ディレクトリ
Ansible
まず今回のサーバ群をまとめて管理する目的で、project_name
を英文字列で決めましょう。
ここでは例として anken
という project_name
にします。
ansible.cfg
ここでは、環境変数 ANSIBLE_CONFIG
で使用する ansible.cfg
を指定します。
私はコード開発で同じホスト名を使いまわしているので、ssh時の引数をここに記載しています。
$ export ANSIBLE_CONFIG=/autotools/ansible/ansible.cfg
[defaults]
[ssh_connection]
ssh_args = -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no
[privilege_escalation]
become = true
SSH秘密鍵
Ansible Managed NodeにSSHする際に使うSSH秘密鍵を /autotools/.ssh
に配置します。
鍵のPathはインベントリファイルに記載します。
インベントリファイル
/autotools/ansible/inventory/
配下にインベントリファイル anken.ini
を配置してください。
基本的にAnsibleのルールに従って記述すればOKですが、serverspecと連携するための仕組みで使うので、以下の3つは必須で設定してください。
- ansible対象ノードはすべて プロジェクト名のグループに所属させてください
-
[all:vars]
でproject_name
を指定してください - ansibleが 対象ノードにssh loginする際に使うユーザID、パスワード or SSH鍵を、それぞれ
ansible_user
、ansible_password
、ansible_ssh_private_key_file
で設定してください。パスワードとSSH鍵はどちらか1つを設定してください。(ここに書いたパスワードとSSH鍵は、後述する変数ファイルを通じて、serverspecでも利用されます)
[anken]
prod_foobar1 ansible_host=xx.xx.xx.xx
dev_foobar1 ansible_host=yy.yy.yy.yy
[anken:vars]
ansible_user=centos
ansible_ssh_private_key_file=~/.ssh/aws_key.pem
[all:vars]
project_name=anken
Ansibleの実行は、ansible_host
のIPもしくは名前に対して実行されます。
inventory_hostname
(例で prod_foobar1
dev_foobar2
を指定している箇所)は、ノードの実際のホスト名と一致する必要はありません。
Playbook
この実装例で使うPlaybook例は以下の通りです。
name: Configure for serverspec at localhost
で、serverspecが利用する変数ファイルを出力しています。
---
- name: Playbook for centos7 managed node
hosts: all
gather_facts: true
tasks:
- name: Create group
group:
name: "{{ item.name }}"
gid: "{{ item.gid }}"
loop: "{{ group }}"
tags: group
- name: Create User
user:
name: "{{ item.name }}"
uid: "{{ item.uid }}"
group: "{{ item.group }}"
groups: "{{ item.groups }}"
home: "{{ item.home }}"
shell: "{{ item.shell }}"
loop: "{{ user }}"
tags: user
- name: System service
systemd:
name: "{{ item.name }}"
enabled: "{{ item.enabled }}"
state: "{{ item.state }}"
loop: "{{ service }}"
tags: service
- name: Configure for serverspec at localhost
hosts: localhost
connection: local
gather_facts: false
tasks:
- name: Dump hostvars for serverspec
copy:
content: "{{ hostvars | to_nice_yaml }}"
dest: "../serverspec/spec_hosts/{{ project_name }}.yml"
tags: serverspec
変数ファイル配置
projectに共通な変数は /autotools/ansible/group_vars/#{project_name}.yml
に配置します。特定のinventory_hostnameにだけ別の変数を指定したい場合は、 /autotools/ansible/host_vars/#{inventory_name}.yml
に配置します。
変数ファイル内で、serverspecでテストする際に使用するroleを、serverspec_role
で指定してください。
serverspec_role:
- base
group:
- name: unyo
gid: 1101
- name: infra
gid: 1102
- name: app
gid: 1103
user:
- name: user1
uid: 2001
group: customer
groups: [ unyo, infra]
home: /home/user1
shell: /bin/bash
- name: user2
uid: 2002
group: customer
groups: [ app ]
home: /home/user2
shell: /bin/bash
- name: user3
uid: 2003
group: customer
groups: [ app, infra ]
home: /home/user3
shell: /bin/bash
service:
- name: chronyd.service
enabled: false
state: stopped
- name: rsyncd.service
enabled: true
state: started
ここでは、prod_foobar
ノードに限定して、一部の変数を上書きしてみます。
group:
- name: unyo
gid: 2101
- name: infra
gid: 2102
- name: app
gid: 2103
ディレクトリ・ファイル構成
必要なファイルを配置したあと、こんな感じになってるはずです。
$ tree /autotools/ansible -aF
/autotools/ansible
|-- ansible.cfg
|-- centos.yml
|-- group_vars/
| `-- anken.yml
|-- host_vars/
| `-- prod_foobar1
`-- inventory/
`-- anken.ini
実行
以下のように、インベントリファイルを指定して centos.yml Playbookを実行してください。
$ cd /autotools/ansible
$ ansible -i ./inventory/anken centos.yml
serverspec
ディレクトリ・ファイル構成
Ansible実行後、serverspec側のディレクトリ・ファイル構成はこんな感じになってるはずです。
# tree /autotools/serverspec -aF
/autotools/serverspec
|-- .rspec
|-- Rakefile
|-- spec/
| |-- base/
| | `-- sample_spec.rb
| `-- spec_helper.rb
`-- spec_hosts/
`-- anken.yml # Ansibleによって生成された変数ファイル
実行コマンド
順番が前後しますが、serverspecの実行コマンドは以下になります。
serverspecで使用する(Ansibleによって生成された)変数ファイル名を、rakeコマンドに引数として渡してあげるイメージです。
$ rake spec anken -T
rake spec # Run spec to all hosts
rake spec:dev_foobar1 # Run spec to dev_foobar1
rake spec:prod_foobar1 # Run spec to prod_foobar1
$ rake spec anken
Rakefile
serverspec-init
で作られる標準の Rakefile
からいくつか変更しています。
- Ansibleが生成した 変数ファイルを読み込むための処理を追加
- serverspec_role と同じ名前のディレクトリ配下の *_spec.rb ファイルを読み込み
- rakeコマンドの引数が rakeタスクと誤認されてエラーになっちゃうので、引数と同じ名前の空タスク作成
require 'rake'
require 'rspec/core/rake_task'
require 'yaml'
# 変数ファイルを読み込み
project_name = ARGV[1]
hosts = YAML.load_file("./spec_hosts/#{project_name}.yml")
desc "Run spec to all hosts"
task :spec => 'spec:all'
namespace :spec do
task :all => hosts.keys.map {|key| 'spec:' + key }
hosts.keys.each do |key|
desc "Run spec to #{key}"
RSpec::Core::RakeTask.new(key.to_sym) do |t|
ENV['INVENTORY_HOST'] = key
ENV['PROJECT_NAME'] = project_name
# serverspec_role と同じ名前のディレクトリ配下の *_spec.rb ファイルを読み込み
t.pattern = 'spec/{' + hosts[key]['serverspec_role'].join(',') + '}/*_spec.rb'
t.fail_on_error = false
end
end
end
# rakeコマンドの引数を空タスクとして偽造
ARGV.slice(1,ARGV.size).each{|v| task v.to_sym do; end}
以下参考にさせていただきました。ありがとうございました。
参考:Rakeタスクで普通の引数っぽい処理を書く
https://qiita.com/nao58/items/aa50514d97f05eb8d128
参考:公式 How to use host specific properties
https://serverspec.org/advanced_tips.html
spec_helper.rb
こちらも初期状態の spec_helper.rb
からいくつか機能を変更しています。
- Ansibleが生成した 変数ファイルを読み込むための処理を追加
- 読み込んだ変数ファイルから、Ansibleで使った host、user、password or key を抽出
require 'serverspec'
require 'pathname'
require 'net/ssh'
require 'yaml'
# 変数ymlファイル読み込み
key = ENV['INVENTORY_HOST']
project_name = ENV['PROJECT_NAME']
properties = YAML.load_file("./spec_hosts/#{project_name}.yml")
set_property properties["#{key}"]
set :backend, :ssh
set :path, '/sbin:/usr/sbin:$PATH'
# ssh実行部
RSpec.configure do |c|
c.before :all do
# 読み込んだ変数ファイルから、Ansibleで使った host、user、password or key を抽出
set :host, property['ansible_host']
options = Net::SSH::Config.for(c.host)
options[:user] = property['ansible_user']
if property['ansible_password']
options[:password] = property['ansible_password']
else
options[:keys] = [ property['ansible_ssh_private_key_file'] ]
end
options[:user_known_hosts_file] = '/dev/null'
set :ssh_options, options
end
end
set :backend, :ssh
としているので、この spec_helper.rb
は WinRM には対応していません。とはいえ、Ruby でなんでも書けるので、きっと Windows対応も難しくはないでしょう。
テストコード
こちらはサンプルです。
spec_helper.rb
で書いているとおり、property['xxx']
で変数ファイルから変数を取り出して再利用可能です。
# frozen_string_literal: true
require 'spec_helper'
puts "\nRun serverspec to #{property['inventory_hostname']}"
property['group'].each do |attr|
describe group(attr['name']) do
it { should exist }
it { should have_gid attr['gid'] }
end
end
property['user'].each do |attr|
describe user(attr['name']) do
it { should exist }
it { should have_uid attr['uid'] }
it { should belong_to_group attr['group'] }
end
end
property['service'].each do |attr|
describe service(attr['name']) do
attr['enabled'] ? it { should be_enabled } : it { should_not be_enabled }
attr['state'] == 'started' ? it { should be_running } : it { should_not be_running }
end
end
実行
繰り返しになりますが、以下のように、rake spec コマンドの引数に Ansibleが生成した変数ファイル名を引数としてつけて実行してあげてください。一台ごとのテスト実行も可能です。
$ rake spec anken
$
$ rake spec anken -T # タスク一覧を表示するコマンド
rake spec # Run spec to all hosts
rake spec:dev_foobar1 # Run spec to dev_foobar1
rake spec:prod_foobar1 # Run spec to prod_foobar1
$
$ rake spec:dev_foobar1 anken
まとめ
Ansible と serverspec で二重管理になりがちな 変数ファイルとイベントリファイル を一本化することができました。また、serverspec公式を記載の方法を参考に、ロール単位でテストコードを管理・実行することができました。
serverspec は かなり Ruby色なツールなので、普段Rubyに触れていない方にはとっつきにくいですが、慣れてくると、いろんな処理が書きやすくて良いですね。
サンプルコード
以下で公開しています。
https://github.com/kentarok/autotools