Macの環境構築をAnsibleで自動化してみた 2021年版

こんにちは、Progateでサーバーサイドエンジニアをしている708uです。 本記事は Progate Advent Calendar 19日目の記事になります。今回はAnsibleを用いたMacの環境構築自動化をご紹介したいと思います。

発端

そもそものお話になりますが、私がProgateにジョインしたのは2021年12月1日、つまりこの記事が投稿される約2週間前になります。となると当然、新しく業務で利用するマシンも支給されるため、1から開発環境構築をする必要がありました。

私自身、私物や業務含めてMacを1からセットアップする作業は何回か行っていて今回もそれが必要になるのですが、その度に以下のような作業が必要になっていました。

  • 普段使っているGUI/CLIツールを調べて片っ端から入れ直す
  • 移行元Macの設定を眺めながら、移行先Macの設定を同じ値に変える
  • (細かい設定方法を忘れて沢山ググる)

いざ久々にやるとなると結構面倒です。ただ、以前は環境構築作業自体は頻度が少ない事もあり、「まあいいか」ぐらいの気持ちでポチポチしていました。

ですが、少し前に「Ansibleを使うといい感じに環境構築自動化ができるらしい」という事を耳にし、その時から「いつか環境構築自動化したい!」という欲を持っていました。

そんな折、新しいおもちゃ仕事道具を支給してもらったので、「これはまさしく今やるタイミングだな」と思いAnsibleでの環境構築をやってみる事にしました。

Ansibleとは

一応Ansible自体についても触れておきます。Ansibleはインフラの構成管理とOSやソフトウェアなどの設定作業を自動化するツールです。yamlに各resourceの設定を記述して実行することで、その構成通りに構築を行うことができます。

また、公式に用意されている機能や有志によって開発されているrole(後述します)は基本的に冪等性が担保されているため、何回実行を行っても同じ結果が担保されるケースが多いです。開発者が直接commandを記述し実行する場合等は、冪等性を担保できる実装にする必要があります。

上記から、一般的なユースケースはインフラ構築が主になるのですが、実はAnsibleを実行するtargetはlocal、つまり自分自身のマシンにすることも可能です。

対象ホストを自分自身に設定することで、Macにインストールすべきアプリケーション等を設定として記述し、新しい環境でansibleのコマンド1発で環境構築することが可能になります。

自動化対象

自動化をするにあたり、以下項目を自動で構築できるようにします。

  • homebrew経由で管理可能なGUI/CLIアプリケーションのインストール
  • App Store経由で管理されているアプリケーションのインストール
  • Macの詳細設定で設定可能な値のセットアップ
  • dotfilesの自動セットアップ
  • zshのセットアップ

自動化の環境再現度は80~90%ぐらいを目指します。

準備

何はともあれAnsibleが使えないと何もできないので、インストールしましょう。homebrew経由で導入できます。

$ brew install ansible

これでAnsibleが使えるようになったので自動化作業を行うことができます。その前にAnsibleにおける基本的な用語やファイルの役割を簡単に紹介します。詳細な仕様に関しては他解説記事や公式ドキュメントを参照ください。

task

Ansible上での処理の1単位です。具体的処理や条件分岐等を組み合わせることで1taskになります。taskを複数組み合わせることで具体的な設定を行っていきます。

role

複数のtaskや変数をまとめる1単位です。再利用や関心の分離のためにユーザー自身で作成する事もできますが、Ansible Galaxyというコミュニティにて便利なroleが公開されているので、それらをインストールして使用する事も可能です。

playbook

taskやroleといった処理の実行、ansibleの設定、deploymentがどのように行われるかをオーケストレーションします。 実際に設定のdeployを行う際は、playbookを指定することで設定どおりの反映を行います。

collection

task、role、playbook等を纏めてパッケージ化したものです。今回使用するcommunity.general.~~と書かれたものはcollectionとなっており、taskから呼び出します。collectionに対してパラメータを渡すことで該当する処理を実行できます。

inventory

どのサーバに対して定義した設定を適用するのかについて、接続先をまとめた定義です。複数ホストを指定したり、groupingする事でまとめて反映を行う事も可能です。 今回はlocalhostに対して反映を行うため、指定はlocalhostのみです。

構成

ディレクトリ構成は以下のようにしています。

├── playbook.yml
├── requirements.yml
└── roles
    ├── zsh
    │   └── tasks
    │       └── main.yml
    ├── homebrew
    │   └── tasks
    │       └── main.yml
    ├── homebrew_cask
    │   └── tasks
    │       └── main.yml
    ├── mac_app_store
    │   └── tasks
    │       └── main.yml
    └── mac_os
        └── tasks
            └── main.yml

playbook.ymlにホストや実行したいroleを記述します。

