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

Ansible実戦

動かす前に止める — 構文・lint・前提条件を一つの関門に

TT Labで続きを見る

目標

構文チェックが通してしまう悪いプレイブックから出発して、ansible-lintのルールとプロファイルで1層ずつ引き上げ、例外を最も狭い範囲に置く方法を身につけ、assertで前提条件を先に止め、この3つを1つのゲートスクリプトにまとめます。

なぜ重要なのか

Ansibleの危険な点は、間違って書いたプレイブックもうまく動いてしまうことです。名前のないタスクも、パイプの入ったシェルも、権限を決めていないファイル書き込みも、すべて緑色で終わります。問題は半年後に来ます。レポートが常にchangedなので誰も読まなくなり、サーバーごとにファイルの権限が変わり、失敗したタスクの名前がshellなので、ログを見ても何が失敗したのかわかりません。リントは、その半年をコミットの直前に前倒しします。ただし、リントを有効にした瞬間、チームはすぐに次の問題に出会います。指摘が数百個出て、そのうちの何個かには、本当に例外が必要です。そのとき、リポジトリ全体でルールをオフにすることと、1行だけ外すことを区別できないと、リントは1か月で形骸化します。このラボは、その区別と、リントが見えないもの(値が妥当か)をassertで止める場所まで、一緒に整えます。

ステップ

  1. /root/anslint/ansible.cfgのデフォルトのインベントリを./inventory/hosts.iniにし、そのファイルにwebグループのweb1・web2を書いてください(2つともansible_host=127.0.0.1、ansible_port=2222で、[all:vars]のansible_userはrootです)。/root/anslint/messy.ymlには、名前のないプレイ1つにタスクを3つ書きます。名前がなくパイプの入ったshellタスク、小文字で始まる名前のcommand: mkdir -pタスク、copy: content=... dest=...のように1行で書いたタスクです。ansible-playbook --syntax-check messy.ymlを実行して、出力を/root/anslint/out/syntax.txtに保存してください。
  2. ansible-lint -f pep8 messy.ymlの出力を/root/anslint/out/lint_before.txtに保存してください(1行に指摘が1つずつ出る形式です)。そのあと、同じファイルをJSON形式でリントして、引っかかったルールidだけを、重複なしで辞書順に1行ずつ、/root/anslint/out/rules.txtに保存してください。
  3. /root/anslint/site.ymlを新しく書いてください。名前のあるプレイ1つに、名前のあるタスクが3つです。1つ目は/root/anslint/out/dataディレクトリを作成し、2つ目は/root/anslint/out/app.confにport=8080の1行を書き、3つ目は/root/anslint/out/upper-<호스트이름>.txt(プレースホルダーはホスト名です)にそのホスト名を大文字で1行書きます。シェルコマンドは1つも使いません。ansible-lint --profile basic site.ymlが通る必要があり、プレイブックを実際に実行して、出力を/root/anslint/out/run.txtに保存してください。
  4. /root/anslint/site.ymlのすべてのモジュールをFQCN(ansible.builtin.<모듈>、プレースホルダーはモジュールです)に変え、ファイルとディレクトリを作るタスクごとにmodeを書いてください。ansible-lint --profile production site.ymlが通る必要があり、その出力を/root/anslint/out/lint_production.txtに保存してください。
  5. /root/anslint/site.ymlに4つ目のタスクを加えてください。ansible.builtin.commandでtar -czf /root/anslint/out/bundle.tgz -C /root/anslint/out app.confを実行し、changed_when: falseを付けます。リントはこのタスクをcommand-instead-of-moduleとして捕まえますが、unarchiveモジュールは展開するだけで、まとめることはできないので、ここではシェルコマンドが適切です。その1行だけをルールから外すコメントを付けて、--profile productionをもう一度通し、プレイブックをもう一度実行して、アーカイブを実際に作成してください。
  6. /root/anslint/site.ymlに5つ目のタスクを加えてください。名前は小文字で始まるnginx health probeで、/root/anslint/out/health.txtにokの1行を書きます。そして、/root/anslint/.ansible-lintを作成して、profile: production、exclude_pathsにmessy.ymlとout/、skip_listにname[casing]を書いてください。引数なしでansible-lintを実行して、ディレクトリ全体が通ることを確認し、出力を/root/anslint/out/lint_repo.txtに保存してください。
  7. /root/anslint/checks.ymlを作成してください。localhostでファクトを集めず、プレイの変数としてapp_port: 8080、app_env: staging、allowed_envs: [staging, prod]を置きます。最初のタスクは、app_portが整数で、1024以上65535以下かを確認し、2つ目のタスクは、app_envがallowed_envsの中にあるかを確認します。どちらもansible.builtin.assertで書いて、fail_msgとsuccess_msgを付けます。デフォルトのまま実行した出力を/root/anslint/out/assert_ok.txtに、-e app_port=80で上書きして実行した出力を/root/anslint/out/assert_fail.txtに保存してください。
  8. /root/anslint/gate.shを作成してください。最初の引数で受け取ったディレクトリ(デフォルトはカレントディレクトリ)に移動して、3つを順に確認します。そのディレクトリの直下の*.ymlごとに構文チェックを行って、OK syntax <파일>(プレースホルダーはファイルです)またはFAIL syntax <파일>を出力し、引数なしでansible-lintを実行して、OK lintまたはFAIL lintを出力し、checks.ymlがあればそれを実行して、OK assertまたはFAIL assertを出力します。1つでも失敗したら、0以外の値で終了します。このゲートを今のディレクトリで実行して、出力を/root/anslint/out/gate.txtに保存してください。

