What's this?

これは次の Ansible Advent Calendar 2020 に参加して書いている記事となります。 他の方の記事については下記のリンクからたどれますので是非あわせてご参照下さい。

Lint を使ってみよう

前回記事 では yamllint の使い方について紹介しました。 本記事では Ansible-lint について使い方を簡単に紹介します。

前準備

ツールの導入を簡単にするために以降では 前々回記事 でふれた tox というツールの利用を前提としています。 この記事に書かれていることを試したりする際はまず tox をご用意下さい。

Ansible-lint を使ってみよう

ansible-lint は pip でインストールできます。 前回記事と同様に tox で試してみましょう。

前準備として次のような tox.ini と tox 内で参照する requirements.txt というファイルを用意しておきます。

[ssato@localhost 04]$ ls
requirements.txt  tox.ini
[ssato@localhost 04]$ cat requirements.txt
ansible-lint
[ssato@localhost 04]$ cat tox.ini
[tox]
envlist = py36
skipsdist = true

[testenv]
deps =
    -r {toxinidir}/requirements.txt
commands =
    ansible-lint --version
    ansible-lint --help
[ssato@localhost 04]$ tox

tox を使って一旦 ansible-lint をインストール、ヘルプを表示してみましょう。

[ssato@localhost 04]$ tox
py36 create: /tmp/0/04/.tox/py36
py36 installdeps: -r/tmp/0/04/requirements.txt
py36 installed: ansible==2.10.4,ansible-base==2.10.3,ansible-lint==4.3.7, .. (snip) ..
py36 run-test-pre: PYTHONHASHSEED='641665413'
py36 run-test: commands[0] | ansible-lint --version
ansible-lint 4.3.7
py36 run-test: commands[1] | ansible-lint --help
usage: ansible-lint [-h] [-L] [-f {rich,plain,rst}] [-q] [-p]
                    [--parseable-severity] [--progressive] [-r RULESDIR] [-R]
                    [--show-relpath] [-t TAGS] [-T] [-v] [-x SKIP_LIST]
                    [-w WARN_LIST] [--nocolor] [--force-color]
                    [--exclude EXCLUDE_PATHS] [-c CONFIG_FILE] [--version]
                    [playbook [playbook ...]]

positional arguments:
  playbook              One or more files or paths. When missing it will
                        enable auto-detection mode.

optional arguments:
  -h, --help            show this help message and exit
  -L                    list all the rules
  -f {rich,plain,rst}   Format used rules output, (default: rich)
  -q                    quieter, although not silent output
  -p                    parseable output in the format of pep8
  --parseable-severity  parseable output including severity of rule
  --progressive         Return success if it detects a reduction in number of
                        violations compared with previous git commit. This
                        feature works only in git repositories.
  -r RULESDIR           Specify custom rule directories. Add -R to keep using
                        embedded rules from
                        /tmp/0/04/.tox/py36/lib/python3.6/site-
                        packages/ansiblelint/rules
  -R                    Keep default rules when using -r
  --show-relpath        Display path relative to CWD
  -t TAGS               only check rules whose id/tags match these values
  -T                    list all the tags
  -v                    Increase verbosity level
  -x SKIP_LIST          only check rules whose id/tags do not match these
                        values
  -w WARN_LIST          only warn about these rules, unless overridden in
                        config file defaults to 'experimental'
  --nocolor             disable colored output
  --force-color         Try force colored output (relying on ansible's code)
  --exclude EXCLUDE_PATHS
                        path to directories or files to skip. This option is
                        repeatable.
  -c CONFIG_FILE        Specify configuration file to use. Defaults to
                        ".ansible-lint"
  --version             show program's version number and exit
_____________________________________ summary _____________________________
  py36: commands succeeded
  congratulations :)
[ssato@localhost 04]$

ansible-lint の対象ファイルは一つ以上の Ansible Playbook ファイルまたは role ディレクトリとなります。 指定のファイルまたはディレクトリを起点にしてたどって参照されている Ansible Role を構成するいくつかのファイルもあわせて読込み、ルールにそってチェックします。

