Ansible Lintのカスタムルール作成
こんにちは。株式会社SHIFT、自動化エンジニアの水谷です。
相変わらずAnsibleを使いこなせるようになるべく、勉強しているのですが、最近その一環として今回はAnsible Lintのカスタムルールを初めて作成してみました。今回はその中で学んだ、カスタムルールの作り方や、気づいた点などをまとめてみたいと思います。
Ansible Lintとは
Ansible Lintは、AnsibleのPlaybookを静的にチェックしてくれるツールで、一般的な記述ルールだけでなく、これまでに蓄積されたベストプラクティスを元に、Playbookの問題点や変更推奨点を見つけてくれます。
※以前Ansible Lintに関する記事を書いていますので、そちらも合わせてご参照ください。
このAnsible Lintには、デフォルトでたくさんのルールが用意されていて、そのままでもたくさんの項目をチェックしてくれるため、導入してそのまま使用するだけでもPlaybookの質を高める効果があります。さらに、デフォルトでは入っていないルールを自作して、チェックを強化することができます。
また、特にPlaybookを複数のエンジニアが手分けして開発する際には、表記を統一するためのルール作りが必要となってくる場合があります。というのも、Playbookの記述方法には柔軟性があり、同じ動作を定義する場合にも複数の記述方法があったりします。そんな場合は、Ansible Lintでカスタムルールを作成して、これを実行することを必須としておくことで、統一性のあって、後にメンテンナンスのしやすいPlaybookを作成することができます。つまり、Ansible LintはPlaybookの間違いを見つけてくれるだけでなく、記述の統一化のためにも使えるツール、ということになります。
カスタムルール作成の準備
まずは、Ansibe Lintのインストールとバージョンについてです。Ansible Lintのインストールは以下のようにpip(pip3)で行います。
※Ansibleの実行環境はすでに準備できていることを想定しています。
$ pip3 install ansible-lint
また、本記事執筆時の最新バージョンは4.3.7で、本記事はこのバージョンを前提として書いていきます。Ansible Lintのルール記述方法は、バージョンによって異なっていて、下の方で出てくるコードは古いバージョンでは動作しない場合がありますのでご注意ください。
なお、Ansible Lintのバージョン確認は以下のコマンドでできます。
ansible-lint --version
また、アップデートは以下のコマンドでできますので、必要に応じてアップデートしておいてください。
$ pip3 install -U ansible-lint
さて、続いてはカスタムルールを格納するフォルダの作成と、定義ファイルの作成です。
Ansible Lintのルールは、組織全体で複数のプロジェクトにまたがって定義することもできますし、プロジェクト単位で定義することもできます。今回はテスト用の小さなプロジェクト内でのカスタムルールを作成しようと思います。
プロジェクトディレクトリ(今回は"AnsibleTest"とします)内に".ansible-lint"というテキストファイルを作成します。これがAnsible Lintの定義ファイルになります。
このファイルには、Ansible Lintの実行対象から外すファイルや、実行しないルールのリストなどを記述できるのですが、ここではデフォルトのルールを有効にすることと、カスタムルールが格納されているディレクトリの指定だけを下のように記述しました。
use_default_rules: true
rulesdir:
- ./myrules/
つまり、このプロジェクトディレクトリ内の"myrules"ディレクトリにカスタムルールファイルを入れておけば、Ansible Lintが実行してくれることになります。
ちなみに、このプロジェクトディレクトリにはテスト用のPlaybookとして以下のような"site.yml"が存在します。内容としてはChocolateyを使って(Windowsマシンに)FirefoxとChromeをインストールするというとても単純なものです。
---
- hosts: all
tasks:
- name: Install App
win_chocolatey: name={{ item }}
with_items:
- firefox
- googlechrome
カスタムルールの作成
Ansible LintのカスタムルールはPythonで作成します。複雑なカスタムルールの作成はある程度Pythonの知識と経験が必要となりますが、簡単なルールの作成でしたら、Python初心者でも書くことができるレベルだと思います。
カスタムルールは、大きく分けて2種類あります。1つは、行単位でのチェックしていくルール、もう1つはタスク単位でチェックするルールです。
ここでは、まず行単位のチェックで1つルールを作成してみたいと思います。チェックする項目は、ループを記述する"with_items:"があるとエラーにし、"loop:を使いなさい"という旨のメッセージを出力するルールです。現時点ではAnsibleで"with_items:"を使ってはダメというルールはないのですが、最近は"with_items:"ではなく"loop:"を使いましょう、とよく言われていますので、その流れ(?)に乗ったルールということになります。
まずはルールに名前を付けたいと思います。ちょっと長くてセンスもないですが"UseLoopInsteadOfWithItems"というルール名にしました。Ansible Lintでは、"<ルール名>.py"というファイルを作り、その中でルール名のクラスを定義するということになっているようなので、先に名前を決めてしまうのが良いと思います。
"UseLoopInsteadOfWithItems.py"という名のファイルを作成し、下のようなコードを記述しました。
from ansiblelint import AnsibleLintRule
class UseLoopInsteadOfWithItems(AnsibleLintRule):
id = 'MYRULE1'
shortdesc = 'Use loop instead of with_items'
description = 'Use "loop" instead of "with_items" for loop'
tags = ['deprecated']
def match(self, file, line):
return 'with_items:' in line
上から見ていきましょう。
1行目は"AnsibleLintRule"をインポートしています。続いて2行目からが、今インポートしたAnsibleLintRuleをベースにしたクラス定義です。ここでは、まず継承した"id"、"shortdesc"、"description"と"tags"に値を代入しています。idはルールのIDで、Ansible Lintが持つデフォルトのルールでは"301"とか"206"などの番号を文字列にしたものが入っていますが、ここは任意の文字列でOKです。この例では”MYRULE1"としました。shortdescとdescriptionは共にこれがどのようなルールなのかを短い表記と長い表記で記述しています。これらの値はAnsible Lint側から使われて、ログなどにも表示されます。そして、tagsはルールの種類を表すものとなります。例えば表記上のフォーマットに関するチェックであれば"formatting"、可読性に関するチェックであれば"readability"を指定する(複数指定可)のですが、ここでは"with_items:"を使うことを非推奨にする、というルールなので、非推奨を表す"deprecated"としました。
実際のチェックはmatch()メソッドで行います。このメソッドはPlaybookの各行に対してコールされ、引数の"line"にその行の内容(テキスト)が入っています。そして、その行に"with_items:"があればTrueを返すことで、チェックに引っかかったことを表し、なければFalseを返しています。
では、さっそく実行して見ましょう。
$ ansible-lint *.yml
結果は以下のように、with_items:を見つけて警告が表示されました。
さて、この行単位のチェックで"with_items:"を見つけるルールは、実はちょっとした問題があります。というのも、このルールは単純すぎて、間違って警告を出す可能性があります。例えば下のように、ダブルクォーテーションで括ったテキスト内に"with_items:"があっても、警告を出してしまいます(最後の行に注目してください)。
---
- hosts: all
become: yes
tasks:
- name: Install App
win_chocolatey: name={{ item }}
with_items:
- firefox
- googlechrome
- notepadplusplus
- name: dummy
debug:
msg: "with_items: in a text"
実際に実行して見ると、Ansible Lintの実行結果は下のように、最後のテキスト内の"with_items:"にも警告を表示してしまっています。
そこで2つ目のルールの種類である、タスク単位でのルールでこの問題を解決してみましょう。
タスク単位のルールを作成する際は、先ほど使った"match"ではなく"matchtask"メソッドを定義します。このメソッドの3つ目の引数にはタスクの名前や使用するモジュールなどが連想配列の形で渡されますので、これを使って、"with_items:"がタスクの要素の1つとして指定されているかどうかが簡単に判別できます。
作成したルールファイルは以下のものです。
from ansiblelint.rules import AnsibleLintRule
class UseLoopInsteadOfWithItems2(AnsibleLintRule):
id = 'MYRULE2'
shortdesc = 'Use loop instead of with_items'
description = 'Use "loop" instead of "with_items" for loop'
tags = ['deprecated']
def matchtask(self, file, task):
return 'with_items' in task
先程のルールとほとんど変わりませんが、メソッド名が"matchtask"になり、3番目の引数"task"のキーに"with_item"があればTrueを返し(ルール違反の発見)、なければFalseを返しています。
結果は以下のように、テキストの中の"with_items:"については警告が出されず、タスクの要素として"with_items:"が使われている場所だけが警告されました。めでたしめでたし。
なお、matchtaskの引数taskについては、タスク名が"task['name']"で、使用しているモジュール名は"task['action']['__ansible_module__']"で参照できます。もちろんそれ以外のタスク内要素やそれに対する値も取得できますので、これらを使えば、タスクの命名規則のチェックや、特定のモジュールにおける(より複雑な)ルールも記述できることになります。
正直なところ、「カスタムルール」という言葉からなんだか難しそうな印象があり、私はPythonもそれほど詳しくないので、このテーマはこれまで放置していましたw そんなAnsible Lintのカスタムルール作成ですが、実際にやってみればそれほど難しいものではなく、思い通りにルール違反を見つけてくれるところを見るのはちょっと楽しかったりしました。今後はいくつかルールを作成して、(自己満足しつつw)Playbookを作成する際の書式を統一していけるといいな、と思っています。皆さんも、Playbook作成の際は、是非Ansible Lintを活用してみていただければと思います。
――――――――――――――――――――――――――――――――――
お問合せはお気軽に
https://service.shiftinc.jp/contact/
SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/
SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/
SHIFTの導入事例
https://service.shiftinc.jp/case/
お役立ち資料はこちら
https://service.shiftinc.jp/resources/
SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/