私の記憶が確かなら…2015年ごろMacの開発環境構築のAnsible化が流行ったと記憶しています。 当時、自分もやってみようかなと思ってましたが、Macを買い換えるタイミングもないし何となく今まで来ていたので、先日MacBookを買い換えて再セットアップが必要になったため今更ながらやってみました。 将来、Macを買い換えた時の自分のためが8割ですが、探すと古い記事が多くてAnsibleやHomebrewが結構変わっているので、2018年版として役に立つこともあるかもと思ったので記事にしてみます。

環境

  • Mac OS 10.14.1 Mojave
  • Homebrew 1.8.2
  • Ansible 2.7.1
  • mas 1.4.3

Homebrew のインストール

まずはHomebrewを入れないことには始まりませんので公式サイトの手順に従ってインストールしましょう。 (そういえば、この時点でXcodeを全く意識してなかったけどすんなり通ってしまった…何か変わったのかな..)

$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

ちなみに、以前はHomebrew-caskを使うためには、brew tap caskroom/cask で参照するリポジトリを追加する必要がありましたが、今はHomebrewに統合されているので必要ありません。
パッケージの検索も brew cask search <package>ではなく、brew search <package>でいけます。

<参考>
HomebrewのCaskのレポジトリがcaskroomからhomebrewへ移動

Ansibleのインストール

HomebrewでAnsibleとPyhonをインストールします。ここまでは手動。

$ brew install python
$ brew install ansible

AnsibleでPlaybookを作っていく

ここからansible-playbookで自動化するため、適当なフォルダを作って作業していきます。

Inventry

とりあえずlocalhostにつなげるInventryファイルを作ります。

echo localhost > hosts

Playbookの作成

次にplaybook本体を作っていきます。今回はplaybook.ymlというファイルを作りました。
冒頭部分から順を追って説明していきます。

よく見る、sudo:という書き方は古いみたいなので become:としました。

- hosts: all
  connection: local
  gather_facts: no
  become: no

Homebrew-caskパッケージのインストール

homebrew_caskモジュールでMac OSアプリをインストールしていきます。 コード自体特筆することは特にありませんが、これ全部サイト回ってインストールしてくのは大変なのですごい楽です。

    - name: install homebrew cask packages
      homebrew_cask:
        name: "{{ item }}"
      with_items:
        - cheatsheet
        - clipy
        - docker
        - dropbox
        - evernote
        - firefox
        - google-chrome
        - intellij-idea
        - iterm2
        - jetbrains-toolbox
        - vagrant
        - virtualbox
        - visual-studio-code

Hombrewパッケージのインストール

homebrewパッケージ でインストールしていきます。 以前はハッシュのリストをwith_itemsでループさせる手順が多く見られましたが今は非推奨みたいで配列を使用しました。

    - name: install homebrew packages
      homebrew:
        name:
          - ansible
          - git
          - go
          - hugo
          - jq
          - mas
          - nkf
          - nodebrew
          - peco
          - python
          - rbenv
          - ruby-build
          - tree
          - wget
          - zsh

以前の記事にも書いたのですが、OS標準のvimを無効にするため、with-override-system-viオプション付きでインストールするためvimは別タスクにしました。

    - name: install vim
      homebrew:
        name:
          - vim
        install_options:
          --with-override-system-vi

zsh関連の設定

oh-my-zshはshellでインストールするので、冪等性は自分で確保しないといけません。 statで~/.oh-my-zsh/を存在チェックした結果を変数に格納して、存在しない場合だけインストールします。 デフォルトシェルの変更はAnsibleに標準で入っている user Moduleで行いました。 また、個人で使うならログインユーザのnameはベタ書きでも良いのですが、公開することを考えて ansible_ssh_user変数から取得するようにしています。

    - name: 'check oh-my-zsh'
      stat: path=~/.oh-my-zsh/
      register: d
    - name: 'install oh-my-zsh'
      shell: sh -c "$(curl -fsSL https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh)"
      when: d.stat.exists == false
    - name: 'change default shell'
      user:
        name: "{{ ansible_ssh_user }}"
        shell: /usr/local/bin/zsh

MasでCaskでインストールできないアプリをインストールする

XcodeなどHomebrew-caskでインストールできないアプリをmas(Mac App Store command line interface)でインストールしていきます。このmasの存在は今回初めて知りました。 この部分はansible-garaxyで公開されているansible-role-masというRoleを使わせてもらいました。

Roleのインストール

あとで使う別のRoleと共にrequirements.ymlというファイルに記述しておきます。

---
- name: geerlingguy.mas
- name: geerlingguy.dotfiles

