2015/09/29

AnsibleとServerspecでインベントリファイルを共通化した

去年の明け頃からAnsibleを使い出して、去年の春ごろから仕事でも充分使えると判断して、今はサーバを構築するのは全てAnsibleでPlaybookを書いて構築→保守という流れが確立しています。とはいえ、他社と共同管理であるサーバも多く、構築完了から時間が経過した後に最初に書いたPlaybookを実行して、Playbookに反映されていない変更が巻き戻るという管理上の問題がありました。`ansible-playbook --check`での検査で何故かChangedになるタスクがあったり(これは僕の書き方が悪いのかも)して、テスト目的ではServerspecを使う事を検討し、実際に使ってみる事にしました。

結論としては、Serverspecは充分使えると判断してAnsible Playbookで実行した内容のテストをServerspecで記述して、構築完了後〜何か変更を加えるときに毎回`rake`を実行するという形が合理的であるとの解を得ました。

となると次は、ディレクトリ名からホスト名を得るServerspecにAnsibleのインベントリファイルを読込んで、対象とするホスト定義を共通化したいと思いました。最終的な目的は、Ansibleのインベントリファイルのみでホスト定義をしてServerspecもそれを読み、AnsibleのBest Practicesに従ったディレクトリ構造の中にServerspecのテストコード用ファイルを入れる事で、それを達成する為にRakefileを改造しました。

make-server

自分で使うAnsibleのロールやPlaybookの動作テスト用にいつでもVagrant環境を準備出来るフレームワーク的な何かをmake-serverという名前でGitHubに置いてまして、今回はそれを大改造しました。

改造項目

  1. ansible/rolesってディレクトリ名を変える→server/roles
  2. roles/以下を構築手段別に再分類する→roles/{rpm,pkg,deb,src,env}とか
  3. rake実行時にインベントリファイルを読込む→Rakefile改造とlib/ansible_helper.rb作成
  4. roles/ロール名/spec/*.rbに入れたテストコードをrakeで実行→lib/ansible_helper.rb作成

改造後

全体のディレクトリ構造

Gistに書いたのですが、以下のような構造になりました。

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:インベントリに書いたホスト名:ロール名って構造にする
ってところです。
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
view raw Rakefile hosted with ❤ by GitHub

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もインベントリファイルもそのホスト専用に書く事が多いからです。
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 件のコメント:

コメントを投稿