参考

構文チェックを通過する悪いプレイブック

/root/anslint/ansible.cfgのデフォルトのインベントリを./inventory/hosts.iniにし、そのファイルにwebグループのweb1・web2を書いてください(2つともansible_host=127.0.0.1、ansible_port=2222で、[all:vars]のansible_userはrootです)。/root/anslint/messy.ymlには、名前のないプレイ1つにタスクを3つ書きます。名前がなくパイプの入ったshellタスク、小文字で始まる名前のcommand: mkdir -pタスク、copy: content=... dest=...のように1行で書いたタスクです。ansible-playbook --syntax-check messy.ymlを実行して、出力を/root/anslint/out/syntax.txtに保存してください。

--syntax-checkは、YAMLを読んで、プレイ・タスクの構造が成り立つか、モジュール名が実在するかまでしか見ません。それ以上は見ません。冪等でないコマンドも、名前のないタスクも、権限を決めていないファイル書き込みも、すべて通過します。このステップの目的は、構文チェックを信じてはいけない理由を目で見ることです。わざと悪く書いてください。

リントが何を捕まえるかをルールidで数える

ansible-lint -f pep8 messy.ymlの出力を/root/anslint/out/lint_before.txtに保存してください(1行に指摘が1つずつ出る形式です)。そのあと、同じファイルをJSON形式でリントして、引っかかったルールidだけを、重複なしで辞書順に1行ずつ、/root/anslint/out/rules.txtに保存してください。

ansible-lintの出力形式は-fで選びます。人が読むデフォルトの形式、1行に1つのpep8、ツールが読むjsonがあります。JSONの各項目には、ルールidがcheck_nameという名前で入っています。jqとsort -uで抜き出せば構いません。ルールidは、name[play]のように角括弧で細目を区別します。同じnameルールでも、どの場所に引っかかったのかをidで知ることができます。リントは違反を見つけると0以外の値で終了するので、保存するときにその点を考慮してください。

basicプロファイルまで引き上げる

/root/anslint/site.ymlを新しく書いてください。名前のあるプレイ1つに、名前のあるタスクが3つです。1つ目は/root/anslint/out/dataディレクトリを作成し、2つ目は/root/anslint/out/app.confにport=8080の1行を書き、3つ目は/root/anslint/out/upper-<호스트이름>.txt(プレースホルダーはホスト名です)にそのホスト名を大文字で1行書きます。シェルコマンドは1つも使いません。ansible-lint --profile basic site.ymlが通る必要があり、プレイブックを実際に実行して、出力を/root/anslint/out/run.txtに保存してください。

プロファイルは、ルールをまとめた層です。min・basic・moderate・safety・shared・productionの順に、上にいくほど厳しくなり、上のプロファイルは、下のプロファイルのルールをすべて含みます。一度にproductionまで行こうとせず、1層ずつ上げるのが実務の順序です。basicが捕まえるのは、主に「名前がない」と「1行のフリーフォームで書いた」です。シェルコマンドをモジュールに変えれば、no-changed-whenも一緒になくなります。

productionプロファイルがさらに求める2つのこと

/root/anslint/site.ymlのすべてのモジュールをFQCN(ansible.builtin.<모듈>、プレースホルダーはモジュールです)に変え、ファイルとディレクトリを作るタスクごとにmodeを書いてください。ansible-lint --profile production site.ymlが通る必要があり、その出力を/root/anslint/out/lint_production.txtに保存してください。

productionプロファイルが追加で求めるものの中で、最もよくぶつかるのがこの2つです。FQCNは名前の衝突を防ぎます。コレクションが増えると、copyという名前があちこちにでき、短い名前は検索パスの順序によって、別のモジュールになりえます。modeを書くというルールは、権限が運任せになることを防ぎます。書かなければ対象のumaskが決めますが、その値はサーバーごとに違います。どのルールがどのプロファイルに属するかは、リントの出力の最後のサマリー表に出ます。

ルールは生かしたまま、1行だけ外す

