てくなべ (tekunabe)

ansible / network automation / 学習メモ

[Ansible] ansible-plyabook コマンドに複数の Playbook を指定すると連続で実行される

はじめに

たまたま気がついたのですが、ansible-plyabook コマンドに複数のPlaybook を指定すると連続で実行されます。

公式ドキュメントの ansible-playbook コマンド説明ページや、ヘルプには以下のような記載があります。

usage: ansible-playbook [-h] [--version] [-v] [-k]
...(略)...
                     playbook [playbook ...]

この記事では、簡単なサンプルで動作例を紹介します。

  • 環境
    • Ansible 2.8.4 / 2.9.0

サンプル Playbook

3つ Playbook を準備します。

 - hosts: localhost
  connection: local
  gather_facts: no

  tasks:
    - name: debug
      debug:
        msg: "Playbook 1"   # ここで区別
 - hosts: localhost
  connection: local
  gather_facts: no

  tasks:
    - name: debug
      debug:
        msg: "Playbook 2"   # ここで区別
 - hosts: localhost
  connection: local
  gather_facts: no

  tasks:
    - name: debug
      debug:
        msg: "Playbook 3"   # ここで区別

実行

ansible-playbook コマンドの引数に、先ほど作成した Playbook 3つを指定して実行します。

$ ansible-playbook -i localhost, p1.yml p2.yml p3.yml 

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

TASK [debug] **********************************************************************************
ok: [localhost] => {
    "msg": "Playbook 1"
}

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


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

TASK [debug] **********************************************************************************
ok: [localhost] => {
    "msg": "Playbook 2"
}

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


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

TASK [debug] **********************************************************************************
ok: [localhost] => {
    "msg": "Playbook 3"
}

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

3つの Playbook が、指定した順番に連続で実行されました。

なお、試した限り、タスクの実行に失敗すると以降の Playbook は実行せずに中断しました。

[Ansible] Cisco ACI モジュールの state: query でオブジェクトの情報を取得する

【目次】

■ はじめに

Ansible は Cisco ACI にも対応して、多数のモジュールがあります。ほとんどのモジュールには、どのような状態であるべきかを指定する state オプションがあります。このオプションは presentabsent のような「状態」に加えて、query という検索する「操作」も指定できます。

この記事では、簡単なサンプルを元に state: query での情報取得の方法と、assert による値の確認などを説明します。

(私自身は調査中の部分もありますが、記録として残しておきます)

  • 環境
    • Cisco DevNet Sandbox (APIC 4.1)
    • Ansible 2.9.0




■ 使い方

基本的な使い方

基本的には、state オプションに query を指定するだけです。

特定の Tenant 情報を取得

f:id:akira6592:20191103225139p:plain
特定の Tenant 情報を取得

まず単純に、Tenant の情報を取得する Playbook です。オブジェクトの階層の上のほうなので、検索条件の指定方法は単純です。

ここでは、aci_tenant モジュールを利用し、tenant1 という名前の Tenant の情報を取得します。 query の結果を register で指定した変数 resutlt に格納して、debug モジュールで表示します。

  • Playbook
---
- name: query
  hosts: apic
  gather_facts: no

  tasks:
    - name: tenant
      aci_tenant:
        host: "{{ ansible_host }}"
        username: "{{ username }}"
        password: "{{ password }}"
        validate_certs: no
        tenant: tenant1   # Tenant 名を指定
        state: query      # query する 
        output_level: debug
      register: result

    - name: debug
      debug:
        var: result
  • 実行結果(debug抜粋)