ansible-lint をより実践的に試すために Ansible Playbook を用意してみましょう。 前回と同様、内容的にあまり意味はないのですがサンプルとして次のようなものを用意してみます。

  • ファイルとディレクトリ構造:
[ssato@localhost 04]$ ls
40_ping.yml  requirements.txt  roles  tox.ini
[ssato@localhost 04]$ tree
.
├── 40_ping.yml
├── requirements.txt
├── roles
│   └── do_ping
│       ├── defaults
│       │   └── main.yml
│       └── tasks
│           ├── debug.yml
│           ├── main.yml
│           └── ping.yml
└── tox.ini

4 directories, 7 files
[ssato@localhost 04]$
  • 40_ping.yml (Ansible Playbook):
---
- hosts: localhost
  roles:
    - do_ping
  • roles/do_ping/defaults/main.yml
---
foo: true
bar: "yes"
  • roles/do_ping/tasks/main.yml
---
- include_tasks: debug.yml
- include_tasks: ping.yml
  • roles/do_ping/tasks/debug.yml
---
- debug:
    msg: >-
      foo: {{ foo | d(true) }}
      bar: {{ bar | d('yes') }}
  • roles/do_ping/tasks/ping.yml
---
- ping:
- name: Run ping command
  shell: >-
    ping -c 3 {{ inventory_hostname }}

ansible-playbook コマンドで --syntax-check し実際に実行しても特に問題はないことがわかります。

[ssato@localhost 04]$ source .tox/py36/bin/activate
(py36) [ssato@localhost 04]$ ansible-playbook --syntax-check 40_ping.yml ; echo $?

playbook: 40_ping.yml
0
(py36) [ssato@localhost 04]$ ansible-playbook 40_ping.yml

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

TASK [Gathering Facts] *******************************************************
ok: [localhost]

TASK [do_ping : include_tasks] ***********************************************
included: /tmp/0/04/roles/do_ping/tasks/debug.yml for localhost

TASK [do_ping : debug] *******************************************************
ok: [localhost] => {
    "msg": "foo: True bar: yes"
}

TASK [do_ping : include_tasks] ***********************************************
included: /tmp/0/04/roles/do_ping/tasks/ping.yml for localhost

TASK [do_ping : ping] ********************************************************
ok: [localhost]

TASK [do_ping : Run ping command] ********************************************
changed: [localhost]

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

(py36) [ssato@localhost 04]$

それでは ansible-lint を試してみましょう。

(py36) [ssato@localhost 04]$ ansible-lint 40_ping.yml
WARNING  Listing 3 violation(s) that are fatal
[502] All tasks should be named
roles/do_ping/tasks/ping.yml:2
Task/Handler: ping

[301] Commands should not change things if nothing needs doing
roles/do_ping/tasks/ping.yml:3
Task/Handler: Run ping command

[305] Use shell only when shell functionality is required
roles/do_ping/tasks/ping.yml:3
Task/Handler: Run ping command

You can skip specific rules or tags by adding them to your configuration file:

┌────────────────────────────────────────────────────────────────────────────────┐
│ # .ansible-lint                                                                │
│ warn_list:  # or 'skip_list' to silence them completely                        │
│   - '301'  # Commands should not change things if nothing needs doing          │
│   - '305'  # Use shell only when shell functionality is required               │
│   - '502'  # All tasks should be named                                         │
└────────────────────────────────────────────────────────────────────────────────┘
(py36) [ssato@localhost 04]$

いくつか警告が表示されましたが次のように変更して対応できます。

(py36) [ssato@localhost 04]$ cp -a roles/do_ping{,_ng}
(py36) [ssato@localhost 04]$ vi roles/do_ping/tasks/ping.yml
(py36) [ssato@localhost 04]$ diff -uNr roles/do_ping{_ng,}
diff -uNr roles/do_ping_ng/tasks/ping.yml roles/do_ping/tasks/ping.yml
--- roles/do_ping_ng/tasks/ping.yml     2020-12-03 23:45:22.893212088 +0900
+++ roles/do_ping/tasks/ping.yml        2020-12-03 23:56:31.211030940 +0900
@@ -1,5 +1,8 @@
 ---