/root/anslint/site.ymlに4つ目のタスクを加えてください。ansible.builtin.commandでtar -czf /root/anslint/out/bundle.tgz -C /root/anslint/out app.confを実行し、changed_when: falseを付けます。リントはこのタスクをcommand-instead-of-moduleとして捕まえますが、unarchiveモジュールは展開するだけで、まとめることはできないので、ここではシェルコマンドが適切です。その1行だけをルールから外すコメントを付けて、--profile productionをもう一度通し、プレイブックをもう一度実行して、アーカイブを実際に作成してください。

# noqa: <규칙id>(プレースホルダーはルールidです)をタスクのどの行にでも付ければ、そのタスクだけがそのルールから外れます。複数のルールを外すときは、空白で並べます。これと設定ファイルのskip_listは、性格がまったく違います。noqaは「ここだけ例外」なので、隣の人がレビューで理由を尋ねられ、skip_listは「リポジトリ全体でこのルールは見ない」なので、そのルールが事実上なくなります。例外を置くときは、常に最も狭い範囲を選びます。今引っかかったルールidは、ステップ2で抜き出した一覧と同じ形です。

リポジトリ全体に掛かるルールを設定ファイルに書く

/root/anslint/site.ymlに5つ目のタスクを加えてください。名前は小文字で始まるnginx health probeで、/root/anslint/out/health.txtにokの1行を書きます。そして、/root/anslint/.ansible-lintを作成して、profile: production、exclude_pathsにmessy.ymlとout/、skip_listにname[casing]を書いてください。引数なしでansible-lintを実行して、ディレクトリ全体が通ることを確認し、出力を/root/anslint/out/lint_repo.txtに保存してください。

設定ファイルがあれば、--profileを毎回手で渡さなくてもよく、CIと人の手が同じルールで動きます。これが設定ファイルを置く本当の理由です。exclude_pathsは「このパスはそもそも見るな」です。教えるために残してある悪い例や、他人が作ったコードを入れます。ただし、ファイル名を直接引数に渡すと、除外の一覧は無視されます。除外は「走査するとき」のルールです。skip_listに入れるのは、チームが合意した例外である必要があります。ここでは、製品名が小文字で始まるタスク名を使うことにした、と考えれば構いません。

値が妥当かどうかを、プレイブック自身に問い合わせさせる

/root/anslint/checks.ymlを作成してください。localhostでファクトを集めず、プレイの変数としてapp_port: 8080、app_env: staging、allowed_envs: [staging, prod]を置きます。最初のタスクは、app_portが整数で、1024以上65535以下かを確認し、2つ目のタスクは、app_envがallowed_envsの中にあるかを確認します。どちらもansible.builtin.assertで書いて、fail_msgとsuccess_msgを付けます。デフォルトのまま実行した出力を/root/anslint/out/assert_ok.txtに、-e app_port=80で上書きして実行した出力を/root/anslint/out/assert_fail.txtに保存してください。

assertは、thatに書いた条件がすべて真かどうかを見ます。1つでも偽なら、そのホストでプレイが止まります。これが目的です。前提条件は、何かを変える前に確認する必要があります。半分くらいデプロイしてから止まると、元に戻す作業がはるかに高くつきます。fail_msgを書かないと、失敗メッセージが条件式の原文で出力され、受け取った人が何を直せばよいのかわかりません。-eで渡した値は、特に指定しなければ文字列です。is integerがなぜ偽になるのか、ここでわかります。失敗する実行は0以外の値で終了するので、出力を保存するときにその点を考慮してください。

3つの層を1つのゲートにまとめる

/root/anslint/gate.shを作成してください。最初の引数で受け取ったディレクトリ(デフォルトはカレントディレクトリ)に移動して、3つを順に確認します。そのディレクトリの直下の*.ymlごとに構文チェックを行って、OK syntax <파일>(プレースホルダーはファイルです)またはFAIL syntax <파일>を出力し、引数なしでansible-lintを実行して、OK lintまたはFAIL lintを出力し、checks.ymlがあればそれを実行して、OK assertまたはFAIL assertを出力します。1つでも失敗したら、0以外の値で終了します。このゲートを今のディレクトリで実行して、出力を/root/anslint/out/gate.txtに保存してください。

ゲートの価値は止めることにあります。通すだけで常に0で終わるスクリプトは、ないのと同じで、むしろ「検査している」という錯覚を作るので、もっと悪いです。そのため、作ったあとには、必ず悪い入力を与えて確認する必要があります。一時ディレクトリにわざと悪いプレイブックを1つ置き、そのディレクトリを引数に渡してみてください。3つの層をこの順序に置く理由もあります。構文チェックは1秒もかからず、リントは数秒、前提条件の検査は実際にAnsibleを動かします。安いものから実行してこそ、間違ったコミットが早く差し戻されます。