TL;DR
- Ansible チュートリアル | Ansible Tutorial in Japanese
- Mac の開発環境構築を自動化する (2015 年初旬編) - t-wadaのブログ
- Macの環境構築をAnsibleでやることにした - Please Drive Faster
これ読めばとりあえず出来る。
この記事では、Homebrewのインストールなども含めて出来る限りのことをAnsibleに任せます。
Vagrantで建てたvmに対してAnsibleのplaybook適用して、Serverspecでテストします。
僕が全部初学者なので雑というのは説明が雑ということです。
AnsibleでMacのプロビジョニングする際の問題点
Ansible、恐らく本来はImmutable InfrastructureとかInfrastructure as Codeと呼ばれるものを実現するためのツールなので、日常的に使用して、状態変化するマシンの環境管理には適していない。
なので、会社と自宅のMacの状態同期に使用するのには適していない。
実際にあった問題なんだけど、事情があってhomebrew/versions/android-ndk-r9dを使っていたけど、事情が解消されたので普通にbrew install android-ndkで入れるようにしたらunlinkとlinkは手作業でしないといけないみたいな事があった。
AnsibleでMacの環境構築・管理をするのは、新規購入したマシンの初期の環境構築のみに使用するか、頑張って頑張るみたいな感じになる。
あとは、テストをTravisCIでやってる人多いっぽいんだけど、遅くて結構辛かったのでTravis使わない事にしてる。
それを踏まえて雑に自動化する。
準備
Vagrantを入れる
VagrantからYosemite起動できるようにする
OSX - OS X YosemiteのVagrant Boxを作る - Qiita
これそのままやる。
Vagrantfile書く
Vagrant.configure(2) do |config|
  config.ssh.insert_key = false
  config.vm.define :yosemite do |node|
    node.vm.box = 'osx-yosemite'
    node.vm.network :forwarded_port, guest: 22, host: 2001, id: "ssh"
    node.vm.network :private_network, ip: "192.168.33.11"
    node.vm.synced_folder ".", "/vagrant", type: "nfs", nfs: true
  end
end
- 
config.ssh.insert_key = falseは、trueだとvmごとにsshキーが生成されて怠いので、怠くないようにする設定
- 
config.vm.define :yosemiteは、yosemiteという名前を付けてvm建てられるようにしてる
- 
node.vm.box = 'osx-yosemite'は、さっき追加したboxを*:yosemite*で使うということ
- 
node.vm.networkは察してください
- 
node.vm.synced_folderは、vmを動かしてるマシンのフォルダにvmからアクセス出来るようにするやつ。後述するのやらないなら不要
ここまでやったらvagrant up yosemiteでvm起動できるはず(画面とかは見られない)。
Ansible入れる
Ansible is Simple IT Automation
任意の方法で入れる。brew install ansibleが楽。
Serverspec
Serverspec入れる
gem install serverspecか、bundler使うなど任意の方法で入れてください。
serverspec-init
serverspec入れたらserverspec-initというコマンドが使えるようになっているはずなので呼ぶ。
選択肢は、UN*XとSSHを選ぶ。
テストをホスト間で共有できるようにする
serverspec-initが生成してくれるテストは、ホストごとに分かれているので、同じテストを使いまわすのが少し怠い。
なので、テストをホスト間で共有できるようにする。
ファイルを移動する
spec/[host]/sample_spec.rbをspec/base/sample_spec.rbに移動する。baseは後で使う名前なので変えない。
Rakefileを変更する
コピペで動くはず
require 'rake'
require 'rspec/core/rake_task'
task :spec    => 'spec:all'
task :default => :spec
properties = {
  'yosemite' => {
    roles: ['base']
  },
  'localhost' => {
    roles: ['base']
  },
}
namespace :spec do
  task :all => properties.keys.map {|key| 'spec:' + key.split('.')[0] }
  properties.keys.each do |key|
    desc "Run serverspec to #{key}"
    RSpec::Core::RakeTask.new(key.split('.')[0].to_sym) do |t|
      ENV['TARGET_HOST'] = key
      t.pattern = 'spec/{' + properties[key][:roles].join(',') + '}/*_spec.rb'
    end
  end