---
- name: set up
  hosts: 127.0.0.1
  connection: local
  gather_facts: no
  become: no

  roles:
    - geerlingguy.dotfiles
    - zsh
    - homebrew
    - homebrew_cask
    - mac_app_store
    - mac_os

各rolesの具体的処理はroles/${ロール名}/tasks/main.ymlに記述します。 Ansibleはplaybookで指定したroleに対して、上記ディレクトリ構造である事を期待して解釈するようになっている為、roleを読み込む為の特別な設定は不要です。 rolesの切り分けは、冒頭でも記述した通り自動化したい項目で行っています。

また、requirements.ymlに関しては後述します。

Roles

それでは、具体的なroleを作っていきます。説明でも触れたとおり、roleはtaskの組み合わせになり、上から順番に実行されていきます。この例では標準出力にhelloこんにちはと出力されます。

- name: greet1
  shell: echo 'hello'

- name: greet2
  shell: echo 'こんにちは'

homebrew

まず手始めに、homebrewで管理するCLIツールのインストールを自動化します。

roles/homebrew/tasks/main.ymlに以下の記述をします。

- name: update homebrew
    community.general.homebrew:
    update_homebrew: true

- name: install homebrew packages
    community.general.homebrew:
    name:
        - ansible
        - anyenv
        - bat
        - exa
        - fd
        - fzf
        - gh
        - ghq
        - git
        - jq
        - lsd
        - mas
        - node
        - procs
        - ripgrep
        - tig
        - tree
        - wget
        - yarn
        - zsh

2つのtaskが実行されているのが分かると思います。何が起きるか何となく分かるとは思いますが、以下を実行しています。

  • homebrew自体のupdate
  • 列挙したhomebrewのpackageをインストール

実行される内容はcommunity.general.homebrew collectionのname: に管理したいツールを配列で渡すことでそのツールがインストールされます。

このcollectionは冪等性が担保されているので、現在homebrewでツールがインストールされているか否かに関わらず、必ず同じ結果になります。

ところで、このtaskで指定しているcollectionはcommunity.general.homebrewとなっていますが、私が類似の開発環境構築エントリを調べていた限りではhomebrewと短縮してしているケースが殆どでした。

この点について挙動が気になったので公式ドキュメントを確認したところ、短縮しても指定自体は可能だが同名のcollectionやmoduleとconflictする可能性があるので、2.10以降のversionではFQDNで指定することが推奨されていました。

https://docs.ansible.com/ansible/latest/collections/community/general/index.html

In Ansible 2.10 and later, we recommend you use the fully-qualified collection name in your playbooks to ensure the correct module is selected, because multiple collections can contain modules with the same name (for example, user). See Using collections in a Playbook.

よって、本エントリ内では基本的にFQDNの指定を行います。

homebrew cask

続いてGUIアプリケーションの管理を記述します。管理にはcommunity.general.homebrew_cask を使用します。

一応homebrew caskについて少し補足すると、GUIでのアプリケーションをhomebrewで管理する事が可能になる機能となります。

とはいえ、設定する内容はhomebrewで行ったものと殆ど相違はありません。単純に指定をhomebrew_caskに変更しただけです。

roles/homebrew_cask/tasks/main.ymlに以下の記述をします。

- name: install homebrew cask packages
  community.general.homebrew_cask:
    name:
      - alfred
      - asana
      - appcleaner
      - discord
      - docker
      - dropbox
      - font-hackgen-nerd
      - google-chrome
      - google-japanese-ime
      - hyperswitch
      - iterm2
      - karabiner-elements
      - ngrok
      - notion
      - postman
      - tableplus
      - visual-studio-code
      - zoom

ここでの注意点は、homebrew_caskでdockerをインストールしている点です。(Ansibleで管理するという文脈からは少しそれますが...)Macで利用する場合、caskでdockerを指定する事によってDocker desktopとその背後にいるDocker Engine周辺のツールがインストールされます。

mas

上記のhomebrew_caskによってGUIアプリケーションの大部分はインストール可能ですが、App store経由で配布されているアプリケーションはその限りではありません。

恥ずかしながら私も今回構築自動化を行うまで知らなかったのですが、mas-cliというツールを使うことでコマンドライン上でApp storeのアプリケーション管理が可能になります。

ref: https://github.com/mas-cli/mas

Ansibleではcommunity.general.masというCollectionを用いることで管理が可能になります。

roles/mac_app_store/tasks/main.ymlに以下の記述をします。

- name: install applications from app store
  community.general.mas:
    upgrade_all: yes
    id:
      - 803453959  # Slack

今までのhomebrewでのインストールと違う点が1箇所あります。定義を見ると分かると思いますが、インストールするアプリケーションの列挙が文字列ではなくidとなっています。

