Macの環境構築をAnsibleでやることにした

明けましておめでとうございます!

設定ファイルの大掃除も兼ねて、自宅Macの環境セットアップをAnsibleで行うようにしてみました。

joe-re/dotfiles · GitHub

Ansibleにした経緯

2台のMacの環境を揃えたい

昨年iMac5kディスプレイモデルを購入した。

それによって今までメインで使用していたMacBookAirは外出用にして、2台で運用している。

そうなるとどうやって環境を揃えようかなー、って悩みが発生する。

なるべく外出時も環境は変えずに開発できるようにしたい。

Ansibleに至るまで

当初はBoxen使ってた。

BoxenはPuppetでMacの環境構築を自動化してくれるツール

PuppetのDSLを覚えなければいけないというハードルはあるものの、 かなり細かいところまで設定できて非常に高機能。

だけどチームならまだしも、個人で使うにはオーバースペック過ぎる感がある。

この記事、すごく共感。

Mac - Boxen使ってて許されるのは2013年だけだった - Qiita

Boxenとにかく重くてストレス溜まるし、依存関係とかで動かなくなると復旧するためにデバッグするのがすごく大変だった。

僕はnodeのバージョンあげてちょっと動かしたいだけなんだよ!!とか思ってると、とりあえず手動でいいやーってなってしまう。

そうなるとこの手のスクリプトはどんどん腐っていく。

腐ったスクリプトを叩いても環境は再現できないし、下手すると叩くと壊れるかもしれない。

そして僕はBoxenを使わなくなった。

そんなこんなで、iMac買ったタイミングでBoxenは投げ捨てて、brew bundle + shellスクリプトで構築することにした。

そしたらすごく快適(早いし失敗した時のエラーが分かりやすい!)になって、いつでも気軽にスクリプト叩けるようになった。

だけどshellスクリプトだと完全に処理だし、YAMLとかで設定っぽく書けたらいいなーとか思いながら書いてた。

そういえばAnsibleってYAMLじゃん!

ということで移行することにした。

書いてみた

Ansible

ちなみに以前のshellスクリプト

#!/bin/bash
set -e

# oh-my-zsh install
oh_my_zsh=~/.oh-my-zsh/oh-my-zsh.sh
if test -e "${oh_my_zsh}"; then
  echo "${oh_my_zsh} is exists"
else
  curl -L http://install.ohmyz.sh | sh
  echo "installed oh-my-zsh"
fi

# brew bundle 叩く
brew bundle || true

# 設定ファイルのシンボリックを作成
echo "start to make sym link"
files=("gitconfig" "vimrc" "zshrc" "tmux.conf" "zprofile" "vrapperrc" "rubocop.yml" "z")

# to home dir
for file in ${files[*]}
do
  filepath="${PWD}/${file}"
  homefile="${HOME}/.${file}"
  ln -sf "${filepath}" "${homefile}"
  echo "made symlink: ${homefile}"
done

# karabiner
karabiner_file="${PWD}/private.xml"
to_karabiner_file=~/Library/Application\ Support/Karabiner/private.xml
ln -sf "${karabiner_file}" "${to_karabiner_file}"
echo "made symlink: ${to_karabiner_file}"

# mkdir vim backup dir
vim_backup_dir="${HOME}/.vim-backup"
if test -e "${vim_backup_dir}"; then
  echo "${vim_backup_dir} is exists"
else
  mkdir "${vim_backup_dir}"
  echo "made dir: ${vim_backup_dir}"
fi

# neobundle install
neobundle_dir=~/.vim/bundle
if test -e "${neobundle_dir}"; then
  echo "${neobundle_dir} is exists"
else
  mkdir -p ${neobundle_dir}
  git clone git://github.com/Shougo/neobundle.vim ~/.vim/bundle/neobundle.vim
  echo "installed neobundle"
fi

# go modules install
go get github.com/motemen/ghq
go get github.com/lestrrat/peco/cmd/peco/
echo "installed go modules"

echo "completed!"

ここに書いてないけどbrewfileが同じ階層に置いてあって、このshellスクリプトからbrew bundleを叩くことで参照している。

行数はそんなに変わらないけど、やっぱりYAMLで書けたほうがぱっと見た時に分かりやすい。

解説

ansible-playbook playbook.yml -i hostsで実行する。

-iでInventoryの指定をしている。

localhostだからssh接続しないし、指定しなくてもよしなにやってくれたらいいんだけど、ansible-playbookはInventoryの指定が必須っぽい。

なのでInventoryには127.0.0.1しか書いていない。

playbook.ymlの3行目にconnection: localと書いている。これでこのplaybookはssh接続はせず、localに適用する。

Homebrewのパッケージのインストールには、@hnakamur2さんが公開しているroleを使わせてもらった。

(roleについてはAnsibleのroleを使いこなす - Qiitaの記事が勉強になりました。)