続いてansible-garalxyコマンドでRoleをインストールするのですが、 よく分からないところにインストールされると嫌なので作業ディレクトリの下のrolesフォルダにインストールしました。

$ ansible-galaxy install -p roles -r requirements.yml

次に、masでインストールするためにはアプリのIDが必要ですので、古い方のMacで mas list してインストールされているアプリのIDを調べます。 今回はXcodeをインストールしたいのでIDを控えておきます。

$ mas list
497799835 Xcode (10.1)
409183694 Keynote (8.3)
408981434 iMovie (10.1.10)
409201541 Pages (7.3)
682658836 GarageBand (10.3.1)
409203825 Numbers (5.3)

playbook.ymlに戻って、先ほど取得したRoleで必要なmas_installed_apps変数にXcodeを定義します。この変数にセットしておくとインストールしてくれる仕組みです。

  vars:
    # for ansible-role-mas
    mas_installed_apps:
      - { id: 497799835, name: "Xcode (10.1)" }

あとはrolesに geerlingguy.mas を定義します。Playbookを実行するとRoleが実行されてXcodeがインストールされます。 (tasksとrolesを同時に使ってるのはイマイチな気がするけど)

  roles:
    - geerlingguy.mas

ちなみにOSX 10.13以上はコマンドでApp Storeにログインできなくなってますが、App Storeアプリを一度立ち上げてログインしておけばそれだけで認証してくれるようです。

$ mas signin <Apple ID>
Error: The 'signin' command has been disabled on this macOS version. Please sign into the Mac App Store app manually.
For more info see: https://github.com/mas-cli/mas/issues/164

dotfiles

自分のdotileはもともとGithubに置いていたのですが、これまたAnsible-garaxyから拝借したRoleを使って設定していきます。 リポジトリのパスとクローンするフォルダ、対象のファイルを変数に指定します。

  vars:
    # for ansible-role-dotfiles
    dotfiles_repo: "git@github.com:eichisanden/dotfiles.git"
    dotfiles_repo_local_destination: "~/src/github.com/eichisanden/dotfiles"
    dotfiles_files:
      - .bashrc
      - .eslintrc
      - .gitattributes
      - .gitconfig
      - .gitignore
      - .psqlrc
      - .vimrc
      - .zshrc

roleを下記のように定義します。実行すると、指定したフォルダにクローン、更にクローンしたファイルをホームディレクトリにシンボリクリンクを貼ってくる仕組みです。

  roles:
    - geerlingguy.dotfiles

Mac OSの環境設定

DockやFinderの設定なども自動化してみました。 標準のosx_defaultsモジュールのdefaultsコマンドで変更できることが設定可能です。 キー名を調べるのが結構面倒くさいのと、即反映される設定とkillall Dockとかしてプロセスを上げ直さないと反映されないものがあり試行錯誤しながら設定していきました。

    - name: 'setting macOS'
      osx_defaults: >
        domain={{ item.domain }}
        key={{ item.key }}
        type={{ item.type }}
        value={{ item.value }}
        state={{ item.state|default('present') }}        
      with_items:
        # auto hide dock (need "killall Dock")
        - { domain: com.apple.dock, key: autohide, type: bool, value: true }
        - { domain: com.apple.dock, key: autohide-time-modifier, type: int, value: 0 }
        # Move dock to left side
        - { domain: com.apple.dock, key: orientation, type: string, value: "left" }
        # Show Status bar in Finder (need "killall Finder")
        - { domain: com.apple.finder, key: ShowStatusBar, type: bool, value: true }
        # Show Path bar in Finder
        - { domain: com.apple.finder, key: ShowPathbar, type: bool, value: true }
        # Show Tab bar in Finder
        - { domain: com.apple.finder, key: ShowTabView, type: bool, value: true }
        # Show the hidden files
        - { domain: com.apple.finder, key: AppleShowAllFiles, type: bool, value: true }
        # Enable text selection in QuickLook
        - { domain: com.apple.finder, key: QLEnableTextSelection, type: bool, value: true }
        # Show the battery percentage from the menu bar (need "killall SystemUIServer")
        - { domain: com.apple.menuextra.battery, key: ShowPercent, type: string, value: "YES" }

参考

おわりに

なんでも自動化すれば良いものではないのでやりすぎは注意ですが、まだ完成していないのでちまちま直してくつもりです。 あと、あまりAnsibleに詳しくない自分にとって練習の題材には良かったです。あとは何年か後にMacを買い換えた俺がこのエントリを思い出して喜んでくれたら幸いです。 あと、作ったものはGitHubに置いてあります。 https://github.com/eichisanden/mackbook-playbook

参考