最近は Ansible で Mac をセットアップするのが流行っているようですね。

「新年度の組織変更のついでに Mac が支給されたウェーイ」なのを機に Ansible に触ってみようという方も多いのではないかと思います。

ところが Ansible のその手軽さゆえ、入門に際して誰しもつまずくのが、

「ググる記事どれもこれもが断片的かつ俺流で、構成管理の作法も正解もわからない... (◞‸◟)」

という点ではないでしょうか。

そこでこの記事では、断片的な個別事例ではなく、Ansible の基本概念や Mac への適用方針など、構成管理サイクル全般の設計に役立つトピックを網羅的に解説してみたいと思います。

ざっくり macOS Sierra + Ansible 2系の環境を想定していますが、基本はどのバージョンでも大きく違わないはずです。

ご参考になれば幸いでございます! (๑˃̵ᴗ˂̵)و


なお、この記事では英字の「Playbook」とカタカナの「プレイブック」を以下の意味で使い分けることにします。

  • Playbook: Ansible の構成ファイルのひとつとしての Playbook そのもの。
  • プレイブック: ↑の Playbook を含む、構成管理に関わるアセット一式。

まず Ansible を動かすまで

どうやって Ansible を動かせる状態に持っていくか?
いろいろと準備が必要で、全自動構成管理、というわけにはいかないみたいです。

プレAnsible

Ansible は出荷時点の Mac にはインストールされていません。
自分でインストールする必要があります。

インストール方法はいくつかあり、さらにそのインストール方法を実行するための事前作業が各々必要です。