TASK [debug] ***********************************************************************************
ok: [apic01] => {
    "result": {
...(略)...
        "current": [
            {
                "fvTenant": {
                    "attributes": {
                        "annotation": "",
                        "childAction": "",
                        "descr": "",
                        "dn": "uni/tn-tenant1",
                        "extMngdBy": "",
                        "lcOwn": "local",
                        "modTs": "2019-11-03T12:25:26.882+00:00",
                        "monPolDn": "uni/tn-common/monepg-default",
                        "name": "tenant1",
                        "nameAlias": "",
                        "ownerKey": "",
                        "ownerTag": "",
                        "status": "",
                        "uid": "15374"
                    }
                }
            }
        ],
...(略)...
}

もし、指定した名前の Tenant がない場合は、current が空のリスト(current: [])になります。

補足: query の条件に使用される値と使用されない値がある

少し注意が必要なのは「query の条件に使用される値と使用されない値がある」という点です。例えば、tenant オプションを指定すれば、「この名前のオプションはあるか」という query になります。 一方で、 description オプションは query 時は無視されます。

具体的には

  - aci_tenant:
      # ...(略)...
      tenant: tenant1
      description: hogehoge
      state: query

という指定した場合、単に

  • Tenant 名が tenant1 であること

のみが query 条件(SQLでいう where 句のようなもの)になります。

  • Tenant 名が tenant1 、かつ description が hogehoge であること

ではありません。

どのようなオプションが query の条件に使用されるか、一言で説明したものは見つけられていません。いくつか試して「オブジェクトの階層を示すオプション」は条件に使用される、という感触はあります。

たとえば、aci_epg モジュールの場合、EPGTenant > Application Profile > EPG というオブジェクトの階層なので、tenantap オプションは query 条件に使用され、 desicription はオブジェクトの階層には関係ない属性値なので query 条件に使用されない、とった具合です。

全 Tenant 情報を取得

f:id:akira6592:20191103225223p:plain
全 Tenant 情報を取得

aci_tenant モジュールで、tenant オプションを指定しないで query すると、全 Tenant の情報を取得します。

  • Playbook
---
  tasks:
    - name: tenant
      aci_tenant:
        host: "{{ ansible_host }}"
        username: "{{ username }}"
        password: "{{ password }}"
        validate_certs: no
        state: query
        output_level: debug
      register: result

    - name: debug
      debug:
        var: result
  • 実行結果(debug抜粋)
TASK [debug] ***********************************************************************************
ok: [apic01] => {
    "result": {
...(略)...
        "current": [
...(略)...
            {
                "fvTenant": {
                    "attributes": {
                        "annotation": "",
                        "childAction": "",
                        "descr": "",
                        "dn": "uni/tn-tenant1",
...(略)...
                    }
                }
            },
            {
                "fvTenant": {
                    "attributes": {
                        "annotation": "",
                        "childAction": "",
                        "descr": "",
                        "dn": "uni/tn-tenant2",
...(略)...
                    }
                }
            },
...(略)...
}

少し応用的な使い方

階層を伴うオブジェクトのモジュールでは、階層を示すオプション(例えば aci_epg モジュール)の tenantap オプション)の有無によって、検索パターンがあります。

ここでは、EPG(階層は Tenant > Application Profile > EPG)を扱う aci_epg モジュールを例にして、パターンごとに説明します。

パターン1. 何も指定なし

f:id:akira6592:20191103225259p:plain
何も指定なし

何も指定しないパターンです。

  tasks:
    - name: epg
      aci_epg:
        host: "{{ ansible_host }}"
        username: "{{ username }}"
        password: "{{ password }}"
        validate_certs: no
        state: query      # query する 
        output_level: debug
      register: result

    - name: debug
      debug:
        var: result

全 Tenant、全 AP、全 EPG の情報を取得できます。dn を見るとオブジェクトの階層が分かります。

  • 実行結果(debug抜粋)
TASK [debug] *******************************************************************
ok: [apic01] => {
    "result": {
...(略)...
        "current": [
            {
                "fvAEPg": {
                    "attributes": {
...(略)...
                        "dn": "uni/tn-tenant2/ap-ap1/epg-epg1",
...(略)...
            {
                "fvAEPg": {
                    "attributes": {
...(略)...
                        "dn": "uni/tn-tenant2/ap-ap2/epg-epg2",
...(略)...
            {
                "fvAEPg": {
                    "attributes": {
...(略)...
                        "dn": "uni/tn-tenant1/ap-ap1/epg-epg1",
...(略)...
            {
                "fvAEPg": {
                    "attributes": {
...(略)...
                        "dn": "uni/tn-tenant1/ap-ap2/epg-epg2",
...(略)...
}

パターン2. Tenant のみ指定

f:id:akira6592:20191103225333p:plain
Tenant のみ指定

Tenant のみ指定しないパターンです。

  tasks:
    - name: epg
      aci_epg:
        host: "{{ ansible_host }}"
        username: "{{ username }}"
        password: "{{ password }}"
        validate_certs: no
        tenant: tenant1   # Tenant の指定
        state: query      # query する 
        output_level: debug
      register: result

    - name: debug
      debug:
        var: result

指定した Tenant 内の、全 AP、全 EPG の情報を取得できます。

  • 実行結果(debug抜粋)
TASK [debug] ***********************************************************************************
ok: [apic01] => {
    "result": {
...(略)...
        "current": [
            {
                "fvTenant": {
                    "attributes": {
...(略)...
                        "name": "tenant1",
...(略)...
                    },
                    "children": [
                        {
                            "fvAp": {
                                "attributes": {
...(略)...
                                    "name": "ap2",
...(略)...
                                },
                                "children": [
                                    {
                                        "fvAEPg": {
...(略)...
                                                "name": "epg2",
...(略)...
                                        }
                                    }
                                ]
                            }
                        },
                        {
                            "fvAp": {
                                "attributes": {
...(略)...
                                    "name": "ap1",
...(略)...
                                },
                                "children": [
                                    {
                                        "fvAEPg": {
                                            "attributes": {
                                                "name": "epg1",
...(略)...
}

パターン3. Tenant、AP を指定

f:id:akira6592:20191103225401p:plain
Tenant、AP を指定

  tasks:
      - name: epg
      aci_epg:
        host: "{{ ansible_host }}"
        username: "{{ username }}"
        password: "{{ password }}"
        validate_certs: no
        tenant: tenant1   # Tenant の指定
        ap: ap1           # AP の指定
        state: query      # query する 
        output_level: debug
      register: result

    - name: debug
      debug:
        var: result

指定した Tenant、AP 内の、全 EPG の情報を取得できます。

  • 実行結果(debug抜粋)
TASK [debug] *******************************************************************
ok: [apic01] => {
    "result": {
...(略)...
        "current": [
            {
                "fvAp": {
                    "attributes": {
                        "annotation": "",
                        "childAction": "",
                        "descr": "",
                        "dn": "uni/tn-tenant1/ap-ap1",
...(略)...
                    },
                    "children": [
                        {
                            "fvAEPg": {
                                "attributes": {
...(略)...
                                    "name": "epg1",
...(略)...
}

パターン4. Tenant、AP、EPG を指定

f:id:akira6592:20191103225438p:plain
Tenant、AP、EPG を指定

  tasks:
    - name: epg
      aci_epg:
        host: "{{ ansible_host }}"
        username: "{{ username }}"
        password: "{{ password }}"
        validate_certs: no
        tenant: tenant1   # Tenant の指定
        ap: ap1           # AP の指定
        epg: epg1         # EPG の指定
        state: query      # query する 
        output_level: debug
      register: result

    - name: debug
      debug:
        var: result

指定した Tenant、AP、EPG の情報を取得できます。

  • 実行結果(debug抜粋)
TASK [debug] *******************************************************************
ok: [apic01] => {
    "result": {
...(略)...
        "current": [
            {
                "fvAEPg": {
                    "attributes": {
                        "annotation": "",
                        "childAction": "",
                        "configIssues": "",
                        "configSt": "applied",
                        "descr": "",
                        "dn": "uni/tn-tenant1/ap-ap1/epg-epg1",
...(略)...
}

パターン5. Tenant、EPG を指定

f:id:akira6592:20191103225507p:plain
Tenant、EPG を指定

  tasks:
    - name: epg
      aci_epg:
        host: "{{ ansible_host }}"
        username: "{{ username }}"
        password: "{{ password }}"
        validate_certs: no
        tenant: tenant1   # Tenant の指定
        epg: epg1         # EPG の指定
        state: query      # query する 
        output_level: debug
      register: result

    - name: debug
      debug:
        var: result

指定した Tenant、内の、全 EPG の情報を取得できます。

  • 実行結果(debug抜粋)
TASK [debug] *******************************************************************
ok: [apic01] => {
    "result": {
...(略)...
        "current": [
            {
                "fvTenant": {
                    "attributes": {
...(略)...
                        "dn": "uni/tn-tenant1",
...(略)...
                    },
                    "children": [
                        {
                            "fvAp": {
                                "attributes": {
...(略)...
                                    "name": "ap1",
...(略)...
                                },
                                "children": [
                                    {
                                        "fvAEPg": {
                                            "attributes": {
...(略)...
                                                "name": "epg1",
...(略)...
}




■ 戻り値について

各モジュールの説明ページの Return Values に戻り値の説明があります。

例: aci_epg モジュールの Return Values

ここまでの Playbook の実行結果でも示されていますが、regsiter で指定した変数の中に query 結果が格納されます。ここでは register: result とした場合で補足説明します。

0件の場合は空のリストの current になる

result.current に query 結果そのものがリストとして格納されます。query 結果が 0 件の場合は、空のリスト result.current[] になります。そのため、result.current 自体の有無では、結果の有無の確認はできないので注意です。もしかしたら、エラーになるケースもあるかもしれません。

output_level: debug しておくとリクエストした URL も分かる

output_level: debug しておくと、result.url には、モジュールが APIC にリクエストした URL が、result.filter にはフィルター文字(クエリストリング)が格納されます。「この指定方法で、実際どんなリクエストをしたんだろう?」と知りたいときに非常に便利です。

ファイル出力時の整形には to_nice_json が便利

結果の内容をファイル場合は、copy モジュールcontent: {{ result }} のように指定します。

ただ、そのままですと、平ぺったい JSON

{"status": 200, "proposed": {}, "url": "https://apic01/api/mo/uni/tn-tenant1.json", ...(略)...

のように、人間には読みにくい形になっていまいます。そこで to_nice_json フィルターを利用すると、読みやすく nice になります。

    - name: file output
      copy:
        content: "{{ result | to_nice_json }}"
        dest: result_tenant.json
$ cat result_tenant.json
{
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python"
    },
    "changed": false,
    "current": [
        {
            "fvTenant": {
                "attributes": {
                    "annotation": "",
                    "childAction": "",
                    "descr": "",
                    "dn": "uni/tn-tenant1",
...(略)...
}

assert でオブジェクトの有無や値をチェックする

情報を取得できるということは、assert モジュールを利用して、期待した状態とあっているかどうかのチェックもできます。

ここでは、オブジェクトの有無を assert する例と、オブジェクトが持っている値を assert する例を紹介します。

オブジェクトの有無を assert する

query した結果の current のリストの長さが 1 以上であることを確認することで、オブジェクトの有無を確認できます。

ここでは、指定した Tenant、AP 内に、指定した EPG があることを確認します。

  tasks:
      - name: assert test
        aci_epg:
          host: "{{ ansible_host }}"
          username: "{{ username }}"
          password: "{{ password }}"
          validate_certs: no
          tenant: tenant1   # Tenant の指定
          ap: ap1           # AP の指定
          epg: epg1         # EPG の指定
          state: query      # query する 
          output_level: debug
        register: result

      - name: assert
        assert:
          that:
            -  (result.current | length) >= 1

他にも色々方法はあると思います。

  • 実行結果(assert抜粋)
TASK [assert] ******************************************************************
ok: [apic01] => {
    "changed": false,
    "msg": "All assertions passed"
}

assert が OK であれば All assertions passed と表示されます。

値を assert する

query した結果の current の中の ssert したい値を指定して assert します。

ここでは、指定した Tenant、AP 内に、指定した EPG の Description が test であることを確認します。

f:id:akira6592:20191103225556p:plain:w400
EPG の Description

  tasks:
    - name: assert test
      aci_epg:
        host: "{{ ansible_host }}"
        username: "{{ username }}"
        password: "{{ password }}"
        validate_certs: no
        tenant: tenant1   # Tenant の指定
        ap: ap1           # AP の指定
        epg: epg1         # EPG の指定
        state: query      # query する 
        output_level: debug
      register: result

    - name: assert
      assert:
        that:
          -  result.current[0].fvAEPg.attributes.descr == "test"

current[0] と指定しているあたりが、あまりスマートではないですね・・)

  • 実行結果(assert抜粋)
TASK [assert] ******************************************************************
ok: [apic01] => {
    "changed": false,
    "msg": "All assertions passed"
}

assert が OK であれば All assertions passed と表示されます。




■ オブジェクト特化モジュールで対応できないなら aci_rest モジュールの出番

これまで紹介したように、aci_tenantaci_epg など、各オブジェクト用のモジュールで state: query を指定することで、query を実行できますが、なかには思うような検索条件を指定できないこともあるかもしれません。

そんなときには、APICREST API を叩くことに特化した、aci_rest モジュールを利用します。Playbook を書く側が REST API の仕様を意識する必要がある文、柔軟な処理を指定できます。

詳細は、公式ドキュメントの aci_rest モジュール の説明ページを参照してください。




■ さいごに

Cisco ACI モジュール の state: query でオブジェクト情報を取得したり、assert する方法をご紹介しました。

設定系のタスクよりも query のほうが、よりオブジェクトの階層を意識する必要があるきがしました。

[Ansible] Cisco ACI モジュールの署名ベース認証方式の仕組みと準備とPlaybookの書き方

【目次】

■ はじめに

Ansible は Cisco ACI にも対応して、多数のモジュールがあります。APICREST API を叩く仕様になっています。

Cisco ACI Guide という Ansible の公式ドキュメントにも記載されていますが、認証方式は 2通りあります。

  1. パスワードベース認証方式 (Password-based authentication)
  2. 署名ベース認証方式 (Signature-based authentication using certificates)

この記事では、署名ベース認証方式の仕組み、環境の準備、Playbook の書き方を説明します。 少々長くなるので先にまとめます。

  • パスワードベース認証では、DoS対策機能にひっかかってしまうことがあるため、自動化においては署名ベース認証方式のほうが吉
  • 準備として、予めAnsible 側で証明書(公開鍵)と秘密鍵のペアを生成し、証明書を APIC に登録しておく
  • Ansible から APIC へのリクエスト時に URL などを元にした署名を Cookie にセットし、APIC で検証、認証する
  • 各 ACI モジュールでは、署名ベース認証用の certificate_nameprivate_key オプションを利用する
  • certificate_name のデフォルト値は private_key の指定方法によって異なる

前提環境

  • APIC: Cisco DevNet Sandbox (APIC 4.1)
  • Ansible 2.8.6
  • 署名ベース認証の対象ユーザー: admin
    • 手順の検証容易性のために admin を利用していますが、運用環境では他のユーザーを利用を推奨

(参考)パスワードベース認証方式の制限

パスワードベース認証方式は、Web GUI と同じ認証方式なので手軽で便利ですが、Password-based authentication の Note や、Known issues には、「パスワード認証は、リクエストの頻度によっては(D)DoS対策の機能にひっかかり、HTTP 503 でログインエラーになる」旨が書かれています。署名ベース認証方式は、この問題の解決にもなります。

Password-based authentication also may trigger anti-DoS measures in ACI v3.1+ that causes session throttling and results in HTTP 503 errors and login failures.

  

Starting with ACI v3.1 the APIC will actively throttle password-based authenticated connection rates over a specific threshold. This is as part of an anti-DDOS measure but can act up when using Ansible with ACI using password-based authentication. Currently, one solution is to increase this threshold within the nginx configuration, but using signature-based authentication is recommended.




■ 仕組み

署名ベース認証方式は、SSH 接続時の公開鍵認証や SSL/TLS におけるクライアント認証とは異なる方式です。

ACI の公式ドキュメントの以下のページや、Ansible のソースコードをもとにして分かった仕組みについてまとめます。

予めしておくこと

予め、以下の流れで準備しておきます。

f:id:akira6592:20191102204740p:plain:w500
秘密鍵と証明書を作成して、APICに証明書を登録しておく

  1. ユーザーの自己署名の証明書(公開鍵含む)と秘密鍵を生成する
  2. 証明書を APIC 側に登録する

より具体的な手順は、後述の「環境準備の手順」で説明します。

リクエストごとに行っていること

リクエストごとに以下の流れで署名データを作成して認証しています。

f:id:akira6592:20191102205029p:plain
署名データなどをCookieに付加してAPIC側で検証

リクエストごとに以下の流れで署名データを作成、付加して認証しています。

  1. Ansible 側で、リクエストの HTTP メソッド(GET/POST/DELETEなど)と、URL、ペイロード(実データのjson/xml)を文字列連結する
  2. Ansible 側で、1 の文字列データのハッシュ値を sha256 で計算する
  3. Ansible 側で、2 のハッシュ値に対して、ユーザーの秘密鍵(A)を用いて署名を生成する
  4. Ansible 側で、Cookie に、署名や、証明書のDN(Distinguished Name: どのユーザーのどの証明書か)などを付加する。DN には、後述するモジュールの usernamecertificate_name オプションの名前が利用される
  5. Ansible から APICREST API のリクエストをする
  6. APIC 側で、送られてきたリクエストの HTTP メソッド(GET/POST/DELETEなど)と、URL、ペイロード(実データのjson/xml)を文字列連結する(Ansible側の 1 と同様)
  7. APIC 側で、5 の文字列データのハッシュ値を sha256 で計算する(Ansible側の 2 と同様)
  8. APIC 側で、リクエストの Cookie 内の証明書のDN(どのユーザーのどの証明書か)から、ユーザーの証明書(公開鍵)を特定
  9. APIC 側で、リクエストの Cookie 内の署名を、ユーザーの証明書(公開鍵)で復号し、元のハッシュ値を計算する
  10. APIC 側で、7 と 9 結果のハッシュ値が一致することの確認をもってユーザー認証する

APIC 側の処理はユーザー側(Ansible)で行っていることからの推測

仕組みは以上です。




■ 環境準備の手順

実際に、Ansible の ACI モジュールで署名ベースの認証を利用するために必要な手順です。

Ansible 側でユーザー側で証明書(公開鍵)と秘密鍵を生成する

Asible 公式ドキュメントの Generate certificate and private key に従い、証明書(公開鍵)と秘密鍵を生成します。有効期限などはセキュリティポリシーに応じて変えます。

$ openssl req -new -newkey rsa:1024 -days 36500 -nodes -x509 -keyout admin.key -out admin.crt -subj '/CN=Admin/O=Your Company/C=US'

なお、subject に指定する値は、認証そのものには影響しません。

APIC 側でユーザーの証明書を登録する

手動で登録する方法と、Ansible で登録する方法があります。

手動で登録する場合

今回利用している Cisco DevNet Sandbox の環境(APIC 4.1)では、Ansible 公式ドキュメントの Configure your local user のステップでは、証明書登録画面にたどり着けませんでした。

代わりに、以下の手順で証明書を登録します。

  1. Admin > AAA > Users でユーザー一覧を開き、対象のユーザーをクリックします。なお、署名ベース認証に対応しているの Local User のみです。

    f:id:akira6592:20191102205454p:plain
    対象ユーザーの選択

  2. 対象ユーザ設定画面内の User Certificates+ をクリックします。

    f:id:akira6592:20191102205530p:plain
    証明書追加ボタン

  3. Create X509 Certificate 画面の NameData を入力して、Submit ボタンをクリックします。

    f:id:akira6592:20191102205605p:plain
    証明書の名前の指定とデータの貼り付け

フィールド名 説明
Name 証明書の名前を指定します。ユーザー名と異なる名前でも構いません。後述する Ansible モジュールで指定する certificate_name が指す名前です。
Data Ansible 側で生成した証明書のデータ(ここでは admin.crt の内容)を貼り付けます。

Ansible で登録する場合

aci_aaa_user_certificate モジュールによって、ユーザーの証明書を登録作業自身を Ansible で実行できます。

# 証明書を登録する Playbook。このタスクの認証自体はパスワードベース。
- name: Ensure we have a certificate installed
  aci_aaa_user_certificate: 
    host: my-apic-1
    username: admin
    password: my-password

    aaa_user: admin   # 証明書を登録する対象のユーザー名
    certificate_name: admin   # 証明書の名前、Create X509 Certificate 画面の Name 相当
    certificate: "{{ lookup('file', 'pki/admin.crt') }}"  # 証明書データ、Create X509 Certificate 画面の Data 相当

引用元: 公式ドキュメント - Configure your local user、コメントは追記

環境の準備は以上です。




■ 署名ベース認証方式を利用するPlaybook の書き方

次は 署名ベース認証方式を利用するPlaybook の書き方についてです。

Playbook

署名ベース認証では、証明書の名前 certificate_name と、秘密鍵のパスまたはデータ private_key というオプションを利用します。

---
- hosts: apic
  gather_facts: no

  tasks:
    - name: tenant
      aci_tenant:
        host: "{{ ansible_host }}"
        username: admin               # ユーザー名
        certificate_name: admin       # 証明書の名前
        private_key: admin.key    # 秘密鍵
        validate_certs: no
        tenant: test_tenant01
        state: present

各オプションと証明書設定画面のフィールドの関係

オプションと証明書設定画面のフィールドの関係は以下のとおりです。

f:id:akira6592:20191102211020p:plain:w500
各オプションと証明書設定画面のフィールドの関係

  • username オプション
    • ユーザ名を指定
    • ユーザー名と証明書の名前が同じであれば usernamecertificate_name のどちらかの指定のみでも可
  • certificate_name オプション
    • 証明書の名前を指定
    • APIC-側でユーザーの証明書を登録する」の手順で name フィールドに指定した名前。今回の場合は admin
    • ユーザー名と証明書の名前が同じであれば usernamecertificate_name のどちらかの指定のみでも可
  • private_key オプション

certificate_name オプションのデフォルト値は、private_key オプションの指定の仕方によります。

private_key の指定方法 certificate_name のデフォルト
データそのものを指定した場合(file lookup plugin 含む) username で指定したユーザー名
秘密鍵のパスを指定した場合 秘密鍵basename から拡張子を除外した値(admin.key なら admin

対応するコードは aci.py のこのあたりです。(当初、各モジュールのページにちゃんと書いてあったことに気が付かずにコードを読んでいました・・)

いくつか具体例をあげます。

      # 1. わかりやすさ重視ですべて明示する場合
      username: admin
      certificate_name: admin
      private_key: "{{ lookup('file', 'crt/admin.key') }}"

      # 2. ユーザー名と証明書の名前が同じの場合
      ## certificate_name 省略パターン
      username: admin
      private_key: "{{ lookup('file', 'crt/admin.key') }}"

      ## username 省略パターン
      certificate_name: admin
      private_key: "{{ lookup('file', 'crt/admin.key') }}"

      # 3. ユーザー名と証明書の名前が異なる場合
      username: admin
      certificate_name: admincert
      private_key: "{{ lookup('file', 'crt/admin.key') }}"

      # 4. ユーザー名、証明書の名前、秘密鍵ファイルのファイル名が同じの場合
      private_key: "crt/admin.key"

補足

  • パスワード認証では usernamepaswword オプションを利用します。
  • validate_certs オプションはユーザー認証には直接関係ありません。APIC への HTTPS 接続する際のサーバー証明書の確認に関するオプションです。

Playbook の実行

Playbook を実行した例を掲載します。

$ ansible-playbook -i inventory.ini tenant.yml 

PLAY [apic] **********************************************************************

TASK [tenant] ********************************************************************

changed: [apic01]

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

無事に、署名ベース認証方式を利用して、Ansible の ACI モジュールを実行できました。




■ まとめ

APIC が備える認証方式のうち、署名ベース認証方式の仕組み、準備、Playbook の書き方についてまとめました。 以下、冒頭に掲載したまとめを再掲します。

  • パスワードベース認証では、DoS対策機能にひっかかってしまうことがあるため、自動化においては署名ベース認証方式のほうが吉
  • 準備として、予めAnsible 側で証明書(公開鍵)と秘密鍵のペアを生成し、証明書を APIC に登録しておく
  • Ansible から APIC へのリクエスト時に URL などを元にした署名を Cookie にセットし、APIC で検証、認証する
  • 各 ACI モジュールでは、署名ベース認証用の certificate_nameprivate_key オプションを利用する
  • certificate_name のデフォルト値は private_key の指定方法によって異なる

[Ansible] ACIモジュールでリクエストしたURLを知るには output_level: debug を指定する

■ はじめに

Ansible には多数のACIモジュールがあり、内部で APICREST API を叩く仕様です。

通常時はあまり意識する必要がないかも知れませんが、デバッグ時など細かい情報を知りたい場合「このタスクでどのような REST API の URL をリクエストしたのか」を知りたいことがあります。

リクエストした URL は以下の方法で確認できます。

  • モジュールのオプションに output_level: debug を指定する
  • タスクの実行結果(戻り値)register 変数に格納する
  • register 変数内の urlfilter を確認する

なお、タスクの実行に失敗(failure)したときは、output_level: debug の指定は不要です。

この記事では、正常時にもリクエストした URL を家訓する方法法を、簡単な例をもとに説明します。


■ サンプル Playbook

ここでは、 aci_bd モジュールを利用して、指定した Bridge Domain を query するタスクで試します。

---
- name: query
  hosts: apic
  gather_facts: False

  tasks:
    - name: query db
      aci_bd:
        host: "{{ ansible_host }}"
        username: "{{ username }}"
        password: "{{ password }}"
        validate_certs: False
        tenant: t_test9999
        bd: bd_test9999
        state: query
        output_level: debug   # point!
      register: result

    - name: debug
      debug:
        var: result


■ 実行結果

$ ansible-playbook -i inventory.ini aci_test.yml 

PLAY [query] **************************************************************************************************************

TASK [query db] ***********************************************************************************************************

ok: [apic01]

 ...(略)...

TASK [debug] **************************************************************************************************************
ok: [apic01] => {
    "result": {
        "ansible_facts": {
            "discovered_interpreter_python": "/usr/bin/python"
        },
        "changed": false,
        "current": [
          {
                "fvBD": {
                    "attributes": {
                        "OptimizeWanBandwidth": "no",
                        "annotation": "",
                        "arpFlood": "yes",
                        "bcastP": "225.1.31.64",
                        "childAction": "",
                        "configIssues": "",
                        "descr": "",
                        "dn": "uni/tn-t_test9999/BD-bd_test9999",
 ...(略)...
        ],
        "failed": false,
        "filter_string": "?rsp-subtree-class=fvRsBdToEpRet,fvRsCtx,fvRsIgmpsn,fvRsBDToNdP&rsp-subtree=full",
        "method": "GET",
        "proposed": {},
        "response": "OK (2639 bytes)",
        "sent": {},
        "status": 200,
        "url": "https://apic01.example.com/api/mo/uni/tn-t_test9999/BD-bd_test9999.json",
 ...(略)...

上記結果の urlfilter_string の値から、今回リクエストした URL は以下であることが分かります。

たとえば、このタスクの挙動を再現する場合は、上記URLをGET(state: query なので)して確認します。別途、認証済み cookie が必要です。

(ちなみに、ACI の各モジュールのドキュメントの Return Values には、 filter_string failure or debug とありますが、debug のことを 当初 ansible-plyabook コマンドの -v オプションのことかと勘違いしておりました・・)


■ まとめ

ACI モジュールでリクエストしたURLを知るには、output_level: debug を指定して、タスク実行結果内の urlfilter を参照する、ということをご紹介しました。

[Ansible] 「Ansible 実践ガイド 第3版」執筆中にあげた issue、出会った issue

book.impress.co.jp

はじめに

こちらの記事でお伝えしましたが、この度「Ansible 実践ガイド 第3版」の「5-3 ネットワーク機器の構成管理」を執筆させていただきました。

この記事では、裏話的な、執筆中に GitHub にあげた issue や、出会った issue をご紹介します。


■ junos_user モジュール の encrypted_password に関する WARNING

「5-3-3 ネットワーク期設定変更の実装」で登場する Playbook で junos_user モジュールを利用しています。Junos の機器に対して、ユーザーを追加や削除するモジュールです。

Ansible 2.8.3 で動作確認をしていた時に、以下の WARNING が表示されることに気が付きました。

 [WARNING]: Module did not set no_log for encrypted_password

encrypted_password オプションは、パスワードを設定するオプションであるのに対して、no_log というログに表示させないための内部の設定がされていない、という旨の WARNINGです。本書の Playbook では encrypted_password オプションは利用していませんが、それでも表示されるのが気になったため、以下の issue をあげておきました。

github.com

その後、ありがたいことに、修正してくださった方のPRが develstable-2.9stable-2.8 ブランチにマージされました。2.8 系については、 Ansible 2.8.6 で修正されました。

  • リリースノート

https://github.com/ansible/ansible/blob/stable-2.8/changelogs/CHANGELOG-v2.8.rst#bugfixes

junos_user - Add no_log=True to junos_user encrypted_password (https://github.com/ansible/ansible/pull/62184)


■ debugger で task_vars が更新できなかった

「7-4-4 Playbook Debugger」では、Playbook の実行を途中でとめて、変数の値を確認、変更などできる、Playbook Debugger の使い方を紹介しています。

Ansible 2.7.0rc4 で動作確認をしていた時に、 task_vars コマンドでタスクの変数を更新しても、実際の動作に反映されない現象に出会いました。どこかのバージョンでこうなってしまったようです。以下の issue をあげておきました。

github.com

最終的には、 task_vars を変更したら、debugger 上の update_task という新しいコマンドを実行する、という手順で対応していただきました。 github.com

Ansible 2.8.0 で修正されました。changelog には見当たりませんが、Playbook Debugger のページには、u(pdate_task)New in version 2.8. と記載されています。

書籍内でも update_task を紹介しています。


■ network_cli の接続タイミングの仕様変更(Ansible 2.9)

この件が一番悩ましかったです。Ansible 2.9 で予定されている仕様変更です。 本書は Ansible 2.8 前提ですし、執筆時リリース Ansible 2.9 は未リリースなので、問題ないといえば問題ないのですが、この点だけはどうしても気になりました。(全部気にしていると、そもそも Ansible 2.8 の本ではなくなる)

この仕様変更が何に影響しそうだったかというと「5-3-1 ネットワークモジュールの特徴」内の「接続確認の方法」です。ここは、インベントリを作って、変数定義を作ったところで ansible コマンドで接続確認してみましょう、というところです。

当初は、 ping モジュールの組み合わせを利用していました。Ansible 2.8 までは、network_cli コネクションプラグインでは、ping モジュールを使うタイミングで、実際にネットワーク機器に接続していました。つまり、ping モジュールのようにネットワーク機器へ接続不要なモジュールでも、接続しにいきます。この性質を利用して接続確認の手順としていました。(ややこしい話ですが、サーバー相手に ping モジュール使うのとは事情が異なります)

ところが、何気なく issue を眺めていると偶然以下のコメントを発見しました。

github.com

Ansible 2.9 では、実際に接続が必要になるまで開始しない。逆に、 ios_command モジュールのように、ネットワーク機器にコマンドを実行するために接続が必要なモジュールが必要なタイミングで開始するということです。

回りくどい説明になってしまいましたが、結果としては、「Ansible 2.9 では network_cliping モジュールで必ず OK になってしまい、接続確認したことにならない」ということになります。

結局は ios_command モジュールを利用する例に差し替えました。一連の執筆作業のかなり終盤での修正でした。編集の方には感謝感謝です。

なお、本件については、2019/11/20 開催のAnsiblejpネットワーク部 2019.11の発表でも触れる予定です。


■ iosxr_* モジュールとコネクションプラグイン

(ちょっと忘れかけています) Ansible 2.8 でも、ioxrs_* モジュールは、モジュールによって network_cli や netconf コネクションプラグインに対応していました。どちらも使えるモジュールについては、「でればあっちを使ってね」といった旨の WARNING が表示されました。そのことも書こうと思っていたのですが、Ansible 2.9 で修正される気配があったので言及はさけました(どのPRかは失念)。 なお、Ansible 2.9 では、IOS XR モジュールを使ううえで役立つ、プロットフォーム個別のガイドが公式ドキュメント上に作成されました。

docs.ansible.com


おわりに

Ansible 実践ガイド 第3版」執筆中に GitHub にあげた issue や、出会った issue をご紹介しました。 日々変わっていくOSSの情報を紙媒体に落とし込むことの難しさを感じました。日頃から issue や PR を眺める習慣が多少あったので、それで救われたことがあったのはとかったです。一方で、メンテナンスしている方がには頭が下がります。

[Ansible] 公式ドキュメントに優先順に関する総合的なページができた(設定、コマンドライン引数、Playbook Keywords、変数)

はじめに

2019年7月、Ansible の公式ドキュメントに、優先度に関する総合的なページができていました。

docs.ansible.com

f:id:akira6592:20191020090515p:plain
左メニューのここから

対象は以下の4つです。

今まで、変数の優先順位や、ansible.cfgの優先順位のように、個別の説明ページはありました。今回はそれらの上位にあたるページのようです。


Playbook keywords の場合

たとえば、以下のように Play レベル、Task レベルそれぞれに connection が指定されている場合、基本は Play レベルで指定されている ssh で、2つめの Task は、Task レベルで指定されている paramiko での方式になります。

- hosts: all
  connection: ssh
  tasks:
    - name: This task uses ssh.
      ping:

    - name: This task uses paramiko.
      connection: paramiko
      ping:

引用元)


ところで

Playbook Keywords と 変数 はどちらが勝つ?

Playbook Keywords 内の対決、変数内の対決は上記のドキュメント経由で分かるのですが、「Playbook Keywords 対 変数」のようにジャンル(?)をまたいだ対決はどうなるのでしょうか。

例えば、コネクションプラグインの指定は複数の方法があります。Playbook Keyword としての connection: hogehoge でも指定できますし、変数として、ansible_connection: hogehoge も指定できます。

以下のツイートで、言及されていました。

勝ち 負け 補足
Task レベルの ansible_connection 変数 Play レベルの connection キーワード
インベントリの ansible_connection 変数 Play レベルの connection キーワード 変数同士の対決であれば、Play レベルが勝つので、直感に反するという意見も

混乱のもとになるので、コネクションプラグインの指定は変数による指定に統一したほうがいいと思います。

CentOS 8 を VirtualBox にインストールする場合は「Server with GUI」以外を選択しないと画面が乱れる

■ はじめに

CentOS 8 をVirtualBox にインストールしようとしたら、終盤に以下のような乱れた画面になってしまいました。

f:id:akira6592:20191019190041p:plain
乱れた画面

これは、実際は LICENSING の画面のようです。 Tab + Enter を駆使してどうにか切り抜けた方もいらっしゃいました。

f:id:akira6592:20191019190129p:plain
本来の画面

調べてみると、インストール途中の「Software Selection」でデフォルトの「Server with GUI」が選ばれていることに起因するようです。

ja/Manuals/ReleaseNotes/CentOS8.1905 - CentOS Wiki

VirtualBox のゲストOSとしてインストールする場合には、インストーラーにて、デフォルトの "Server with a GUI" / "サーバー (GUI使用)" を選択すべきではありません。これを選んだ場合には既知の問題があります。

RHEL 8 VirtualBox Quick Install | Red Hat Developer

Note: Do not choose Server with a GUI as your base environment. There is a known issue that will prevent the graphical desktop from starting


■ 対処方法

インストール途中の INSTLLATION SUMMRY 画面で「Software Collection」をクリックします。

f:id:akira6592:20191019191130p:plain
Software Collection をクリック

「Server with GUI以外 を選択して、Done をクリックします。

f:id:akira6592:20191019191856p:plain
Server with GUI 以外を選択

その後、インストールを続行すると無事にインストールが完了します。

f:id:akira6592:20191019191328p:plain
インストール後のバージョン確認

以上です。