会社で新しいMacBookProが支給されたので、開発環境のセットアップを行いました。その際に、何を入れたらいいかなどをチェックするのが大変だし、毎度毎度手間がかかっても面倒です。やることはだいたい決まっているのに、セットアップに1日か2日費やすことになるのは勿体ないなと思いました。また、OSを更新するときにはクリーンインストールしたいと思いまして(High Sierraも出るし)、Macの環境構築の自動化に乗り出すことを決意しました。
これができたら、他のメンバーの環境構築も自動化できるし、githubに構築スクリプトをアップしといたらPull Reqがくるかもしれないしなぁという算段です。
開発環境の自動化は、Ansibleを使うことにしました。Ansibleはサーバの構築の自動化ツールとしてよく使われていますが、Macなどの環境構築でもよく使われていて実績がインターネット上にたくさんあります。なお、実験台となったPCはスペック的に厳しくなってきたMBA11インチ 64GBのマシンです。
要件を決める
自動化とやみくもにいっても、自動化するのに時間がかかってしまうので、まず要件を決めます。
最低要件
まずはこれだけは最低でも行いたいという要件を決めてから作業しました。
- homebrew経由で必要なライブラリなどのインストールが完了していること
- homebrew caskでの必要なソフトウェアのインストールが完了していること
- Ruby, Node.jsが使えるようになっていること
すぐに開発に取り掛かれるという意味では、ここまでの自動化でも十分かなと思います。
個人的要件
これらは、私がどのMacでもやっていることなので、ついでに自動化できたらしたいなぁ、という程度。だけれど、いつも面倒だったので是が非でも自動化したい気持ち。
- shellがfishになっていること
- oh-my-fishをインストールすること
- 便利なエイリアスコマンドの設定が終わっていること
- gitの設定が終わっていること
- pecoとそれを使った関数の設定が終わっていること
- ghqが使えること
fishは便利なshellで、oh-my-fishはコンソール画面を便利にカスタマイズしたり、fishのプラグインを簡単に入れられるようにするものです。 便利なエイリアスコマンドは、例えばbundle installをbiだけで済むようにするショートカット的なやつの集合です。
pecoは、標準出力結果を選択して次の関数に渡せるやつで、pecoを絡めて独自に関数を作ると超便利です。例えば、pidの一覧から選択してプロセスを終了させるpeco_killコマンドがよく使われるものとして有名です。
ghqは、githubリポジトリのcloneを決められたルールで行ってくれるものです。いろんなプロジェクトのソースコードがどこにあるかわからなくなったりすることがなくなり、とても便利です。
あとで追加した要件
Ansibleを使っている途中で、OSXのデフォルトの定義の変更も手軽に行えるというのを見つけたので、それらも自動化するようにしました。
その他、まだいろいろやりたいことはあるのですが、やり方がわからないものとかがあったので、まだやってません。
自動化の準備
XCodeのインストール
Homebrewを入れるために、XCodeのインストール。下記のコマンドを入力するだけでXCodeのCommand Line Toolのインストールが終わりました。
sudo xcodebuild -license
Homebrewのインストール
homebrewのインストールを行います。
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
Ansibleのインストール
homebrew経由でpythonとansibleを入れておきます。
brew install python brew install ansible
Playbookを作っていく
Ansible、というか構成管理ツールは冪等性(べきとうせい)が重要になってきます。冪等性とは、何度同じ命令を実行しても結果が同じになることです。冪等性を確保するためにPlaybookを作るのに試行錯誤したので、最後にgithubのリポジトリへのリンクを貼っておくので、細かいことは書きません。ざっくりと、やったことと、Ansibleで便利だったモジュールについて書いていきます。
inventoryの作成
inventoryとは、作業対象のホストの情報になります。サーバの管理の場合、web, dbなどのグループ分けに対応していて、グループ毎に設定処理を分けたりすることができます。今回は、作業対象はMac自身なので、hostsファイルに以下のように設定します。
[localhost] 127.0.0.1
Playbookを書く
次に、Playbookを作ります。今回は、localhost.ymlというplaybookを作り、taskをrole毎に分けて指定していく方式にします。タスクの量が大したことがないとか、わざわざ分ける方がわかりにくい場合は、1つのファイルに全て書いてもいいと思います。
--- - hosts: localhost connection: local gather_facts: no roles: # 略
このPlaybookを実行するには、以下を実行します。
ansible-playbook -i hosts localhost.yml --ask-become-pass
--ask-become-pass
オプションは、playbook内でbecomeを使わなければ、別に渡す必要はありません。
roles以下は定義したroleを追記していきます。roleはrolesディレクトリ以下に、role名で作成します。roles/[role_name]/tasks/main.yml
に、そのroleのタスクを定義します。
Role: homebrew_tapの作成
homebrew_tapモジュールを使い、brew tap
の自動化を行います。今回はcaskとversionsを使うことになったので2つです。with_itemsを使うと、処理を繰り返すことができます。{{ item }}
に配列の要素が1つずつ入って処理されます。
--- - name: homebrewのtapリポジトリの追加 homebrew_tap: tap="{{ item }}" state=present with_items: - caskroom/cask - caskroom/versions
Role: homebrewの作成
次に、homebrewモジュールを使って、brew install
の自動化を行います。
まずはhomebrewのアップデート。
--- - name: homebrewのアップデート homebrew: update_homebrew=yes
次に、brewパッケージのインストール。自分がよく使うものを入れます。ここは好きに変えてください。自分はRailsアプリケーションの開発とHerokuをプラットフォームとしてよく使うので、それらに関連するもののみ、とりあえず入れておきました。
Railsアプリ開発者として、自分的に一番肝なのは、imagemagick@6
を入れるところです。これは、Rubyでimagemagickを扱うgem rmagickがimagemagickのバージョン7に対応していないためです。imagemagick
のみの指定だと7系がインストールされます。2017年9月現在、それだとrmagickを使っているプロジェクトでbindle installがコケてしまいます。ちなみに、imagemagick@6
を入れるために、tapのところでcaskroom/versions
を指定していました。
- name: brewパッケージのインストール homebrew: name="{{ item.name }}" state="{{ item.state | default('latest') }}" install_options="{{ item.install_options | default() | join(',') if item.install_options is not string else item.install_options }}" with_items: - { name: ghq } - { name: git } - { name: heroku } - { name: imagemagick@6 } - { name: libxml2} - { name: openssl } - { name: peco } - { name: phantomjs } - { name: postgresql } - { name: readline } - { name: wget } - { name: yarn }
そして、brewパッケージのリンクの張り替えを強制的に行っておきます。これをしていないと、imagemagick@6
などが、システム的に認識されません。
- name: brewパッケージのリンクの貼り替え homebrew: name="{{ item.name }}" state="{{ item.state | default('latest') }}" install_options="{{ item.install_options | default() | join(',') if item.install_options is not string else item.install_options }}" with_items: - { name: imagemagick@6, state: linked, install_options: force} - { name: openssl, state: linked, install_options: force} changed_when: False
Role: homebrew_caskの作成
次に、homebrew_caskモジュールを使って、アプリをインストールしていきます。caskを使うと、Google ChromeやMicrosoft Officeなどのインストールをコマンドラインで行うことができます。/Applications/直下にインストールされます。
ここでの肝は、become: yes
を指定しておくことです。become: yes
をすると、sudo
コマンドで処理を実行してくれます。brew cask installを行うと、かなりの頻度でパスワードを聞かれるので、そこで自動化が止まるのを防ぎます。
failed_when: False
を指定するのは、インストールが失敗しても無視するためです。あえて無視させてます。インストールが失敗することがあるのか?という疑問が湧くと思いますが、例えばansibleを実行する前に既にChromeをインストールしていた場合、このroleを流すとChromeのインストールが失敗します(既に/Applications/Chrome.appがあるから)。GUIアプリケーションは冪等性の担保がしづらいので、まぁなければ入れるくらいでいいやというノリで管理します。
--- - name: caskパッケージのインストール homebrew_cask: name="{{ item.name }}" state="{{ item.state | default('installed')}}" with_items: - { name: atom } - { name: docker } - { name: docker-toolbox } - { name: firefox-esr } - { name: iterm2 } - { name: google-chrome } - { name: java } - { name: kitematic } - { name: microsoft-office } - { name: rubymine } - { name: skitch } - { name: thunderbird } - { name: vagrant } - { name: virtualbox} - { name: visual-studio-code } become: yes failed_when: False
Role: anyenvの作成
anyenvは、Rubyのバージョン管理のrbenvやNodeのバージョン管理のndenvなど、様々なプログラミング言語の**envを管理するためのツールです。インストールとPATHを通すのを自動化します。
ここでの肝は、lineinfileモジュールを使うところです。lineinfileモジュールは、指定された文字列を追加または置換することができます。
置換する場合は、正規表現で対象行を指定します。追記ならば、特に指定なくてもいいです。実行するたびに毎度毎度追記されるんじゃないか?と思っていたのですが、そんなことはありませんでした。
--- - name: anyenvをcloneする git: repo: 'https://github.com/riywo/anyenv.git' dest: ~/.anyenv - name: anyenvへのPATHを通す lineinfile: dest: ~/.bash_profile line: "{{ item }}" with_items: - export PATH="$HOME/.anyenv/bin:$PATH" - eval "$(anyenv init -)"
Role: rbenvの作成
anyenv経由でrbenvを入れます。shellモジュールを使うと毎回実行されてしまいますが、createsオプションを渡すと、既にファイルが存在すれば実行しなくなります。今回は既にrbenvディレクトリが作られていたら実行しないようにしています。Rubyのインストールのところも同様です。
また、Rubyのインストールのところでは、with_itemsにruby_versions
という変数を渡しています。そして、whenにruby_versions is defined
という条件を渡しています。これは、インストールするRubyのバージョンを色々変えられるようにです。特に指定がない場合はこの処理自体が無視されます。
最後に、rbenv rehash
を行います。これは必ずchangedになってしまうので、changed_when: Falseを指定しています。
--- - name: rbenvのインストール shell: anyenv install rbenv args: creates: ~/.anyenv/envs/rbenv/bin/rbenv - name: Rubyのインストール shell: rbenv install {{ version }} args: creates: ~/.anyenv/envs/rbenv/versions/{{ version }} with_items: "{{ ruby_versions }}" loop_control: loop_var: version when: ruby_versions is defined - name: rbenv rehash shell: rbenv rehash changed_when: False
Role: ndenvの作成
こちらもrbenvと同様です。
--- - name: ndenvのインストール shell: anyenv install ndenv args: creates: ~/.anyenv/envs/ndenv/bin/ndenv - name: Nodeのインストール shell: ndenv install {{ version }} args: creates: ~/.anyenv/envs/ndenv/versions/{{ version }} with_items: "{{ node_versions }}" loop_control: loop_var: version when: node_versions is defined - name: ndenv rehash shell: ndenv rehash changed_when: False
Role: osx_defaultsの作成
osx_defaultsモジュールを使うと、Mac OSXの設定の変更が容易にできるということだったのでやってみました。
まぁGUIから変えても大した時間かからなかったりもしますが、Finderの設定は毎回面倒なので自動化しとくと楽ですね。どうせ拡張子と隠しファイルは見れないと開発者的に仕事が捗らないし。
--- - name: Safariのデバッグメニューを有効化 osx_defaults: domain: com.apple.Safari key: IncludeInternalDebugMenu type: bool value: true state: present notify: Restart Safari - name: Finderで隠しファイルを表示 osx_defaults: domain: com.apple.finder key: AppleShowAllFiles type: bool value: true state: present notify: Restart Finder - name: Finderで拡張子を表示 osx_defaults: domain: com.apple.finder key: AppleShowAllExtensions type: bool value: true notify: Restart Finder - name: FinderでPathBarを表示 osx_defaults: domain: com.apple.finder key: ShowPathbar type: bool value: true notify: Restart Finder - name: Finderでステータスバーを表示 osx_defaults: domain: com.apple.finder key: ShowStatusBar type: bool value: true notify: Restart Finder - name: Dockを自動的に隠す osx_defaults: domain: com.apple.dock key: autohide type: int value: 1 state: present notify: Restart Dock
Role: fishの作成
fishのインストールとoh-my-fishのインストールとpecoとanyenvのfish対応設定のダウンロードとか諸々やってます。
ここでの肝は、get_urlモジュールを使って、ファイルのダウンロードを行っています。私がgistに書いているpecoの便利関数のfish版などを取得しています。よくある手法では、dotfilesなどを作って、いろいろな設定ファイルを管理するというのがありますが、oh-my-fishやプラグインのインストールとかをすることもあるだろうから、あえて管理をdotfilesから分けてgistにしました。
--- - name: homebrewのアップデート homebrew: update_homebrew=yes - name: fishのインストール homebrew: name: fish state: latest - name: fishを選択可能なshellとして追加 lineinfile: dest: /etc/shells line: /usr/local/bin/fish become: yes - name: 既存のshellを確認する shell: echo $SHELL register: current_shell changed_when: false - name: fishをデフォルトのshellにする shell: chsh -s /usr/local/bin/fish become: yes when: current_shell.stdout != '/usr/local/bin/fish' - name: oh-my-fishをclone git: repo: https://github.com/oh-my-fish/oh-my-fish dest: ~/oh-my-fish - name: oh-my-fishをlocalからインストールする shell: bin/install --offline --noninteractive args: chdir: ~/oh-my-fish creates: ~/.config/omf - stat: path=~/.anyenv register: anyenv - name: anyenvの設定を行うfishファイルをコピーする get_url: url: https://gist.githubusercontent.com/patorash/b5a1033c08d2c4df103457866b2dcaa2/raw/9a6c7e2111d95db0fdb07d43a41fa2eea5352b5f/anyenv.fish dest: ~/.config/fish/conf.d/anyenv.fish when: anyenv.stat.exists - name: pecoの便利関数のfishファイルをコピーする get_url: url: https://gist.githubusercontent.com/patorash/1de94fcb7efeb847501d2fb7900c2deb/raw/7251e11091b388dd527e03c172913e188d1432f6/peco.fish dest: ~/.config/fish/conf.d/peco.fish - name: aliasがまとめられたfishファイルをコピーする get_url: url: https://gist.githubusercontent.com/patorash/ca18e28b22f12a55e2539477abcda26d/raw/1ecc260dad5779be94a4304f2980a114ea85e7ac/alias.fish dest: ~/.config/fish/conf.d/alias.fish - name: functionsディレクトリを作る file: path: ~/.config/fish/functions state: directory - name: カスタムキーバインディングのfishファイルをコピーする get_url: url: https://gist.githubusercontent.com/patorash/47e7e179fdd80fe7e9517b93cb2f3d82/raw/8f74b8614eb0ddc3f3f3ce0c1964117cdbb08fdf/fish_user_key_bindings.fish dest: ~/.config/fish/functions/fish_user_key_bindings.fish force: true
Role: dotfilesの作成
dotfilesも使います。fishの設定は他の管理にしましたが、gitの設定ファイルなどはdotfilesを使います。 単純に、.gitconfigと、.global_gitignoreというファイルを含むリポジトリをcloneして、ホームディレクトリにシンボリックリンクを作成しているだけですが、これも自分でやると結構面倒なので自動化しておくと楽ですね。
--- - name: dotfilesをgit cloneする git: repo: https://github.com/patorash/dotfiles.git dest: ~/dotfiles - name: シンボリックリンクの作成 file: src: ~/dotfiles/{{ item }} dest: ~/{{ item }} state: link force: true with_items: - .gitconfig - .global_gitignore
変数を定義する
RubyとNodeのインストールで変数を渡すようにしていたので、その辺りも記述します。vars/main.yml
に定義してみました。
--- ruby_versions: - 2.3.4 - 2.4.2 node_versions: - v8.5.0
localhost.yml
完成したのがこちら。作ってきたroleを順番に実行させていっているだけですが。
--- - hosts: localhost connection: local gather_facts: no vars_files: - vars/main.yml roles: - homebrew_tap - homebrew - homebrew_cask - anyenv - rbenv - ndenv - osx_defaults - fish - dotfiles
気付き
数日間に渡ってMacの環境構築の自動化に取り組んできたのですが、自分で確認しながら取り組むと、よりAnsibleへの理解度が深まり、やってよかったなと思いました。以前にAnsible本を読んでサーバの環境構築の自動化の写経はしたのですが、普段Herokuを使っているのでほとんど自動化する機会がなく、せっかく読んだのに忘れてしまっていました(それでもうっすら記憶にあって助かりましたが)。
また、fishのセットアップの自動化や、anyenvのセットアップの自動化のあたりでも色々調べて新しい知識が得られたりもしました。副次的な効果でしたが、そういうのもまとまって理解できたのもよかったです。
成果として、Playbookが完成した後にザーッと流すと、1日ちょっとかかっていたMacのセットアップが、1〜2時間程度でほとんど終わるようになりました。これでもうMacを初期化するのが億劫になることもなさそうです。
リポジトリ
自分用にブランチを分けたので、masterとは結構変わってます。
GitHub - patorash/ansible-mac-provisioning at for-patorash
masterのほうが作りが雑なので、あとあと直していきたいところです。