Ansible をインストールするためにアレ入れてコレ入れて、ソレはどうやって構成管理しようかな?
...あれ、何のために Ansible 入れたいんだっけな? (○´―`)ゞ

と、Ansible を使い始める前に構成管理を迫られてしまい悲しい気分。

また、dotfiles の配備や *env系のセットアップなどプラットフォームの基本的な設定も Ansible に先立って適用しておきたいところですし、後述しますがいつでも何に対しても Ansible を使うのが最適というわけでもないです。

したがって、Mac全体のセットアップのおおまかな流れは、

  1. Ansible のインストールや dotfiles の配備などの基本セットアップをおこなう。
  2. プレイブックを適用する。
  3. Ansible では賄いきれない、または妥当ではない部分を別のスキームでセットアップする。

とするのがよさそうです。

インストール

Ansible のインストールは Homebrew を使うのがお手軽確実でよいでしょう。

ところで Homebrew は「ほーむぶるぅ」と発音するのが一般的ですが、わたしは「ほーむぶりゅぅっ!ぶりゅぅっっ! (ง°̀ロ°́)ง」と言っています。
これは昔、ガラケー全盛の時代に、au の EZアプリなるサービスが BREW というアプリケーション・プラットフォームを採用していて、それが「ぶりゅー」と発音されていたことに影響を受けてうんちゃらかんちゃら。
ええ、どうでもいい話ですね。
(au の公式サイトに情報が載っているということは、今も EZアプリって生きているのでしょうか???)

それはさておき。

Homebrew を動かすためには Xcode (のコマンドライン・ツール) が必要っぽいので、適当にインストールしておきましょう。

Homebrew のインストールは、公式サイトの手順を見ると curl で公式インストール・スクリプトを持ってきて ruby に流し込むだけのようなので自動化できなくもありません。
が、いつ手順が変更されるかもわかりませんので、都度手作業でインストールしたほうがよいでしょう。

Homebrew のインストールが完了したら続いて brewコマンドで Ansible をインストールします。

command
brew install ansible

これで ansible, ansible-playbook などのコマンドが使えるようになっているはずです。

プレイブック作成の前に知っておくべきこと

プレイブックを組む前に知っておきたいトピックを解説します。

構成要素

まずは Ansible を構成する要素を押さえておきましょう。

Inventory

Ansible を作用させる対象ホストを列挙したファイルです。

いくつかのホストを Group としてまとめて扱うことができます。
Group の区分に持たせる意味はまったくの自由なので、自分の都合に合ったグループ分けができます。

他のファイルはだいたい YAML なのですが、Inventory は INI形式で記述します。

Playbooks

ホストに対する処理内容を記述したファイルです。
(正確には「当該ホストのあるべき状態を定義する」と述べるのが適切なのですが、Ansible の用途が構成管理に留まらないため敢えてこう書きます)

具体的な処理内容 (Action = 実行する Module とそのパラメータ) やループ、条件分岐などの制御構造をタスクとして列挙しつつ、適用対象のホストを Inventory の Group から選択して宣言します。

なお、Module とは実際の処理機能を提供するプラグイン式のライブラリのことです。(付属Module一覧)

Roles

Playbook中の複数の処理とリソースをひとまとめに構造化して名前を与えたものです。

関連する処理ごとにまとまるので見通しがよくなったり、複数の Playbooks でコードを使い回したり、巨大な Playbook を細分化できたりと、うまく使うと諸々幸せになれます。

ヘタクソな使い方をすると、同じ処理が別々の箇所で重複したり、とっ散らかって全体像が見えづらくなったりして不幸せになれます。

Variables

プレイブックには色々な箇所、色々な方法で変数を注入することができます。

あまりに自由に注入できるので、変数はきちんとルールを定めて扱うのがよいです。
(Ansible Best Practices でも Keep It Simple と言っています)

公式ドキュメントではさらっと触れているだけですが スコープが 3種類 あり、技巧的に変数定義を試みたものの意図通りに動かない場合はだいたいこのスコープの挙動を読み誤っています。

また、機密情報を暗号化して格納するために Vault という仕組みが用意されていますが、これを利用するかどうかは一考の余地ありです。(後述)

Configuration file

Ansible全体の挙動を制御する設定ファイルです。

とりあえず example をベースにカスタマイズするとよいでしょう。

設定ファイルを配置しなくてもデフォルト設定で Ansible を動かすことはできますが、その挙動が実行環境の変化に対して脆弱になるのできちんと配置することをオススメします。

運用支援

運用ツール/方式は充実しているものの、基本的には小細工なしで「Mac上にプレイブックを展開して普通に ansible-playbook 実行」で充分です。

Ansible Galaxy

コミュニティによる Roles の共有サービスとして Ansible Galaxy というものがあるのですが...試用/研究目的以外で活用するのは難しい印象です。

所詮は他人の組んだ俺流Roles なので、痒いところには絶妙に手が届かず。
あくまで参考程度に留めておくのがよいでしょう。

Ansible Tower

Ansible Tower なんていう運用ツールもありますが...Mac の構成管理ごときにこんなものを持ち出すのはちょっと大げさですね。

チームで多数のサーバを管理するような場合には役に立つかもしれません。

Ansible Pull

Ansible は通常、処理対象のリモート・ホストに SSH経由で作用します。(Push式)

ansible-pullコマンドを使うとこの流れを逆向き (Pull式) に、すなわち、リポジトリから手元にプレイブックを clone してきてそのまま自分自身に適用することができます。

自律的にノード数が増減する分散システムのセットアップには効果を発揮しそうですが、Mac のセットアップは手元のプレイブックをいじる&適用の繰り返しなので、敢えて Ansible Pull を使わなくても普通に ansible-playbook で充分だと思います。

なお、localhost にプレイブックを適用する場合は Ansible に その旨を指図する 必要があります。

Ansible でインストールするもの/しないもの

これは Ansible というよりもソフトウェア・インストール用の Module の特性なのですが、なんでも Ansible を使えば幸せというわけではありません。

Ansible で Mac にソフトウェアをインストールする場合は基本的に homebrew/homebrew_cask module を使うのがよいのですが、これら Homebrew系と相性の悪いソフトウェアがあります。

実際に Homebrew で色々とインストールしてみると、以下のような不便さを感じることになるでしょう。

Formula がいつまでも古い

Formula とは、Homebrew におけるソフトウェアのインストール・レシピです。

バージョンアップする際はこの Formula を更新してリポジトリに Push する必要があるのですが、メンテナによってはなかなか Formula が最新化されないことがあります。

例えば android-sdk は 2017年4月現在 ver. 24.4.1 のまま更新が止まっています。
(Android Studio公式サイト は ver. 25.2.3 ...あれ、こっちもちょっと古い?)

Macエコシステムはなにかと最新環境であることを要求するので、場合によっては手動管理を採用したほうがよいでしょう。

ただし Homebrew公式リポジトリに自分で Formula の Pull Request をねじ込む、または オレオレHomebrewリポジトリ を維持管理する気概があるならばその限りではありません。

ソフトウェア自体の自動アップデートを使うに使えない

ソフトウェアが自前で用意している自動アップデート機能を使ってしまうと、Homebrew が認識しているバージョンと齟齬が発生してしまい、以降の運用に支障をきたす恐れがあります。

かといって一度ソフトウェアを終了してからプレイブックを実行してバージョンアップ、などという運用も二度手間感が半端ないでしょう。
(しかも Cask はバージョンアップ操作を 正式にサポートしていない ...)

頻繁に起動する GUIアプリケーションは特に煩わしいので、これも手動管理とすることをオススメします。

一方で Java など pkg形式で配布されているソフトウェアは、総じて構成が独自でバージョン管理がメンドクサイので、自動更新を無効化したうえで Ansible (homebrew_cask) でインストールするのが合理的なケースもありえます。

ソフトウェアの配布形態とバージョンアップ方針を鑑みて、個別に決定するのがよいでしょう。

ちなみに Java の自動更新の無効化は osx_defaults module でいけます。 (参考)

サーバとは違う

バージョン戦略

上でも触れましたが、基本的に Mac で利用するソフトウェアは最新バージョンに追随しておくとトラブルが少ないです。

Unix系サーバの運用経験のある方はバージョンを固定したくなる誘惑に駆られることと思いますが、Mac でそれをやるのは修羅の道です。

サーバ脳から Mac脳に頭を切り替えて、最新バージョンをインストールするようにしましょう。

ペットと家畜

何年か前から仮想化/クラウド界隈で「Pets vs. Cattle」なるコンセプトが聞かれるようになりました。

Pets はペットであり、個々のマシンを長く大事に手間暇かけて運用管理します。
構成変更の際もマシンが壊れないように細心の注意を払います。

一方で Cattle は家畜。
ぶっ壊れたり古くなったらガンガン捨てて、ガンガン新規構築&投入します。
構成変更なんていう、しち面倒くさい概念はありません。

バージョン戦略と同じく、サーバ側の方々はつい今までの観念に従って家畜としてプレイブックを組みたくなるでしょうが、Mac はペットです。
大事に育てましょう。

プレイブックを組む

前置きは以上です。
それではプレイブックの作り方を見ていきましょう。

見本のプレイブックを GitHub に置いておきましたので、これを絡めながら説明していきます。

kkitta/ansible-example - GitHub

全体のディレクトリ構成

上記「構成要素」で取り挙げた要素をふんだんに使った構成にするとよいでしょう。
構成管理対象が Mac 1台程度ではやり過ぎ感も否めませんが、定型として持っておけば様々なケースに対応できるのでオススメです。

tree
./<ANSIBLE-PROJECT>
├── ansible.cfg
│
├── inventory/
│   ├── <INVENTORY>
│   ├── ...
│   │
│   ├── group_vars/
│   │   ├── <GROUP>/
│   │   │   ├── <VARIABLES>.yml
│   │   │   └── ...
│   │   └── .../
│   │
│   └── host_vars/
│       ├── <HOST>/
│       │   ├── <VARIABLES>.yml
│       │   └── ...
│       └── .../
│
├── playbooks/
│   ├── <PLAYBOOK>.yml
│   └── ...
│
└── roles/
    ├── <ROLE>/
    │   ├── files/
    │   ├── handlers/
    │   ├── meta/
    │   ├── tasks/
    │   ├── templates/
    │   └── vars/
    └── .../

Roles配下以外は規約的強制力を有するものではありませんが、この構成であればファイルの役割ごとにわかりやすく仕分けできます。

なお、Roles には本来 defaults (Role Default Variables) ディレクトリが存在するのですが、だいたい vars で充分なので省略しています。

以下、各要素を個別に見ていきます。

Configuration file

設定ファイルはプロジェクト直下に ansible.cfg というファイル名で配置します。

tree
./mac/
└── ansible.cfg

まずは example をそのままダウンロードして、必要に応じて順次調整していきましょう。

command
curl -O https://raw.githubusercontent.com/ansible/ansible/devel/examples/ansible.cfg

Inventory

inventoryディレクトリに配置します。

Mac の場合は以下のように 1枚だけ用意すれば充分でしょう。

tree
./mac/
└── inventory/
    └── default

中身は適当に local groupに localhost とでも登録しておきます。
(local group の設定は後ほど group_vars で見ていきます)

mac/inventory/default
[local]
localhost

ansible.cfg にて Inventoryファイルの格納ディレクトリを指定します。

mac/ansible.cfg
[defaults]
inventory = ./inventory

なお、それなりに規模の大きなシステムを運用する場合は任意の系統ごとにファイルを分割することもできます。

例えばデプロイメント環境で分割して、

tree
./env/
└── inventory/
    ├── development/
    │   └── default
    │
    ├── production/
    │   └── default
    │
    └── staging/
        └── default

ansible-playbookコマンドの -iオプションを指定することで、環境ごとにプレイブックを適用できます。
(Inventoryファイルが格納されているディレクトリを指定します)

command
# 開発環境に適用
ansible-playbook -i ./inventory/development ./playbooks/<PLAYBOOK>.yml

# 本番環境に適用
ansible-playbook -i ./inventory/production ./playbooks/<PLAYBOOK>.yml

Group/Host variables

Group/Host に割り当てる Variables をそれぞれ inventory/group_vars, inventory/host_varsディレクトリに配置します。

Inventory で定義した Group/Host と同名のディレクトリを作成し、その配下に複数のファイルを配置することができます。
(ファイル名は自由です)

tree
./mac/
└── inventory/
    └── group_vars/
        └── local/
            └── ansible.yml

この例では、プレイブックをローカル適用するための Variable である ansible_connectionlocal group に割り当てています。

mac/inventory/group_vars/local/ansible.yml
ansible_connection: 'local'

これは構成管理そのものに使う Variable ではなく Ansible の振る舞いを制御する ためのものなので、わかりやすいように ansible.yml というファイル名で定義しています。

また、全ての Host に適用したい場合は all というメタな Group を利用できます。
使い道としては、

  • 複数系統の Python を同時にインストールしている場合に ansible_python_interpreter でインタプリタのパスを指定する。
  • Inventory をデプロイメント環境で分割した例において、env: 'development' などと環境名を設定する。

などといった活用が考えられます。

どの Variables を Group variables として定義すべきかについては Group のまとめ方や Roles の組み方にもよりますが、Variables の定義箇所と利用箇所が遠いと見通しが悪くなるため、基本的に Roles に任せられるものは Roles の Variables として定義したほうがよいでしょう。

具体的には、環境名 env を Group variables として定義した例において以下の Role を考えた場合、

tree
./env/
└── roles/
    └── site/
        ├── tasks/
        │   └── main.yml
        │
        └── vars/
            ├── development.yml
            ├── production.yml
            └── staging.yml

環境別の Variablesセットを varsディレクトリに別々のファイルとして定義し分けたうえで、Variable env によって対象環境の Variablesセットを読み込むことができます。
(include_vars module で読み込んだ Variables はプレイブック全体からアクセスできてしまうため、意図しない上書きを避けるために割り当て名を name でユニークに指定したほうがよいでしょう)

env/roles/site/tasks/main.yml
- include_vars:
    file: '{{ env }}.yml'
    name: 'site'

なお、今回は localhost に対する Host variables は特に必要ないので host_varsディレクトリは空です。
もし Variables を割り当てたい場合は inventory/host_vars/localhost/<何か>.yml というファイルを作成すればよいでしょう。

例えばサーバ用のプレイブックでは、各マシンのスペックに応じたアプリケーション・プロセス数の調整や特定ハードウェア・ベンダ専用のセットアップなど、マシン筐体個々の差異に応じた構成管理に利用できます。

Playbooks

playbooksディレクトリに配置します。

tree
./mac/
└── playbooks/
    └── my-mac.yml

ここでは my-mac.yml playbook を作成し、local group に対して basedev-tools の 2つの Roles を紐付けています。

mac/playbooks/my-mac.yml
- hosts: 'local'
  roles:
    - 'base'
    - 'dev-tools'

Playbook名にはその Group が提供する役務を包括的に表す名称を、Role名には独立して付け替え可能な機能名を付けるとよいでしょう。
この例でいうと、もし「おれはエンジニアをやめるぞ! ジョジョーーッ!! (๑•́‧̫•̀๑)」となった場合でも dev-tools role を取り外すだけで OK、という具合です。

また、Roles は階層化できるので、大規模なシステムを扱う場合は層別に整理してもよいかもしれません。
(詳細は次の Roles のセクションで解説します)

Roles

rolesディレクトリ配下を Roles探索対象とするように ansible.cfg を変更します。

mac/ansible.cfg
[defaults]
roles_path = ./roles

roles配下に Role ごとにディレクトリを作成していきます。

各Role は以下のディレクトリから構成されます。
規約に沿ってファイルを配置すれば自動でロードされるほか、include系の Module を実行する際に適切なディレクトリから探索されるようになります。

tasks

個別の処理内容を格納するディレクトリです。

main.yml に記述しますが、処理が多数に及ぶ場合は固まりごとにファイルに小分けするとよいでしょう。
小分けにしたファイルは include module で読み込むことができます。

例えば dev-tools role では以下の固まりに分割してみました。

ファイル 処理対象
tools.yml 雑多なコマンドライン・ツール
dnsmasq.yml Dnsmasq

これらを main.yml で順に読み込みます。

mac/roles/dev-tools/tasks/main.yml
- include: 'tools.yml'
- include: 'dnsmasq.yml'

また、Module への引数の与え方は以下2通りのスタイルいずれでも記述することができます。

  • key=valueスタイル

    - name: 'Dock tilesize'
      osx_defaults: 'domain=com.apple.dock key=tilesize type=float value=60 state=present'
    
  • YAMLスタイル

    - name: 'Dock tilesize'
      osx_defaults:
        domain: 'com.apple.dock'
        key: 'tilesize'
        type: 'float'
        value: 60
        state: 'present'
    

YAMLスタイルのほうが複雑な引数でも記述しやすく読みやすい気がするので、特別な理由がなければ YAMLスタイルを採用するのがよいでしょう。

ちなみに shell などのコマンド系Modules は引数が特殊で、実行するコマンドを自由書式で記述したうえで、他の引数を与えたい場合は argsパラメータを使用します。

mac/roles/dev-tools/handlers/main.yml
- name: 'reload dnsmasq'
  shell: |
    SERVICE='kkitta.dnsmasq'

    launchctl list ${SERVICE} \
      && launchctl unload ./${SERVICE}.plist

    launchctl load ./${SERVICE}.plist
  args:
    chdir: '/Library/LaunchDaemons'
  become: true

files

copy module で配備する元ファイルや script module で実行するスクリプトを格納します。

script にはシェルだけでなく Python, Ruby など任意のスクリプト言語 (要Shebang) や、さらにはバイナリ・コマンドも利用できます。
「可搬性に優れた Go製のバイナリをプレイブックに同梱しておく」といったパターンが今後は見られるようになるかもしれません。

templates

template module で使うテンプレートを格納します。

テンプレート・エンジンは Jinja2独自拡張 を施したものが使われています。

テンプレートを作成する際は、 素の Jinja2 のドキュメント も合わせて確認するとよいでしょう。

なお、テンプレーティングを使えるのはテンプレート・ファイル内に限りません。
「Group/Host variables」で例示したように、Tasksなどでも利用できます。

env/roles/site/tasks/main.yml
- include_vars:
    file: '{{ env }}.yml'
    name: 'site'

handlers

Tasks と同じように処理を記述しますが、ここには特定の処理の実行結果に応じてトリガされるものを格納します。

トリガとしたい処理に notify: '<Handler名>' というパラメータを付与することで、当該処理の実行結果が changed となった場合に指定した Handler が実行されます。
(変更なしの場合は Handler は実行されません)

例えばプレイブック見本では、Dock を再起動するコマンドを Handler として定義したうえで、

mac/roles/base/handlers/main.yml
- name: 'restart dock'
  command: |
    killall 'Dock'

Dock のタイル・サイズが変更された場合にのみ当該Handler が発動するようにしています。

mac/roles/base/tasks/main.yml
- name: 'Dock tilesize'
  osx_defaults:
    domain: 'com.apple.dock'
    key: 'tilesize'
    type: 'float'
    value: 60
    state: 'present'
  notify: 'restart dock'  # « タイル・サイズが変更されたら `restart dock` handler 発動

Handlers を活用することで実行する必要のない処理はスキップされるようになり、プレイブック適用にかかる時間も短縮されて幸せです。

ちなみに通常 notify はキューイングされ、Tasks の実行がすべて完了してから最後にまとめて Handlers が実行されます。
途中で Handlers を実行したい場合は、 meta: 'flush_handlers' という Action を Tasks に差し挟むことでそのタイミングでキューをフラッシュすることができます。

vars

当該Role に紐づく Variables を格納します。

main.yml は自動的にロードされますが、他のファイルは include_vars module で明示的に読み込む必要があります。

なお、Role variables はデフォルトではグローバル変数のように扱われ、他の Role からも参照できてしまいます。
スコープが広いと色々と面倒なので参照範囲を Role内に絞ることもできますが、

env/ansible.cfg
private_role_vars = yes

ただし、この設定が効くのは main.yml のみです。
include_vars module で読み込んだ Variables はこの設定があっても普通に Role外から参照されてしまいますので、あくまで気休め程度として、罠に嵌りにくい明確な名前空間を真面目に設計したほうがよいでしょう。

meta

Role間の依存関係を定義します。

Mac の規模では依存を張ることは少ないと思いますが、例えば以下のように、アプリのビルド機能を中核Role として、そこからデスクトップRole と CI Role が派生する層別モデルを考えてみます。

レイヤ 役務
apps Role の本質的な提供機能 desktop, ci
engines 機能の提供形態によらない中核部分 build
tree
./layered/
├── playbooks/
│   ├── ci.yml
│   └── desktop.yml
│
└── roles/
    ├── apps/
    │   ├── ci/
    │   └── desktop/
    │
    └── engines/
        └── build/

engines/build role がビルド機能の中核を提供するので apps/* role各々から engines/build role に依存を張る、という使い方になります。
依存を張る際には併せて Variables を指定することもできます。

layered/roles/apps/ci/meta/main.yml
dependencies:
  - role: 'engines/build'
    headless: true
layered/roles/apps/desktop/meta/main.yml
dependencies:
  - role: 'engines/build'
    headless: false

この例では GUI環境の有無を表す headless という Variable を指定してみました。
engines/build role では当該Variable の値に応じて処理を切り替えています。

layered/roles/engines/build/tasks/main.yml
- block:  # ヘッドレス環境向け
  - ...
  when: 'headless | bool'

- block:  # GUI環境向け
  - ...
  when: 'not (headless | bool)'

その他のトピック

他、説明しきれなかった雑多なトピックをまとめて紹介します。

Become

別アカウントにスイッチして処理を実行したい場合は become: true を付与します。

スイッチするアカウントは become_user で指定します。
(省略した場合は root)

Well-knownポートを待ち受けるプロセスの操作などに root権限が必要となります。

mac/roles/dev-tools/handlers/main.yml
- name: 'reload dnsmasq'
  shell: |
    SERVICE='kkitta.dnsmasq'

    launchctl list ${SERVICE} \
      && launchctl unload ./${SERVICE}.plist

    launchctl load ./${SERVICE}.plist
  args:
    chdir: '/Library/LaunchDaemons'
  become: true

Blocks とエラーの扱い

block で括れば複数の処理をまとめてグループとして扱ったりエラーを拾ったりできます。

mac/roles/dev-tools/tasks/dnsmasq.yml
- block:
  - name: 'resolverディレクトリ 作成'
    ...
  - name: 'resolv.conf 配備'
    ...
  - name: 'Dnsmasq plist 配備'
    ...
  become: true

この例では blockbecome: true を併記しており、Block内部の 3つの処理は root権限で実行されます。

block と併せて rescuealways を記述することでエラー・ハンドリング (一般的なプログラミング言語における try-catch-finally) を実現できますが、あまり出番はないように思います。
通常の構成管理のオペレーションにおいてはエラーが発生したら普通に失敗して OK なケースが多い気がするので、敢えて使い所を探すとすればリカバリの時間的余裕がないケース、例えばアプリケーションのデプロイなどでは役に立つかもしれません。
(普通に失敗して OK = 落ち着いて原因特定&修復すればそれでよし。というか、だいたい予期せぬ事象なので自動リカバリしきれない)

なお、エラーを無視 したり エラーと判断する条件を変更 したりすることもできます。
次で説明する changedステータスの制御と併せて、お好みで活用してください。

冪等性を備えているアピール

この手の構成管理ツールの特徴のひとつに、プレイブックを何度繰り返し実行しても対象ホストがただ一つの状態に収束する「冪等性」なる概念があります。

ある Action を実行した結果、対象マシンの状態が変化すればステータスは changed となる一方、すでに目的の状態であるならば状態は変化せず ok となり、プレイブック実行結果がすべて ok であれば当該プレイブックは冪等性を備えているといえるわけです。

console
$ ansible-playbook ./playbooks/my-mac.yml
SUDO password:

PLAY [local] *******************************************************************

TASK [setup] *******************************************************************
ok: [localhost]

TASK [base : Dock tilesize] ****************************************************
ok: [localhost]

...

TASK [dev-tools : Dnsmasq plist 配備] ********************************************
ok: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=9    changed=0    unreachable=0    failed=0

Mac のように繰り返し適用するプレイブックは冪等性の具備が必須となるでしょう。

ここで Ansible の公式Modules であればだいたい冪等性を考慮した挙動となるのですが、 shell などのコマンド系Modules は処理内容によらず必ず changed となるので注意が必要です。
もちろんコマンドによっては冪等となる場合もあるので「すべて ok じゃないから冪等じゃない」というわけではありませんが、冪等かどうかの判断にいちいちコマンドを確認して判断するのもダルいものです。

なので、先述のエラー制御と同じく changed とする条件も設定 できるので、コマンド系Modules を使う際はできる限り changedステータスを制御して冪等性を主張することをオススメします。
(もちろん、そもそも冪等とならないコマンドの実行は NG です)

Mac でしか使わなそうな Modules

以下の Modules は多分 Mac でしか使いません。
せっかくなので積極的に使っていきましょう。

Module 機能 代替するコマンド
homebrew Homebrew のパッケージをインストール brew install
homebrew_tap 任意の Homebrewリポジトリを Tap brew tap
homebrew_cask Homebrew Cask のパッケージをインストール brew cask install
osx_default macOSシステムやアプリの設定を変更 defaults write
osx_say 喋る say

サービス管理に必須の launchctlコマンドと、Mac頻出の plistファイルの操作に役立つ plutil, /usr/libexec/PlistBuddyコマンドに代わる Modules が存在しないのが惜しいところです。

Serverspec

ご存知 Serverspec を併用すべきかどうか?

Ansible を個人や組織内で使う分には別に要らないかなぁと感じています。

TDD なんかも含めてテストの価値は大きく以下3つくらいだと思っていて、

  1. 仕様の明確化の支援
  2. 品質の保証
  3. 変更に対する心理的障壁の引き下げ

つまり、結局どう動くかわからんコードよりもテスト項目がそれすなわち満たすべき仕様であり、こんだけテストしたんだから品質バッチリだよと言うための証拠集めであり、とりあえずテスト全部パスしてるんで勇気出して Pull Request 送ってみようかな、という具合ですね。

このうち 1番/2番に対しては、コード作成よりもテスト作成/実施のほうが容易でないといけません。
テストのコストがコード作成を上回るなら、テストの時間をコード作成に当てたほうが合理的です。

プログラミング領域においては「手続き的なコードを宣言的なテストでカバーする = 別なるパラダイムで補う」という図式によりコスト差異が生み出されている一方で、Ansible も Serverspec もどちらも「マシンのあるべき状態を宣言的に記述するもの = パラダイムが同一である」という点が決定的に異なっているのではなかろうかと思います。

この相違により上記 1番/2番の価値が薄まり、3番も単一組織内に閉じた運用ならばさほど問題にはならないと思われるため、敢えて Serverspec を導入する理由がない、という理屈になります。

ただし、Continuous Delivery的な文脈でプレイブックを常時正常に保つ、最低限のエビデンスという意味での自動テストには大いに賛成でございます。
(プレイブック適用成功をもってよしとするか、Serverspec でがっつりテストを組むかは何を最低限とするかによりけりですね)

Vault を使うか?

Vault という暗号化の仕組みが用意されていますが...暗号化されているとはいえ、敢えて人目に晒す必要はないのでパブリックなリポジトリでは使わないほうがよいでしょう。

代わりに別のプライベートなストアに格納したうえで、プレイブック適用時に都度引っ張ってきて環境変数か何かで注入することをオススメします。

Vault の使い所としては、社内リポジトリなど基本的に信頼できるネットワーク内ではありつつも、しかしある程度のアクセス制御は施したいような環境下に限定して利用するのが妥当かなと思います。

最後に

プレイブックの組み方に正解なんてないよ、ひとりひとりが、オンリーワン。 (ง ´͈౪`͈)ว

他に知りたいトピックがあればコメント欄でお知らせください。