このidはアプリケーション固有に振られているようですが、一瞥するとインストールしたいアプリケーションのidが何番なのかは分からないかと思います。

こちらを調べる方法はいくつかあるのですが、最も単純な方法はmas-cliを用いて検索を行うことです。mas-cliはhomebrewでインストール可能です。検索結果の左端に表示されている数値がアプリケーションのidとなります。

$ brew install mas
$ mas search slack
   803453959  Slack for Desktop                              (4.23.0)
  1176895641  Spark – Email App by Readdle                   (2.11.8)
  1043270657  GIF Keyboard                                   (2.0.5)
  ...

dotfiles

次は、dotfilesの設定を自動化しましょう。個人的にdotfilesの管理は別repositoryで行いたい為、Ansible Galaxyからgeerlingguy.dotfilesというroleをインストールして使用します。

Ansible Galaxyでホスティングされているroleは、以下手順でインストールできます。

  1. requirements.ymlに以下を追記
---
- name: geerlingguy.dotfiles
  1. 以下コマンドを実行
$ ansible-galaxy install -r requirements.yml

インストールが完了すると、roleにgeerlingguy.dotfilesが指定できるようになります。

このroleはdotfilesのrepositoryと、どのdotfilesを使用するのかをvarsで指定することで、Home配下に各dotfilesへのシンボリックリンクを貼ってくれます。これで、環境構築自動化とdotfiles管理を別々のrepositoryで行うことができます。

あとは、varsにdotfiles用の設定を行いましょう。私はplaybook.ymlに直接記述していますが、別ファイルに切り出しても良いかもしれません。

  vars:
    dotfiles_repo: "git@github.com:path/to/your/repo"
    dotfiles_repo_version: main
    dotfiles_repo_local_destination: "path/to/your/destination"
    dotfiles_files:
      - .gitconfig
      - .zshrc

1点注意点として、dotfilesのrepositoryをcloneする際のdefault branch名がmasterになっている為、branch名をmainにしている場合は処理が失敗します。 その場合、上記のように dotfiles_repo_version: main を指定することでcloneするbrunch名を変更できます。

zsh

macOS Catalinaからdefaultのshellはbashからzshに変更になっていますが、バージョンアップが面倒なのでbrewでインストールしたものを使うように変える処理を自動化してみます。

roles/zsh/tasks/main.ymlに以下の記述をします。

- name: edit /etc/shells
  lineinfile:
    dest: /etc/shells
    line: "/usr/local/bin/zsh"
  become: true

- name: change shell to brew zsh
  shell: chsh -s /usr/local/bin/zsh

これで、使用されるshellがbrewでインストールしたzshに差し替える処理が自動化できます。一点注意が必要な点として、/etc/shellsを書き換えるにはsudoが必要なため、become: trueを指定しています。そのため、playbook実行時にpassword入力が求められます。

system setting

最後の設定項目です。キーボードのリピート速度等、Macの詳細設定で指定できる値を自動で設定します。ちなみに、この設定値はGUIの詳細設定で設定できる最小/最大値を超えた値を指定できるので、標準では設定不能な値に書き換える事も可能です。

roles/mac_os/tasks/main.ymlに以下の記述をします。

- name: KeyRepeat
  community.general.osx_defaults:
    key: KeyRepeat
    type: int
    value: 1

- name: AppleShowAllFiles
  community.general.osx_defaults:
    domain: com.apple.finder
    key: AppleShowAllFiles
    type: bool
    value: TRUE

- name: AppleShowAllExtensions
  community.general.osx_defaults:
    domain: NSGlobalDomain
    key: AppleShowAllExtensions
    type: bool
    value: TRUE

設定できる値はMacのdefaultコマンドで設定できるものとなります。但し、これらの設定値は公式には公開されておらず、またOSのバージョンアップ時に変更される可能性もあります。

上記以外にも設定できる項目はたくさんあるのですが、全て自動化しようとすると無限に時間が溶けていくので、ちょうどよい塩梅を目指して設定していくのが良いかもしれません。

反映

ここまで記述できたら、あとは以下コマンドを実行するだけです。環境構築が終わるまでコーヒーでも飲みながら待ちましょう。

ansible-playbook playbook.yml -K

-K オプションを指定することで、become: true時にpasswordを求められる箇所で入力するpasswordを指定できます。今回はzshのchshを行うために設定していますが、becomeが不要な方は外しても大丈夫です。

まとめ

いかがでしたでしょうか。すべてを自動化してわけではないので完全では無いものの、環境構築時にやらなければいけない作業はだいぶ減ったと思います。 個人的には、途中から自動化作業自体が楽しくなっていたりしました。次回のマシンの環境構築が楽しみになりました。

明日はsatetsu888 さんによる Progate AdvendCalendar 20日目です。お楽しみに!