Ansible Azure リソース管理テクニック

## 概要

[Ansible] による [Azure Resource Manager (ARM)][arm] リソース管理において、関連リソースの参照解決を伴う複雑なタスクを楽に作成するテクニックを紹介します。次のサービスやコンポーネントを活用します。

- [Azure Resource Explorer](https://resources.azure.com)
- Ansible ARM 汎用モジュール: [azure_rm_resource], [azure_rm_resource_info]
- `azure_rids` フィルタプラグイン: [yaegashi.ansibleplugins](https://galaxy.ansible.com/yaegashi/azureplugins)

この記事で紹介する手法はあまり一般的なものではありません。基礎については [Azure 上の Ansible のドキュメント](https://docs.microsoft.com/ja-jp/azure/ansible/)に丁寧な説明があるのでそちらを参照してください。

## Azure Resource Explorer

[Azure Resource Explorer](https://resources.azure.com) は ARM リソースの閲覧や操作ができる、開発者向けの Web アプリです。サインインすると、Azure サブスクリプション・リソースグループのツリー構造に含まれる様々な ARM リソースを探索できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61759/5b46916c-c643-edc6-047e-20d50039bbd5.png)

この Web アプリから各リソースに対して REST API が発行できて、さらにドキュメントも埋め込まれているので、眺めているだけで ARM にはどんな API があって何ができるかがだいたい把握できます。

そして Azure Resource Explorer にはなんと Ansible のサポートが組み込まれています。各リソースについている Ansible のタブを選択すると、 Ansible の ARM 汎用モジュールである [azure_rm_resource] と azure_rm_resource_facts (Ansible 2.9 からは [azure_rm_resource_info][azure_rm_resource_info]) タスクのテンプレートが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61759/720bc2cb-0bae-40de-fd5c-127cb41cb0dc.png)

これによりプリミティブな ARM REST API を発行する Ansible タスクが簡単に書けるようになります。

ただし、このような REST API の発行には冪等性がないので、通常のリソース管理には使うべきではありません。たとえば Azure の DNS ゾーンを作りたいのであれば、画像にある `CREATE dnsZones` タスクではなく、まずは素直に [azure_rm_dnszone] モジュールを使うべきでしょう。 Ansible のモジュールがサポートしていない特殊な操作が必要なときに限って利用するのがよいと思います。

## Ansible ARM 汎用モジュールとリソース ID の活用

[Ansible の ARM モジュール](https://docs.ansible.com/ansible/latest/modules/list_of_cloud_modules.html#azure)はとても充実しており、様々な種類の [ARM] リソースを操作できますが、各モジュールの操作対象となるリソースの指定に苦労することはないでしょうか。

たとえば VM の NIC に関連づけた NSG のルールをいじることはよくあると思いますが、その NSG の所在と名前が不明なことが多く、結果として「リソースグループ X に属する名前 Y の VM にくっついている最初の NIC にくっついている NSG のルールに OpenVPN のポート (1194) の受信許可がなければ作る」のような、関連リソースの参照を解決していく複雑なタスクを作ることになりがちです。

ARM ではリソースの所在と名前は次の形式の「リソース ID」で参照することになっています。 VM に関連づけられた NIC や NSG もこの形式で記録されています。

```text
/subscriptions/{サブスクリプションID}/resourceGroups/{リソースグループ名}/providers/{プロバイダ名}/{リソースタイプ名}/{リソース名}/{サブリソースタイプ名1}/{サブリソース名1}/...
```

ARM 汎用モジュール [azure_rm_resource] および [azure_rm_resource_info] では、操作対象リソースの指定において `url` パラメータでこのリソース ID が利用できます。これと Azure Resource Explorer を活用すれば、関連リソースの参照を解決するタスクの記述が簡単にできるようになります。

次は前述の VM から関連をたどって所在を得た NSG のルールを更新するタスクを記述したプレイブックです。

```yaml:playbook.yml
- hosts: localhost
  connection: local
  gather_facts: no
  vars:
    resource_group: GROUP
    vm_name: VM
    nsg_rules:
      - name: AllowOpenVPN
        priority: 200
        destination_port_range: 1194
        direction: Inbound
        access: Allow
  tasks:
    - name: (1) azure_rids フィルタプラグイン読み込み
      include_role:
        role: yaegashi.azureplugins
    - name: (2) VM リソース情報取得
      azure_rm_resource_info:
        resource_group: "{{resource_group}}"
        provider: Compute
        resource_type: virtualMachines
        resource_name: "{{vm_name}}"
      register: r
    - name: (3) NIC リソース情報取得
      azure_rm_resource_info:
        url: "{{r.response[0].properties.networkProfile.networkInterfaces[0].id}}"
      register: r
    - name: (4) NSG ルール追加
      azure_rm_securitygroup:
        resource_group: "{{rid.resource_group}}"
        name: "{{rid.name}}"
        rules: "{{nsg_rules}}"
      vars:
        rid: "{{r.response[0].properties.networkSecurityGroup.id | azure_rids}}"
```

タスクの説明

- タスク (1) では yaegashi.azureplugins ロールを読みこんで `azure_rids` フィルタプラグインを利用可能にしています (後述)。
- タスク (2) ではリソースグループ・プロバイダ・リソースタイプ・リソース名の指定により VM リソースの情報を取得しています。
- タスク (3) では VM 情報に埋め込まれた NIC のリソース ID を使って NIC リソースの情報を取得しています。
- タスク (4) では NSG のルールを冪等操作するために [azure_rm_securitygroup](https://docs.ansible.com/ansible/latest/modules/azure_rm_securitygroup_module.html) を利用していますが、このモジュールはリソース ID を受け付けないので、リソース ID からリソースグループとリソース名を取り出すために `azure_rids` フィルタを使用しています。

ポイント

- [azure_rm_resource_info] は Ansible 2.9 で azure_rm_resource_facts から名前が変わったモジュールです。 Ansible 2.9 より前のバージョンでは azure_rm_resource_facts を使用してください。
- [azure_rm_resource_info] の実行結果を変数 `r` に register した場合 REST API の結果は `r.response[0]` に入っています。この中のオブジェクト構造は [Azure Resource Explorer](https://resources.azure.com) を参照して調べます。<br/>次の画像は VM リソースの中身を見たものです。「VM にくっついている最初の NIC のリソース ID」は `properties.networkProfile.networkInterfaces[0].id` で参照できることがわかります。
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61759/a218b8a0-2a28-a64e-54b1-53e80b2c648d.png)
- タスク (4) のように `vars` を使うと一時的な変数の定義ができます。わざわざ `set_fact` タスクを書く必要がなくなるので便利です。ただし上位のプレイブックから呼び出される場合はそこで同じ名前の変数が定義されると上書きされてしまうので注意が必要です。

## Ansible azure_rids フィルタプラグイン

Ansible ARM モジュールが利用している [Azure SDK for Python](https://docs.microsoft.com/ja-jp/azure/python/python-sdk-azure-overview) には [parse_resource_id](https://docs.microsoft.com/en-us/python/api/msrestazure/msrestazure.tools?view=azure-python#parse-resource-id-rid-) という関数があり、これを使うとリソース ID に含まれるリソースグループ名やリソース名といった要素を抽出することができます。この関数を Jinja2 のフィルタとして使えるようにしたのが `azure_rids` です。

このフィルタプラグインは [Ansible Galaxy の yaegashi.azureplugins ロール](https://galaxy.ansible.com/yaegashi/azureplugins)に登録していますので、これをインストールして最初に呼び出すだけで `azure_rids` フィルタが使えるようになります。

次のプレイブックは、様々なリソース ID に対する `azure_rids` の動作をテストするものです。

```yaml:playbook.yml
- hosts: localhost
  connection: local
  gather_facts: no
  vars:
    resource_ids:
      - /subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/RG1/providers/Microsoft.Compute/virtualMachines/VM1
      - /subscriptions/22222222-2222-2222-2222-222222222222/resourceGroups/RG2/providers/Microsoft.Network/virtualNetworks/VNET1/subnets/SUBNET1
      - /subscriptions/33333333-3333-3333-3333-333333333333/resourceGroups/RG3/providers/Microsoft.Compute/galleries/SIG1/images/DEF1/versions/1.2.3
  tasks:
    - name: (1) azure_rids フィルタプラグイン読み込み
      include_role:
        role: yaegashi.azureplugins
    - name: (2) azure_rids テスト
      debug:
        msg: "{{item | azure_rids}}"
      loop: "{{resource_ids}}"
```

実行結果は次のとおりです。仮想ネットワークのサブネットや共有イメージギャラリーのバージョンのような、多段階のサブリソースを持つリソース ID でも、各サブリソースのタイプや名前が取り出せることがわかります。

```
PLAY [localhost] *************************************************************************************

TASK [(1) azure_rids フィルタプラグイン読み込み] ******************************************************************

TASK [(2) azure_rids テスト] ****************************************************************************
ok: [localhost] => (item=/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/RG1/providers/Microsoft.Compute/virtualMachines/VM1) => 
  msg:
    children: ''
    name: VM1
    namespace: Microsoft.Compute
    resource_group: RG1
    resource_name: VM1
    resource_namespace: Microsoft.Compute
    resource_parent: ''
    resource_type: virtualMachines
    subscription: 11111111-1111-1111-1111-111111111111
    type: virtualMachines
ok: [localhost] => (item=/subscriptions/22222222-2222-2222-2222-222222222222/resourceGroups/RG2/providers/Microsoft.Network/virtualNetworks/VNET1/subnets/SUBNET1) => 
  msg:
    child_name_1: SUBNET1
    child_parent_1: virtualNetworks/VNET1/
    child_type_1: subnets
    children: /subnets/SUBNET1
    last_child_num: 1
    name: VNET1
    namespace: Microsoft.Network
    resource_group: RG2
    resource_name: SUBNET1
    resource_namespace: Microsoft.Network
    resource_parent: virtualNetworks/VNET1/
    resource_type: subnets
    subscription: 22222222-2222-2222-2222-222222222222
    type: virtualNetworks
ok: [localhost] => (item=/subscriptions/33333333-3333-3333-3333-333333333333/resourceGroups/RG3/providers/Microsoft.Compute/galleries/SIG1/images/DEF1/versions/1.2.3) => 
  msg:
    child_name_1: DEF1
    child_name_2: 1.2.3
    child_parent_1: galleries/SIG1/
    child_parent_2: galleries/SIG1/images/DEF1/
    child_type_1: images
    child_type_2: versions
    children: /images/DEF1/versions/1.2.3
    last_child_num: 2
    name: SIG1
    namespace: Microsoft.Compute
    resource_group: RG3
    resource_name: 1.2.3
    resource_namespace: Microsoft.Compute
    resource_parent: galleries/SIG1/images/DEF1/
    resource_type: versions
    subscription: 33333333-3333-3333-3333-333333333333
    type: galleries

PLAY RECAP *******************************************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
```

## まとめ

Ansible の ARM リソース管理タスクの作成に Azure Resource Explorer や `azure_rids` フィルタを活用する手法について紹介しました。

Ansible では `azure_rids` のような基本機能のフィルタは標準添付になっていてほしいですし、また [azure_rm_resource] や [azure_rm_resource_info] だけでなくすべての ARM モジュールで、リソース ID を受け付ける `url` パラメータを追加してくれないかなと思っています。そのうち時間を見つけて、このような改善を Ansible にコントリビュートしていきたいです。

[ansible]: https://www.ansible.com
[arm]: https://docs.microsoft.com/ja-jp/azure/azure-resource-manager/management/overview
[azure_rm_resource]: https://docs.ansible.com/ansible/latest/modules/azure_rm_resource_module.html
[azure_rm_resource_info]: https://docs.ansible.com/ansible/latest/modules/azure_rm_resource_info_module.html
[azure_rm_dnszone]: https://docs.ansible.com/ansible/latest/modules/azure_rm_dnszone_module.html