TT Lab
はじめる
学ぶ 学習パス コース

Ansible実戦

ルール・プロファイル・例外、そして関門

TT Labで続きを見る

一言でいうと

--syntax-checkはYAMLとして成り立つかまでしか見ず、品質はansible-lintが見ます。そして、リントを長く生かしておく秘訣は、ルールをたくさん有効にすることではなく、例外を最も狭い範囲に置くことです。

なぜ必要なのか

Ansibleが危険な理由は、間違って書いたプレイブックも緑色で終わるからです。名前のないタスクも、パイプの入ったシェルも、権限を決めていないファイル書き込みも、すべてokやchangedで終わります。問題は半年後に来ます。

リントは、この半年をコミットの直前に前倒しします。ところが、有効にした瞬間、チームはすぐに次の問題に出会います。古いリポジトリにリントを掛けると、指摘が数百個出て、そのうちの何個かには、本当に例外が必要です。そのとき例外をどう置くかが、リントが半年後も生き残っているかどうかを決めます。

どう動くのか

層が違う

ツール 見るもの 見えないもの
--syntax-check YAMLの構造、プレイ・タスクの形、モジュール名が実在するか 品質、冪等性、値が妥当か
ansible-lint 名前・冪等性・権限・FQCNのような規約 実行時点の値
assertタスク 値が妥当か(ポートの範囲、環境名) 静的な規約
molecule ロールを実際に立ち上げて、収束・冪等性・検証のシナリオを見る 上の3つの代わりにはならない

下にいくほどコストが高くなります。そのため、ゲートは安いものから実行します。

ルールidとプロファイル

ansible-lintの指摘には、常にルールidが付きます。name[play]のように、角括弧で細目を分けることもあります。以下は、このコースのラボイメージに入っているバージョン(6.17.2)で、実際に出るものです。

ルールid 何を捕まえるか
name[play] 名前のないプレイ
name[missing] 名前のないタスク
name[casing] 小文字で始まる名前
no-free-form copy: src=a dest=bのように、1行で書いた呼び出し
no-changed-when 状態を変えうるコマンドに、報告の基準がない
risky-shell-pipe パイプを使いながらpipefailを立てていない
risky-file-permissions ファイルを作りながらmodeを決めていない
command-instead-of-module 専用のモジュールがあるコマンドを、シェルで呼んでいる
fqcn[action-core] 短いモジュール名

ルールはプロファイルにまとめられています。min・basic・moderate・safety・shared・productionの順に、上にいくほど厳しく、上のプロファイルは下のルールをすべて含みます。そのため、古いリポジトリにリントを導入するときは、一度にproductionへ行きません。basicで掛けてCIを緑にし、翌月にmoderateへ上げます。プロファイルがある理由が、この移行パスです。

バージョンが違えば、ルール名も違います。そのため、常にansible-lint -Lで自分のバージョンの一覧を先に見ます。

例外を置く2つの場所

- name: Pack the release bundle  # noqa: command-instead-of-module
  ansible.builtin.command: tar -czf /tmp/rel.tgz -C /srv app.conf
  changed_when: false

# noqa: <규칙id>(プレースホルダーはルールidです)は、そのタスクだけをそのルールから外します。隣に残っているので、レビューで「なぜ外したのか」を尋ねられ、理由がなくなれば消せます。

# .ansible-lint
profile: production
exclude_paths:
  - legacy/
skip_list:
  - name[casing]

skip_listは、リポジトリ全体でそのルールをオフにします。チームが合意した規約(例: 製品名が小文字で始まるタスク名を許可する)にだけ使います。指摘が多いからと、ここにルールをどっさり入れると、残るのは「リントが有効になっている」という錯覚だけです。

exclude_pathsはまた別です。ルールをオフにするのではなく、そのパスをそもそも走査しないものです。他人が作ったコードや、教えるために残してある悪い例を入れます。ただし、ファイル名を直接引数に渡すと、この一覧は無視されます。除外は「走査するとき」のルールです。

