patorashのブログ

方向性はまだない

Macの環境構築を自動化してOS再インストールに備える

会社で新しい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のデフォルトの定義の変更も手軽に行えるというのを見つけたので、それらも自動化するようにしました。

  • Finderで隠しファイルを表示できるようにすること
  • Finderで拡張子を表示できるようにすること
  • Dockは自動的に隠すようにすること
  • Safariデバッグツールを有効にすること

その他、まだいろいろやりたいことはあるのですが、やり方がわからないものとかがあったので、まだやってません。

自動化の準備

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を入れるところです。これは、Rubyimagemagickを扱う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 ChromeMicrosoft 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モジュールは、指定された文字列を追加または置換することができます。

lineinfile - Ensure a particular line is in a file, or replace an existing line using a back-referenced regular expression. — Ansible Documentation

置換する場合は、正規表現で対象行を指定します。追記ならば、特に指定なくてもいいです。実行するたびに毎度毎度追記されるんじゃないか?と思っていたのですが、そんなことはありませんでした。

---
- 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の設定の変更が容易にできるということだったのでやってみました。

osx_defaults - osx_defaults allows users to read, write, and delete Mac OS X user defaults from Ansible — Ansible Documentation

まぁ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のほうが作りが雑なので、あとあと直していきたいところです。