結論としては、Serverspecは充分使えると判断してAnsible Playbookで実行した内容のテストをServerspecで記述して、構築完了後〜何か変更を加えるときに毎回`rake`を実行するという形が合理的であるとの解を得ました。
となると次は、ディレクトリ名からホスト名を得るServerspecにAnsibleのインベントリファイルを読込んで、対象とするホスト定義を共通化したいと思いました。最終的な目的は、Ansibleのインベントリファイルのみでホスト定義をしてServerspecもそれを読み、AnsibleのBest Practicesに従ったディレクトリ構造の中にServerspecのテストコード用ファイルを入れる事で、それを達成する為にRakefileを改造しました。
make-server
自分で使うAnsibleのロールやPlaybookの動作テスト用にいつでもVagrant環境を準備出来るフレームワーク的な何かをmake-serverという名前でGitHubに置いてまして、今回はそれを大改造しました。
改造項目
- ansible/rolesってディレクトリ名を変える→server/roles
- roles/以下を構築手段別に再分類する→roles/{rpm,pkg,deb,src,env}とか
- rake実行時にインベントリファイルを読込む→Rakefile改造とlib/ansible_helper.rb作成
- roles/ロール名/spec/*.rbに入れたテストコードをrakeで実行→lib/ansible_helper.rb作成
改造後
全体のディレクトリ構造
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
mx.nyaan.jp/ | |
├── Ansible.mk ------- Ansible用Makefile | |
├── Makefile --------- いろいろ便利ターゲットがある | |
├── NodeLocal.mk ----- このディレクトリ特有のターゲットを書く | |
├── Rakefile --------- 改造したRakefile | |
├── Serverspec.mk ---- Serverspec用Makefile | |
├── Vagrant.mk ------- Vagrant用Makefile | |
├── ansible.cfg -> ./server/ansible-config | |
├── lib/ | |
│ ├── Makefile | |
│ ├── ansible_helper.rb --- インベントリを読込んだりいろいろ | |
│ └── spec_helper.rb ------ 殆ど改造してない | |
├── server/ | |
│ ├── 10-build-stage.yml -- Python入れたりsudo設定したりするPlaybook | |
│ ├── 11-selinux-off.yml -- setenforce 0 | |
│ ├── 20-deploy-user.yml -- メインのPlaybookを実行するユーザを作る | |
│ ├── 21-setup-repos.yml -- EPELとか入れる | |
│ ├── 22-add-network.yml -- eth1とか定義する | |
│ ├── 30-update-sshd.yml -- SSHdのポートを変えたり | |
│ ├── 41-vagrant-uid.yml -- vagrantユーザのuidを500以外に変更する | |
│ ├── 49-make-sslkey.yml -- 秘密鍵とかCSRとか作る | |
│ ├── 50-make-server.yml -- サンプル | |
│ ├── Makefile | |
│ ├── ansible-config ------ Ansibleの設定ファイル | |
│ ├── build-machines.yml -- メインのPlaybook | |
│ ├── group_vars/ | |
│ │ └── all ------------ 共通変数を定義する | |
│ ├── develop ------------- 開発機用インベントリ | |
│ ├── install ------------- rootで入る初期構築準備用インベントリ | |
│ ├── product ------------- 本番サーバ用インベントリ | |
│ ├── sandbox ------------- 練習サーバ用インベントリ | |
│ ├── staging ------------- ステージングサーバ用インベントリ | |
│ ├── log ----------------- ansible-configで指定するログファイル | |
│ ├── roles/ -------------- このディレクトリにロールを入れる | |
│ │ ├── Makefile | |
│ │ ├── bootstrap/ ----- メインのPlaybookで最初に実行するロール | |
│ │ ├── cleandown/ ----- メインのPlaybookで最後に実行するロール | |
│ │ ├── env/ ----------- 環境設定をするロールはここに入れる | |
│ │ │ └── selinux/ -- SELinuxをどうにかするロール | |
│ │ │ ├── defaults/ | |
│ │ │ ├── files/ | |
│ │ │ ├── handlers/ | |
│ │ │ │ └── main.yml | |
│ │ │ ├── meta/ | |
│ │ │ │ └── main.yml | |
│ │ │ ├── spec/ ----------------- Serverspecのテストコードを入れる | |
│ │ │ │ └── make-config.rb --- make-config.ymlの実行内容をテストする | |
│ │ │ ├── tasks/ | |
│ │ │ │ ├── main.yml --------- 他の*.ymlを読込むだけ | |
│ │ │ │ └── make-config.yml -- SELinuxを無効にするタスク | |
│ │ │ ├── templates/ | |
│ │ │ └── vars/ | |
│ │ │ └── main.yml | |
│ │ ├── rpm/ --------------------------- RPMで入れる何かはこのディレクトリ以下 | |
│ │ │ └── ruby/ --------------------- RPMでRubyを入れるロール | |
│ │ │ ├── handlers/ | |
│ │ │ │ └── main.yml | |
│ │ │ ├── meta/ | |
│ │ │ │ └── main.yml | |
│ │ │ ├── spec/ ----------------- Serverspecのテストコードを入れる | |
│ │ │ │ └── install-pkg.rb -- /usr/bin/rubyがあるかテスト | |
│ │ │ ├── tasks/ | |
│ │ │ │ ├── install-pkg.yml -- yum install rubyを実行 | |
│ │ │ │ └── main.yml --------- 他の*.ymlを読込むだけ | |
│ │ │ └── vars/ | |
│ │ │ └── main.yml | |
│ │ └── src/ --------------------------- ソースビルドで入れる何かを入れる | |
│ │ └── nginx/ --------------------- nginxをソースから入れるロール | |
│ │ ├── handlers/ | |
│ │ │ └── main.yml | |
│ │ ├── spec/ ------------------ tasks/*.ymlの実行結果テスト用 | |
│ │ │ ├── boot-script.rb ---- tasks/boot-script.ymlの結果をテスト | |
│ │ │ ├── compile-src.rb ---- tasks/compile-src.ymlの〃 | |
│ │ │ ├── create-user.rb ---- tasks/create-user.ymlの〃 | |
│ │ │ ├── install-pkg.rb ---- tasks/install-pkg.ymlの〃 | |
│ │ │ └── make-config.rb ---- tasks/make-config.ymlの〃 | |
│ │ ├── tasks/ | |
│ │ │ ├── boot-script.yml --- /etc/init.d/nginxを設置するなど | |
│ │ │ ├── compile-src.yml --- nginxをコンパイルしてインストール | |
│ │ │ ├── create-user.yml --- nginx用ユーザの作成とか | |
│ │ │ ├── get-archive.yml --- nginxのTar玉をダウンロードして開ける | |
│ │ │ ├── install-pkg.yml --- nginxのコンパイル前に必要なものを入れる | |
│ │ │ ├── main.yml ---------- 他の*.ymlをincludeで読込む | |
│ │ │ └── make-config.yml --- nginx.confとか各種設定をする | |
│ │ ├── templates/ | |
│ │ └── vars/ | |
│ │ └── main.yml ----------- nginx関連変数を定義 | |
│ └── spec/ ------------------------------- server/*.ymlのテスト用 | |
│ ├── 10-build-stage.rb -------------- server/10-build-stage.ymlの実行結果をテスト | |
│ ├── 11-selinux-off.rb -------------- server/11-seilnux-off.ymlの〃 | |
│ ├── 20-deploy-user.rb -------------- server/20-deploy-user.ymlの〃 | |
│ ├── 30-update-sshd.rb -------------- server/30-update-sshd.ymlの〃 | |
│ └── Makefile | |
└── tmp/ | |
Rakefile
たぶんですが、同じ目的を達成するソフトウェアは探せば何処かにあると思います。が、シシマイ(p5-Sisimai)のRuby化をするにあたってRubyに慣れておきたいという意図もあり、結局は自分でRubyを学びつつ、改造する事にしました。改造した内容は、
- .default-inventoryfileの中身またはENV['INVENTORY']からインベントリファイルを決定
- roles/ロール名/spec/*.rb以下のファイルを探してターゲットを作る
- lib/spec_helper.rbに渡す環境変数を作る
- ターゲット名はspec:インベントリに書いたホスト名:ロール名って構造にする
ってところです。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require 'rake' | |
require 'rspec/core/rake_task' | |
require './lib/ansible_helper' | |
task :spec => 'spec:all' | |
task :default => :spec | |
RolesDir = './server/roles' | |
RSpecDir = './server/spec' | |
namespace :spec do | |
# Serverspec related targets | |
inventory = File.read('.default-inventoryfile').chomp | |
hosttable = MakeServer::Ansible.load_inventory( ENV["INVENTORY"] || inventory ) | |
roleindex = MakeServer::Ansible.make_roleindex | |
rolespecs = {} | |
tasknames = [] | |
roleindex.unshift('bootstrap') | |
roleindex.each do |role| | |
# Find *.rb files in spec directory from all the roles | |
rolespath = sprintf( "%s/%s/spec/*.rb", RolesDir, role ) | |
Dir.glob(rolespath).each do |file| | |
next unless File.exist?(file) | |
rolespecs[role] ||= [] | |
rolespecs[role] << File.basename(file) | |
end | |
end # End of roleindex.each | |
hosttable.each_key do |v| | |
# Build environment variable for spec_helper.rb | |
hostname = hosttable[v]['hostname'] | |
tasknames = [] | |
ENV['SPEC_HOSTNAME'] = hostname | |
ENV['SPEC_THEGROUP'] = hosttable[v]['group'] | |
ENV['SPEC_USERNAME'] = hosttable[v]['username'] | |
ENV['SPEC_SSHDPORT'] = hosttable[v]['sshdport'] | |
ENV['SPEC_IDENTITY'] = hosttable[v]['identity'] | |
if hosttable[v]['password'] then | |
ENV['SPEC_PASSWORD'] = hosttable[v]['password'] | |
end | |
rolespecs.each_key do |role| | |
# Build each target and task | |
thistask = sprintf("%s:%s", hostname, role ) | |
tasknames << thistask | |
desc sprintf( "Run serverspec tests to %s(%s)", hostname, role ) | |
RSpec::Core::RakeTask.new(thistask) do |task| | |
# Define spec:<hostname>:<role name> task | |
task.pattern = sprintf( "%s/%s/spec/*.rb", RolesDir, role ) | |
task.verbose = true | |
end | |
end # End of rolespecs.each_key | |
desc 'Run tests for Ansible environment' | |
RSpec::Core::RakeTask.new(hostname + ':ansible-env') do |task| | |
task.pattern = RSpecDir + '/[0-9][0-9]-*.rb' | |
end | |
tasknames.unshift( hostname + ':ansible-env' ) | |
# tasknames << hostname + ':ansible-env' | |
desc sprintf( "Run all the serverspec tests to %s", hostname ) | |
task hostname + ':all' => tasknames | |
end # End of hosttable.each_key | |
task :all => tasknames | |
task :default => ':all' | |
end # End of namespace :spec | |
lib/ansible_heler.rb
Rakefileから読込まれるファイルとしてansible_helper.rbってのを作りました。目的は
- Ansibleのインベントリファイルを読込みspec_helper.rbに渡すテスト対象ホスト名を取得
- spec/*.rbでroles/ロール名/vars/main.ymlを読込んで変数を参照する
- spec/*.rbでgroup_vars/の該当するファイルを読込んで参照する
あたりです。とはいえ、mx[0:4].nyaan.jpみたいな形式のホスト定義は未だ読めないので、さっさと直したいところではあります。この問題解決を後回しにしたのは、普段Ansibleを使う目的がWeb,Mail,DBをそれぞれ1台作るってケースが多くて、Webのクラスタを5台作るようなケースが殆ど無いので、1台ごとにホスト名を冠したディレクトリを作ってPlaybookもインベントリファイルもそのホスト専用に書く事が多いからです。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require "yaml" | |
module MakeServer | |
module Ansible | |
# Helper class for Ansible files | |
class << self | |
@@RootDir = 'server' | |
@@RoleDir = @@RootDir + '/roles' | |
@@SpecDir = @@RootDir + '/spec' | |
def load_inventory(inventory) | |
# Load inventory file from ./server directory | |
inventory = @@RootDir + '/' + inventory | |
groupvars = {} | |
hosttable = {} | |
File.open(inventory) do |f| | |
# Open inventory file | |
currhostname = nil | |
currgroupname = nil | |
varstable = { | |
'ansible_ssh_host' => 'hostname', | |
'ansible_ssh_port' => 'sshdport', | |
'ansible_ssh_user' => 'username', | |
'ansible_ssh_pass' => 'password', | |
'ansible_ssh_private_key_file' => 'identity', | |
} | |
f.each_line do |line| | |
# Read each line of inventory | |
next if line.match %r/\A#/ | |
next if line.match %r/\A\s*\z/ | |
line = line.gsub %r/\s\z/, '' | |
if line.match %r/\A\[(.+):vars\]\s*\z/ then | |
# Start group variables section, eg) [product:vars] | |
currgroupname = $1 | |
elsif line.match %r/\A\[(.+)\]\s*\z/ then | |
# Start group section, eg) [product] | |
currgroupname = $1 | |
else | |
# Each host entry or variable | |
if line.match %r/\A[^\s]+\s+[^\s]+\z/ then | |
# This line includes 1 or more space character like | |
# vm ansible_ssh_host=192.0.2.2 | |
line.split(' ').each do |e| | |
# Check each token splited by ' ' | |
if e.match %r/(.+)=(.+)/ then | |
# Variable: ex) ansible_ssh_port=2022 | |
hosttable[currhostname][varstable[$1]] = $2 | |
else | |
# Deal as hostname or IP address | |
hosttable[e] ||= {} | |
hosttable[e]['hostname'] = e | |
hosttable[e]['group'] = currgroupname | |
currhostname = e | |
end | |
end | |
elsif line.match %r/([^\s]+)=([^\s]+)/ then | |
# The line may be in [group:vars] section | |
next unless varstable.key?($1) | |
groupvars[currgroupname] ||= {} | |
groupvars[currgroupname][varstable[$1]] = $2 | |
else | |
# Deal as hostname or IP address, eg) 192.0.2.1 | |
hosttable[line] ||= {} | |
hosttable[line]['hostname'] = line | |
hosttable[line]['group'] = currgroupname | |
currhostname = line | |
end # End of line.match(2) | |
currhostname = '' | |
end # End of line.match(1) | |
end # End of f.each_line | |
hosttable.each_key do |e| | |
# Fill values in hosttable with groupvars | |
next unless hosttable[e]['group'] | |
v = hosttable[e]['group'] | |
groupvars[v].each_key do |x| | |
# Set values if the value in hosttable is empty | |
hosttable[e][x] ||= groupvars[v][x] | |
end | |
# Default values | |
hosttable[e]['sshdport'] ||= 22 | |
hosttable[e]['username'] ||= 'root' | |
hosttable[e]['identity'] ||= '~/.ssh/id_rsa' | |
end # End of hosttable.each_key | |
end # End of File.open(inventory) | |
return hosttable | |
end | |
def make_roleindex | |
# Find roles from ./server/role directory | |
medialist = [ 'src', 'rpm', 'deb', 'pkg', 'env' ] | |
roleindex = [] | |
medialist.each do |e| | |
# Find role directories | |
rolespath = sprintf( "%s/%s/*", @@RoleDir, e ) | |
Dir.glob(rolespath).each do |role| | |
next unless File.directory?(role) | |
role = role.gsub( @@RoleDir + '/', '' ) | |
roleindex << role | |
end | |
end # End of medialist.each | |
return roleindex | |
end | |
def load_variables | |
# Function to load server/roles/*/vars/main.yml | |
variables = { 'role' => nil, 'host' => nil, 'group' => nil, 'all' => nil } | |
# Role variables | |
v = caller[0].split(':')[0] | |
v = v.gsub( %r|\A.+/(#{@@RoleDir}/.+)/spec/.+[.]rb|, '\1/vars/main.yml' ) | |
if File.exists?(v) then | |
# Load server/roles/*/vars/main.yml | |
variables['role'] = YAML.load_file(v) | |
end | |
# Host variables | |
v = @@RootDir + '/host_vars/' + ENV['SPEC_HOSTNAME'] | |
if File.exists?(v) then | |
# Load server/host_vars/<hostname> | |
variables['host'] = YAML.load_file(v) | |
elsif File.exists?( v + '.yml' ) then | |
# Try to load the file with ".yml" | |
variables['host'] = YAML.load_file( v + '.yml' ) | |
end | |
# Group variables | |
v = @@RootDir + '/group_vars/' + ENV['SPEC_THEGROUP'] | |
if File.exists?(v) then | |
# Load server/group_vars/<hostname> | |
variables['group'] = YAML.load_file(v) | |
elsif File.exists?( v + '.yml' ) then | |
# Try to load the file with ".yml" | |
variables['group'] = YAML.load_file( v + '.yml' ) | |
end | |
# All group variables | |
v = @@RootDir + '/group_vars/all' | |
if File.exists?(v) then | |
# Load server/group_vars/<hostname> | |
variables['all'] = YAML.load_file(v) | |
elsif File.exists?( v + '.yml' ) then | |
# Try to load the file with ".yml" | |
variables['all'] = YAML.load_file( v + '.yml' ) | |
end | |
return variables | |
end | |
end # End of class << self | |
end # End of module Ansible | |
end # End of module MakeServer | |
rake -T
改造したRakefileと作ったlib/ansible_helper.rbによって、`rake -T`を実行すると次のようなターゲットが構成されるようになりました。
% rake -T ⏎ rake spec:mx:all # Run all the serverspec tests to mx rake spec:mx:ansible-env # Run tests for Ansible environment rake spec:mx:bootstrap # Run serverspec tests to mx(bootstrap) rake spec:mx:src/dovecot # Run serverspec tests to mx(src/dovecot) rake spec:mx:src/opensmtpd # Run serverspec tests to mx(src/opensmtpd) rake spec:mx:src/perl # Run serverspec tests to mx(src/perl)
`rake`を実行すると上記のターゲットが全て実行されますし、OpenSMTPDのテストだけするなら`rake spec:mx:src/opensmtpd`を実行するという使い方になります。
Makefile
おっさんなのでMakefileとの付き合いも長く、Makefileのほうが好きです。自分が使う目的で、なるべくAnsibleとServerspecを使う為の環境が楽に作れるように、決まりきった作業をなるべく短いコマンドで実行出来るようにいくつかターゲットを定義しています。
- make login → Vagrant仮想マシンにSSHログインする
- make up, make down → Vagrant仮想マシンを起動・停止する
- make server → AnsibleとServerspec実行に必要なファイルをcloneしたmake-serverからコピーする
- make ロール名-role → ロールのディレクトリ構造(tasks,vars,templates,filesとか)を作る
- make *-box → Vagrant仮想マシンを作ってVagrantfileや専用インベントリファイルを作る
- make test → rake spec:*を実行する
- make build → ansible-playbook -i server/インベントリ server/Playbookファイルの長いコマンドを実行する
何より`make`ってコマンドは打ちやすいので気に入ってます。
今後のmake-server
make-serverに置いてる全てのロールにServerspecのテストコードを入れているわけではないので、書き上がり次第追加する予定です。
Playbookも今のところ、ソースビルドするものが中心で、毎回コンパイルオプションをメモ書きから調べるのが面倒なので、Playbookにしておくって程度の充実度です。
Serverspecのテストやロールが充実してきたら、Infratasterのテストもいれようかと思っていまが、とりあえずは対象サーバを構築するのに最低限必要なファイルとPlaybookとテストコードが一元管理できる状態には出来たので一旦満足したとこです。
0 件のコメント:
コメントを投稿