end
Shellでrake -Tするとrake spec:yosemiteとかが出てくるようになってるはず。
rake spec:yosemite呼んでみるとテスト落ちるはず。require 'spec_helper'以外全て消そう。
Ansibleいろいろ書く
ファイル作る
- playbook-vagrant.yaml
- vars.yaml
- roles/base/tasks/main.yaml
- 
spec/base/all_spec.rb
- sample_specは消す。雑にテストのファイル1つでやるのでもっと良い名前あったら教えて欲しい。
 
https://gist.github.com/gin0606/25f1b59e8a6602a758b9
全部書いてたらめっちゃ長くなりそうなんでmain.yamlだけgistに上げた。brewとbrew-caskとgemだけ使うならvars.yaml編集するだけで終わる。
playbook-vagrant.yamlに書くことはこれだけ。
- hosts: all
  roles:
    - role: base
  vars_files:
    - vars.yaml
Command Line Tools入れる
Homebrew入れるのにCommand Line Tools必須なので。
- name: Command Line Toolsが入っているかの確認
  stat: path=/usr/include
  register: command_line_tools_dir
- name: Command Line Toolsが入っていなかったらインストールする
  shell: |
    PLACEHOLDER=/tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress
    touch $PLACEHOLDER
    PROD=$(softwareupdate -l | grep "\*.*Command Line" | head -n 1 | awk -F"*" '{print $2}' | sed -e 's/^ *//' | tr -d '\n')
    softwareupdate -i "${PROD}"
    [[ -f $PLACEHOLDER ]] && rm $PLACEHOLDER
  when: not command_line_tools_dir.stat.exists
最初にstatで状態確認してから、次のtaskのwhenで実行するか否か決めると、2回目の実行時にchangeが少なくなって良い。
シェルからCLT入れるのはdotfiles/install.sh at master · r7kamura/dotfilesから頂きました。
Homebrew入れる
- name: homebrewが入っているかの確認
  stat: path=/usr/local/bin/brew
  register: brew_command
- name: homebrewが入っていなかったらインストールする
  shell: echo '\n' | ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
  when: not brew_command.stat.exists
テスト書く
describe file('/usr/local/bin/brew') do
  it { should be_executable }
end
Homebrewでいろいろ入れる
brew update
- homebrew: update_homebrew=yes
brew-cask使えるようにする
- homebrew_tap: tap=caskroom/cask state=present
- homebrew: name=brew-cask
その他tapしたいの指定する
homebrew:
  tap:
    - homebrew/versions
- homebrew_tap: tap={{ item }} state=present
  with_items: homebrew.tap
with_itemsにyamlのarrayを指定すると、{{ item }}に各要素が展開されてループみたいな感じになる。
更に雑にやりたい場合は、vars.yaml書かないでこんなかんじでもいい。
- homebrew_tap: tap={{ item }} state=present
  with_items:
    - homebrew/versions
caskでいろいろ入れる
homebrew:
  cask:
    - virtualbox
    - vagrant
- homebrew_cask: name={{ item }}
  with_items: homebrew.cask
brewでいろいろ入れる
homebrew:
  formula:
    - git
- homebrew: name={{ item }}
  with_items: homebrew.formula
テスト書く
describe package('git') do
  it { should be_installed.by('homebrew') }
end
brewの場合はupdateコマンドで入るversion変わるので書かないほうがいい気がするけど、be_installed.by('homebrew').with_version('2.3.5')とか書くとversion違うの入ってた場合テストが落ちる
VMに対して適用してみる
VagrantのProvisionerにAnsibleを指定する
最初に作ったVagrantfileを編集する。
diff --git a/Vagrantfile b/Vagrantfile
index 90ff975..d31b4c7 100644
--- a/Vagrantfile
+++ b/Vagrantfile
@@ -13,5 +13,9 @@ Vagrant.configure(2) do |config|
     node.vm.network :forwarded_port, guest: 22, host: 2001, id: "ssh"
     node.vm.network :private_network, ip: "192.168.33.11"
     node.vm.synced_folder ".", "/vagrant", type: "nfs", nfs: true