AnsibleにはAnsible Galaxyという公式が運用しているエコシステムがあって、Ansibleを入れると同時に使える。

具体的にはansible-galaxy install hnakamur.homebrew-packagesとすればhnakamur.homebrew-packagesというroleが使えるようになる。

上記2つのroleをインストールするコマンドは事前に叩いておく必要がある。

他に設定した内容で、ノウハウになりそうなことをメモ的に書いておく。

ファイルの存在チェック

oh-my-zshのインストールの時にやっている。(playbook.ymlの37行目あたり。)

この解説が参考になった。

Ansible - Only if a file exists or does not exist

oh-my-zshのインストール処理は、ファイルの状態チェックタスクと実際にインストールするタスクとで2つに分かれている。

ファイルの状態チェック(oh-my-zsh: get status)ではstatで指定したパスの状態を取得して、結果をregisterで変数に格納する。

ファイルが存在しているときは変数名.stat.existsにtrueがセットされ、存在していないときはfalseがセットされる。

インストールタスク('oh-my-zsh: install')では、whenで変数名.stat.existsがfalse(存在していない)時にタスクが実行されるように指定する。

ちょっと冗長な感じだけど、Ansibleでファイルの存在チェックで処理を分岐させたければこういう感じに書くっぽい。

こういう単純なやつならwhenで全部書ければいいんだけどなー。

特定のディレクトリに格納された設定ファイルのsymlinkを作成

'make symlinks to under home dir'というタスク(45行目)とかでやってる。

playbook.ymlと同じ階層に'home_symlinks'というディレクトリがある。

このタスクではそこに格納されている全てのファイルのsymlinkを実行ユーザのホームディレクトリに、先頭に.を付与した上で配置する。

そこで使えるのがwith_fileglobで、↓のように書いておくとhome_symlinks配下のファイル全てに対してループできる。

with_fileglob:
  - home_symlinks/*

このループで対象になっている要素はitem変数でアクセスできる。これはAnsibleの決まりごとで、全てのループが同じ仕様。

Loops — Ansible Documentation

symlinkはfilestate=linkを指定すれば作れる。

file - Sets attributes of files — Ansible Documentation

notifyで処理を連携

'mkdir vim-neobundle dir'(59行目)でやっている。

vimのneobundleはgit cloneでインストールするので、毎回実行はしない。

なのでインストール先のディレクトリを作成したタイミング(初回)でのみ実行するようにした。

特定のタスクの実行をトリガーに別の処理を行う時に使うのが、notifyとhandlerだ。

notifyではhandlersセクションに定義されている処理を呼ぶことができる。

ここではnotify: install neobundleと指定しているので、handlersセクションに定義されているinstall neobundle(77行目)が呼ばれることになる。

ちなみに実行される順番はpre_tasks → roles → tasks → handlers → post_tasksだ。

なのでinstall neobundleはすべてのtasksの実行後に実行されることになる。

複数回notifyされていたとしても、実行されるのは1回だけ。

失敗条件と変更条件をカスタマイズ

goのモジュールをインストールする時のチェック('check go modules status')に使っている。(62行目)

移行前のshellスクリプトでは毎回go get module名するように書いていて、go getコマンドは2度目以降は実行されないだけなので問題なかった。

けどAnsibleでは毎回実行されるように書いてしまうと、毎回実行結果のchangedに計上されてしまうのでうざい。

なので存在チェックするようにして、ないものだけをgo getするようにしたい。

go list package名コマンドは、gopath配下にインストールされたmoduleがあればそのpathが返却される。

なければエラー(exitコード1)になってインストールされていない旨のメッセージが表示される。

このコマンドをチェックに使いたいのだけど、そのまま使うと毎回実行することになるのでchangedに計上されてしまうし、 デフォルトではexitコードが1であればエラーとするので、未インストールのモジュールは必ずfailedになってしまう。

そこでfailed_whenchanged_whenを指定し、failed、changedの条件を変更した。

Error Handling In Playbooks — Ansible Documentation

ここではexitコード1をfailed、changed条件から除外したので、基本的にこのタスクで失敗が出る事はない。

このタスクの結果はregisterを使って変数に格納しておいて、実際にインストールするタスク('install go modules'(71行目))で参照している。

チェック処理にはloopを使っている。この設定をする前はloop処理中にregister指定ってできるのかな? と疑問だった。

実際は変数名.resultsに配列で結果が格納される。使い勝手が良い。

Loops — Ansible Documentation

感想

やっぱりYAMLで書けるのがいい!

シンプルで、基本的には上から順番に実行されるという思想も分かりやすい。

しかしシンプル故に、すごく複雑なことをやろうとすると途端に難しくなってしまいそう。

ただこういう開発環境構築みたいに、ある程度お手軽にやりたいケースには向いている。

書籍

入門Ansible

入門Ansible

読みやすい。一度目さえ通しておけばリファレンス的に使えて良い。