-- ping:
+- name: Run ping module
+  ping:
+
 - name: Run ping command
-  shell: >-
+  command: >-
     ping -c 3 {{ inventory_hostname }}
+  changed_when: false
(py36) [ssato@localhost 04]$ ansible-lint 40_ping.yml ; echo $?
0
(py36) [ssato@localhost 04]$

このルールも含めた ansible-lint でチェック可能な標準ルールについては公式文書もあわせてご参照下さい。

ansible-lint のルールの探索パス

ansible-lint は default では ansible-lint の python モジュールのインストールパス下から適用するルールを探します。

  • 標準ルール rules/ 内の .py*: Python ファイル、ansiblelint.rules.AnsibleLintRule class を継承する class 定義を必ず含み、一つルールを実装
  • (plugin 形式の) カスタムルール rules/custom/<plugin_name>/ 内の .py*: 場所が異なることを除き、標準ルールと同じ

後者については筆者が中心となってこの場所に標準化しました [1] ので作法に沿って python パッケージとして作られた ansible-lint plugin [2] であればインストールするだけで標準ルールに加えて適用されます。

ルールの探索パスは -r と -R の二つのコマンド行オプションによって制御できます。 これらのオプションの組合せによって default に加えてルール探索パスを追加したり、 default の探索パスは使わず指定のルール探索パスだけを利用したりできます。

詳細については公式文書も含めてご参照下さい。

[1]https://github.com/ansible/ansible-lint/pull/935
[2]カスタムルールの plugin パッケージ化については https://ansible-lint.readthedocs.io/en/latest/rules.html#packaging-custom-rules を参照。

ansible-lint のルールの有効化・無効化

Ansible-lint は yamllint と違い、通常ルール側で何か特別な設定方法がないかぎり [3] は細かな設定などはできず、無効化できるのみとなります。

Ansible-lint のルールは各ルールにあらかじめ指定されているタグを使って適用するものを指定したり、除外 したり、対象ファイルの中でコメントや task 定義の中の tags に skip_ansible_lint を指定してスキップ (その部分についてルールを適用しない = 無効化) したりできます。 また .ansible-lint という設定ファイルでも細かな挙動の制御に加えて同様の設定が可能です。

いずれの設定方法についても公式文書に詳細に説明がありますのでご参照下さい。

なお筆者は default ルールを無効化することはほぼなく、あるとしても局所的に tags に skip_ansible_lint を指定するなどで対応しています。 そしてほとんどの場合 default ルールだけでは十分なチェックとはならず、カスタムルールを実装、パッケージ化し追加しています。

yamllint の記事でもふれましたが default ルールには ansible 利用者の経験からくる知見に基づく根拠のあるものがつまっています。 何か警告が出たら設定でとにかくそれを表示させなくするのではなく、まず修正を試みましょう。 ほとんどの場合ルールによる警告などの指摘は正しく、無効化の必要はないはずです。

[3]各ルールは python のコードなので、その中で設定ファイルを読込み、細かな挙動を変えるといった独自の仕組みを実装することはできます。

ansible-lint によるチェックを CI に組み込む

実際、筆者は yamllint と同様に ansible-lint を直接実行することはほぼなく、CI の中か tox (molecule) 経由で実行することがほとんどです。 おすすめは role のテストも実装し molecule を使う方法ですが、すぐには難しい場合は先にあげた例のように tox.ini の commands に列挙されている一部に yamllint . 行を例えば追加し、tox 経由で実行するのが良いでしょう。

tox 経由で実行されるようにしてあれば yamllint と同様に意識することなく CI または tox 実行で一緒に実行されるようにできます。

また公式文書で示されているように pre-commit を使うのも良いでしょう

次回予告

次回の記事までは少し期間が空くかもしれませんが Ansible Lint のカスタムルール実装の方法とパッケージ化の実際について簡単な説明を予定しています。