+
+    node.vm.provision "ansible" do |ansible|
+      ansible.playbook = "playbook-vagrant.yaml"
+    end
   end
 end
これ書くとvagrant upしたらさっき書いたyamlの内容を適用できる。変更あったらvagrant provisionでプロビジョニング出来る。
適用してみる
vagrant upすると自動的に適用される。ログは長いのでいろいろ削ってます。
$ vagrant destroy yosemite
$ vagrant up yosemite
Bringing machine 'yosemite' up with 'virtualbox' provider...
==> yosemite: Importing base box 'osx-yosemite'...
# 略
PLAY [all] ******************************************************************** 
GATHERING FACTS *************************************************************** 
ok: [yosemite]
TASK: [base | Command Line Toolsが入っているかの確認] *************** 
ok: [yosemite]
TASK: [base | Command Line Toolsが入っていなかったらインストールする] *** 
skipping: [yosemite]
TASK: [base | homebrewが入っているかの確認] ************************* 
ok: [yosemite]
TASK: [base | homebrewが入っていなかったらインストールする] *** 
changed: [yosemite]
TASK: [base | homebrew update_homebrew=yes] *********************************** 
ok: [yosemite]
TASK: [base | homebrew_tap tap=caskroom/cask state=present] ******************* 
changed: [yosemite]
TASK: [base | homebrew name=brew-cask] **************************************** 
changed: [yosemite]
TASK: [base | homebrew_tap tap={{ item }} state=present] ********************** 
changed: [yosemite] => (item=homebrew/versions)
TASK: [base | homebrew_cask name={{ item }}] ********************************** 
changed: [yosemite] => (item=virtualbox)
changed: [yosemite] => (item=vagrant)
TASK: [base | homebrew name={{ item }}] *************************************** 
changed: [yosemite] => (item=git)
PLAY RECAP ******************************************************************** 
yosemite                   : ok=20   changed=15   unreachable=0    failed=0   
もう一度vagrant provisionでansibleで書いた内容適用してみると、changedが減ってるはず。
テストしてみる
rake spec:yosemiteでプロビジョニング後のvmの状態が所望の状態になっているかをテストできる。
テスト落ちたらyaml見なおすか、テスト見直す。
そんな感じであとは頑張れ。
その他
localhostに適用する
playbook-localhost.yamlを作る
- hosts: localhost
  connection: local
  roles:
    - role: base
  vars_files:
    - vars.yaml
echo 'localhost' >> hosts
ansible-playbook -i hosts playbook-localhost.yaml
というような感じで出来る。
localhostの環境をserverspecでテストする
serverspec-initで作った状態だとlocalhostのテストが出来ないので、sshとlocalで別の初期化走るようにする。
正しい方法は不明なので知ってる人いたら教えて欲しい。
# 元の内容を spec_helper_ssh.rb に移動する
if ENV['TARGET_HOST'] == 'localhost'
  require 'spec_helper_exec'
else
  require 'spec_helper_ssh'
end
require 'serverspec'
set :backend, :exec
# spec_helperに書いてあった内容
require 'serverspec'
require 'net/ssh'
require 'tempfile'
set :backend, :ssh
if ENV['ASK_SUDO_PASSWORD']
  begin
    require 'highline/import'
  rescue LoadError
    fail "highline is not available. Try installing it."
  end
  set :sudo_password, ask("Enter sudo password: ") { |q| q.echo = false }
else
  set :sudo_password, ENV['SUDO_PASSWORD']
end
host = ENV['TARGET_HOST']
`vagrant up #{host}`
config = Tempfile.new('', Dir.tmpdir)
config.write(`vagrant ssh-config #{host}`)
config.close
options = Net::SSH::Config.for(host, [config.path])
options[:user] ||= Etc.getlogin
set :host,        options[:host_name] || host
set :ssh_options, options
# Disable sudo
set :disable_sudo, true
# Set environment variables
# set :env, :LANG => 'C', :LC_MESSAGES => 'C'
# Set PATH
set :path, '$HOME/.rbenv/shims:$HOME/.rbenv/shims:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/X11/bin'