Ansibleを使って雑にMacの環境構築を自動化する

# TL;DR
* [Ansible チュートリアル | Ansible Tutorial in Japanese](http://yteraoka.github.io/ansible-tutorial/)
* [Mac の開発環境構築を自動化する (2015 年初旬編) - t-wadaのブログ](http://t-wada.hatenablog.jp/entry/mac-provisioning-by-ansible)
* [Macの環境構築をAnsibleでやることにした - Please Drive Faster](http://joe-re.hatenablog.com/entry/2015/01/02/212007)

これ読めばとりあえず出来る。

この記事では、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を入れる
[Download Vagrant - Vagrant](https://www.vagrantup.com/downloads.html)

## VagrantからYosemite起動できるようにする
[OSX - OS X YosemiteのVagrant Boxを作る - Qiita](http://qiita.com/uzimith/items/39d3018803ec6418efa9)

これそのままやる。

### *Vagrantfile*書く
```rb: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](http://www.ansible.com/home)

任意の方法で入れる。`brew install ansible`が楽。

## Serverspec
### Serverspec入れる
[Serverspec - Home](http://serverspec.org/)

`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を変更する
コピペで動くはず

```rb: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*は消す。雑にテストのファイル１つでやるのでもっと良い名前あったら教えて欲しい。

https://gist.github.com/gin0606/25f1b59e8a6602a758b9
全部書いてたらめっちゃ長くなりそうなんでmain.yamlだけgistに上げた。brewとbrew-caskとgemだけ使うなら*vars.yaml*編集するだけで終わる。

*playbook-vagrant.yaml*に書くことはこれだけ。

```yaml:playbook-vagrant.yaml
- hosts: all
  roles:
    - role: base
  vars_files:
    - vars.yaml
```

## Command Line Tools入れる
Homebrew入れるのにCommand Line Tools必須なので。

```yaml:roles/base/tasks/main.yaml
- 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](https://github.com/r7kamura/dotfiles/blob/master/install.sh#L15-L22)から頂きました。

## Homebrew入れる
```yaml:roles/base/tasks/main.yaml
- 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
```

### テスト書く
```rb:spec/base/all_spec.rb
describe file('/usr/local/bin/brew') do
  it { should be_executable }
end
```

## Homebrewでいろいろ入れる
### *brew update*
```yaml:roles/base/tasks/main.yaml
- homebrew: update_homebrew=yes
```

### brew-cask使えるようにする
```yaml:roles/base/tasks/main.yaml
- homebrew_tap: tap=caskroom/cask state=present
- homebrew: name=brew-cask
```

### その他tapしたいの指定する
```yaml:vars.yaml
homebrew:
  tap:
    - homebrew/versions
```

```yaml:roles/base/tasks/main.yaml
- homebrew_tap: tap={{ item }} state=present
  with_items: homebrew.tap
```

`with_items`にyamlのarrayを指定すると、`{{ item }}`に各要素が展開されてループみたいな感じになる。

更に雑にやりたい場合は、vars.yaml書かないでこんなかんじでもいい。

```yaml:roles/base/tasks/main.yaml
- homebrew_tap: tap={{ item }} state=present
  with_items:
    - homebrew/versions
```

## caskでいろいろ入れる
```yaml:vars.yaml
homebrew:
  cask:
    - virtualbox
    - vagrant
```

```yaml:roles/base/tasks/main.yaml
- homebrew_cask: name={{ item }}
  with_items: homebrew.cask
```

## brewでいろいろ入れる
```yaml:vars.yaml
homebrew:
  formula:
    - git
```

```yaml:roles/base/tasks/main.yaml
- homebrew: name={{ item }}
  with_items: homebrew.formula
```

### テスト書く
```rb:spec/base/all_spec.rb
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
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`すると自動的に適用される。ログは長いのでいろいろ削ってます。

```shell-session
$ 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*を作る

```yaml:playbook-localhost.yaml
- hosts: localhost
  connection: local
  roles:
    - role: base
  vars_files:
    - vars.yaml
```

```shell-session
echo 'localhost' >> hosts
ansible-playbook -i hosts playbook-localhost.yaml
```

というような感じで出来る。

## localhostの環境をserverspecでテストする
`serverspec-init`で作った状態だとlocalhostのテストが出来ないので、sshとlocalで別の初期化走るようにする。
正しい方法は不明なので知ってる人いたら教えて欲しい。

```rb:spec/spec_helper.rb
# 元の内容を spec_helper_ssh.rb に移動する
if ENV['TARGET_HOST'] == 'localhost'
  require 'spec_helper_exec'
else
  require 'spec_helper_ssh'
end
```

```rb:spec/spec_helper_exec.rb
require 'serverspec'

set :backend, :exec
```

```rb:spec/spec_helper_ssh.rb
# 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'
```