設定ファイルを置く本当の理由は、利便性ではなく、人の手とCIが同じルールで動くようにすることです。CIでだけ--profile productionを渡すと、開発者は通ったと思ってプッシュし、CIで落ちます。

リントが見えない場所: assert

リントは静的な規約を見ます。ところが、事故は値からも起きます。ポートに80が入ってくる、環境名にタイプミスがある、レプリカ数が0になる、といったことです。これは実行時にしかわからないので、ansible.builtin.assertで、プレイブック自身に問い合わせさせます。

- name: Assert that the port is usable
  ansible.builtin.assert:
    that:
      - app_port is integer
      - app_port >= 1024
    fail_msg: "app_port must be an integer of 1024 or above, got {{ app_port }}"

大切なことが2つあります。1つ目は、何かを変える前に確認することです。半分くらいデプロイして止まると、元に戻す作業がはるかに高くつきます。2つ目は、fail_msgを必ず書くことです。ないと、失敗メッセージが条件式の原文で出力され、受け取った人が何を直せばよいのかわかりません。そして、-eで渡した値は、特に指定しなければ文字列です。is integerがなぜ偽になるのかが、ここで分かれます。

moleculeが追加でやってくれること

Moleculeは、ロールをテストするための枠組みです。シナリオごとに対象を立ち上げ(Docker・Podman・クラウド)、ロールを適用し、もう1回適用してchanged=0かどうか(冪等性)を確認し、検証用のプレイブックで結果を確認し、片づけます。リントが見えない「本当に収束するか」を、自動で見ます。

このラボイメージにはMoleculeが入っていません。ラボのPodはインターネットがなく、インストールもできず、コンテナを立ち上げることも禁止されています。そのため、このモジュールのラボではMoleculeを外し、その前段(構文・リント・前提条件)だけを整えます。Moleculeを使うチームでも、この前段はそのまま必要です。Moleculeは数分かかり、構文チェックは1秒もかからないからです。

現場での姿

1つ目は、導入はいつも「全部直す」ではなく「これ以上悪くしない」から始まることです。低いプロファイルで掛けてCIを緑にし、新しいコードにだけ高い基準を適用し、古いパスはexclude_pathsに入れておいて、手を入れるときに取り出します。

2つ目は、no-changed-whenの指摘の半分は、本当の欠陥だということです。参照コマンドにchanged_when: falseを付けるのは、形式を合わせる作業ではなく、レポートを正直にする作業です。常にchangedになるプレイブックは、本当に何かが変わった日を隠します。

3つ目は、ゲートスクリプトは、必ず実際に止めるところまで確認することです。検査だけして常に0で終わるスクリプトが、実際によくあります。そのようなゲートは、ないよりも悪いものです。検査しているという錯覚を作るからです。作った日に、わざと悪い入力を与えて、0以外の値で終わるかどうかを見るのが、そのスクリプトの最初のテストです。

4つ目は、例外には期限があるということです。# noqaを付けるとき、なぜ付けたのかを同じ行かすぐ上に1行で書いておけば、半年後にその理由がなくなったときに、消せます。理由が書かれていない例外は、永遠に残ります。

参考ドキュメント

次のラボですること

構文チェックを通過する悪いプレイブックをわざと書き、リントが捕まえるルールidを抜き出して、一覧として残します。同じ作業をするきれいなプレイブックを、basicプロファイルまで引き上げたあと、FQCNとmodeを加えてproductionまで上げます。モジュールのないコマンド1つに# noqaで1行だけ例外を置き、.ansible-lintにプロファイルと除外パスとスキップするルールを書いて、リポジトリ全体を一度に通します。assertで前提条件を止め、最後に、構文・リント・前提条件を一度に見るゲートスクリプトを作って、悪いディレクトリを与えたときに本当に止まるかまで確認します。