Mac App StoreでリリースされているMac用アプリケーションをAnsibleで管理しよう

AnsibleでMac App Storeのアプリケーションを管理する方法を紹介します。
2020.02.18

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは。サービスグループの武田です。

ここ最近Ansibleしか触ってません。いや嘘です。

Ansibleで管理するMacのアプリケーションがHomebrewなどで完結する場合はいいのですが、やはりMac App Store(以下、App Store)経由でインストールしなければいけないものも出てきます。代表格がXcodeでしょうか。というわけで、今回はAnsibleを使ってApp Storeのアプリケーションを管理する方法を調べてみました。

事前の準備

プレイブックの中でmasというCLIツールを使用しているのですが、App Storeへのサインインがコマンド経由ではできませんでした(OSのバージョン依存?)。そのため、事前にApp Storeを開き、サインインをしておく必要があります。

環境

次の環境で検証しました。

$ sw_vers
ProductName:	Mac OS X
ProductVersion:	10.15.3
BuildVersion:	19D76

$ ansible-playbook --version | grep -E 'playbook|version'
ansible-playbook 2.9.3
  executable location = /usr/local/bin/ansible-playbook
  python version = 3.8.1 (default, Dec 27 2019, 18:06:00) [Clang 11.0.0 (clang-1100.0.33.16)]

またディレクトリの構成は次のようになっています。

$ tree
.
├── ansible.cfg
├── hosts
├── roles
│   └── mas
│       ├── tasks
│       │   └── main.yml
│       └── vars
│           └── main.yml
└── site.yml

4 directories, 5 files

周辺ファイル

先に本質ではない部分のファイルをざっと紹介します。

ansible.cfgは全体の設定をするファイルです。今回はinterpreter_pythonのみ指定しています。

ansible.cfg

[defaults]
interpreter_python=/usr/bin/python3

インベントリファイルです。名前は任意ですが、今回はhostsとしました。

hosts

[localhost]
127.0.0.1

プレイブックの本体です。これも名前は任意ですが、今回はsite.ymlとしました。アプリケーションの管理はロールとして切り出したため、rolesで指定しています。

site.yml

---
- name: ansible
  hosts: localhost
  connection: local
  become: no
  gather_facts: no
  roles:
    - mas

App Storeアプリケーションをインストールするプレイブック

それでは本題のプレイブックです。まずApp Storeでインストールしたいアプリケーションのリストは変数ファイルに分けています(後述)。ところで、App Storeの管理ができるモジュールはAnsibleでは提供されていません。そのためmasコマンドを使用することにします。CLIでApp Storeのアプリケーションを操作できるもので、Homebrewでインストールできます(masのインストールは別ロールに分けてもOK)。

プレイブックを作成する際に、基本的には提供されているモジュールが冪等性を確保してくれますが、command/shellモジュールを使用する場合は自分で確保しなければいけません。そのためmas installを実行する前に、name: fetch listでインストール済みのアプリケーション一覧を取得し、これを使用して実際にコマンドを実行するかの条件を記述しています。whenの条件がやや複雑ですが、取得した一覧に管理対象のアプリケーションがあるかを探索し、なければコマンドを実行するという条件です。selectはジェネレータを返すため| listでリストにし、その長さをチェックしています。

roles/awscmasli/tasks/main.yml

---
- block:
  - name: install mas cli
    homebrew:
      name: mas
      state: present

  - name: fetch list
    command: mas list
    register: installed_list
    check_mode: no
    changed_when: no

  - name: install App Store applications
    command: "mas install {{ item.id }}"
    when: "installed_list.stdout_lines | select('search', item.id) | list | length == 0"
    loop: "{{ apps }}"

  tags:
    - mas

インストールしたいアプリケーションは変数ファイルに記述します。idだけでも動きますが、番号だけではどれがどれだかわからなくなってしまうためnameも書くようにしています。ちなみにアプリケーションのIDはApp Storeをブラウザで開き、対象アプリケーションのページにいくとURLでわかります。

roles/mas/vars/main.yml

---
apps:
  - { name: "Xcode", id: "497799835" }
  - { name: "Keynote", id: "409183694" }
  - { name: "Numbers", id: "409203825" }
  - { name: "Pages", id: "409201541" }

ちなみに、installed_listは次のような内容になっています。

"installed_list": {
    "changed": false,
    "cmd": [
        "mas",
        "list"
    ],
    "delta": "0:00:00.042378",
    "end": "2020-02-18 12:00:00.000000",
    "failed": false,
    "rc": 0,
    "start": "2020-02-18 12:00:00.000000",
    "stderr": "",
    "stderr_lines": [],
    "stdout": "409183694 Keynote (9.2.1)\n497799835 Xcode (11.3.1)\n409203825 Numbers (6.2.1)",
    "stdout_lines": [
        "409183694 Keynote (9.2.1)",
        "497799835 Xcode (11.3.1)",
        "409203825 Numbers (6.2.1)"
    ]
}

実行してみる

書いたプレイブックを実行すると次のようになりました。

$ ansible-playbook -i hosts site.yml

PLAY [ansible] *************************************************************************************

TASK [mas : install mas cli] ***********************************************************************
ok: [127.0.0.1]

TASK [mas : fetch list] ****************************************************************************
ok: [127.0.0.1]

TASK [mas : install App Store applications] ********************************************************
skipping: [127.0.0.1] => (item={'name': 'Xcode', 'id': 497799835})
skipping: [127.0.0.1] => (item={'name': 'Keynote', 'id': 409183694})
skipping: [127.0.0.1] => (item={'name': 'Numbers', 'id': 409203825})
changed: [127.0.0.1] => (item={'name': 'Pages', 'id': 409201541})

PLAY RECAP *****************************************************************************************
127.0.0.1                  : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

未インストールのアプリケーションはインストールされ、インストール済みのものはちゃんとskipされていますね!(whenを消すとすべてchangedになるので試してみてください)

まとめ

AnsibleでApp Storeのアプリケーションを管理する方法について紹介しました。アプリケーションのインストールには時間がかかるので、プレイブックを実行したらコーヒーでも飲んでゆっくり待ちましょう。