blog.manj.io

ごく稀に更新

ロールを安全に実行するために冒頭にassertを書こう

Qiitaに書いた記事のバックアップです。

qiita.com


前提: What is Role?

Ansibleでは、複数のtaskをまとめて実行するための実行単位としてRoleという仕組みをサポートしています。

Roleでは以下のことが可能で、「OOというアプリをインストールする」とか「OOの設定を行なう」というような単位でtaskを分類するのに便利な機能です。

  • 複数のtaskを任意の名前空間(=Role名)で区切ること
  • role内で使用する変数のデフォルト値を設定すること (defaults / vars)
  • 特定のモジュールから使用するファイルを相対パスで参照できるようにすること (templates / files)

Ansible Best Practice でも playbook -> role -> tasksという階層構造を持たせることを推奨しており、role単位で一般外部に自作tasksを公開するansible-galaxyというシステムも用意されていることから、Ansibleの中で最も触れる機会が多い機能の一つではないかと思います。

defaultsについて

roleの機能の中でも特徴的なのが、role毎に「変数のデフォルト値」を設定できる機能です。 これは使いどころを間違えなければとても便利で、

  • defaultsに定義した変数 = role内で使われる変数、という前提を置くことで、roleのspecの一部を示すことができる
  • そのRole内でしか使用されない、という前提を置くことで、定数定義に用いることができる
  • Jinja2を併用することで、(外部で定義されていることを期待した)変数群を操作して新しい変数を定義する、という操作をset_factを使わずに表現することができる

のような使い方が可能です。

Role実行時に起こりがちなトラブル

AnsibleにおいてRoleはtaskを参照するための名前を分離する機能であり、実行単位を保証する機能ではありません。 いいかえると、Roleに含まれるtaskをまとめてアトミックに実行するわけではなく、一部のtaskが失敗すると中途半端な形でAnsibleが終了することがあります。

中途半端にroleが終了することで

  • バイナリは配置されたがconfig等が配置されない
  • serviceファイルは配置したがそれが実行できる状態ではない

みたいな事態が起こり得ます。

deb/rpmのインストール時にconfigファイルをデフォルトのもので置き換えるような挙動をするものもあるので、上記のような事態が発生することでアプリのアップデートなどのユースケースで致命的な障害を発生させてしまうかもしれません。

taskが失敗する原因は色々ありますが、経験上9割くらいは変数参照のトラブルです。以下に例を挙げます。

どこかで同名の変数が定義されていたことによる誤動作

defaultsに設定された変数は、Ansibleの仕様上使用の優先度が最低に設定されています。 つまり、別のroleでset_factされた変数やgroup_vars等で設定された変数と名前が重複すると、その変数は上書きされてしまいます。

一般に使われやすい変数名でRoleの定数を定義しているときに、「どこかで同名の変数が設定されている」という事態が起こり得ます。 これによってある変数をjinja filterにかけるときにエラーが発生したり、不正なconfigを生成してしまうなどのトラブルが発生する恐れがあります。

(最悪の場合、不正なconfigが生成されたことに誰も気づかないままroleが流れきってしまうケースもあります)

評価タイミングによるトラブル

Ansibleの仕様として、変数の評価は参照時に実施されます。これによって、Jinja2構文を用いた変数は参照するタイミングによって評価結果が変わったり、jinja2のエラーによってtask自体が停止することがあり得ます。

defaultsに定義された変数についても、role実行開始時には評価されず、task等で参照されたときに初めて評価されるので、このトラブルが発生する恐れがあります。

Check modeで対策できないか?

これは「場合による」としか言えないです。 checkmodeで検査できるのがもちろん一番いいのですが、

  • git moduleのようにcheckmodeでの動作に絶妙なバグがあるmoduleを使っているtask
  • 開発者が独自にcheck mode時に通らないようにしているtask

などの存在によって、完全にcheck modeを流すのが困難なケースが存在します。

そのような場合でもroleで使用される変数の正当性を確認する手段として、assert moduleを使用することを思い付きました。

assertによる変数チェック

assertはAnsibleのmoduleで、if conditionを列挙するとそのうちの1つでもfalseになったときに失敗する、という機能を持っています。

assert – Asserts given expressions are true

role内で使用する変数全てに対するassertを書くことで、最低でも「undefinedによるエラー」 「jinjaエンコード失敗によるエラー」等の「変数定義が不正なことによるエラー」をtask実行前に検知することができるので、role実行の安全性を高めることができます。

また、Role変数に対する詳細なspecを定義できるので、コードリーディングや機能追加の大きな助けになります。

以下に例を挙げます。

例1: Role内で用いる変数に必須/任意の区別を付ける

空文字やNoneなどをassertで検出することで、間接的に引数の性質を定義することができます。

---
# Required
req_a:
req_b:

# optional
opt_a: value1
opt_b: value2
- name: check vars
  assert:
    that:
    # このケースでは空文字等を許容している
    - req_a is not None
    # 空文字、空リスト等も弾きたい場合
    - req_b
    ...

例2: enumな変数の定義

inを使うことで、enumやboolをspecとして定義することもできます。 これらはdefaults内で enum_a_def みたいな形でリストを定義しておくと見通しがよくなるかもしれません。

- name: check vars
  assert:
    that:
    - enum_a in ["a", "b", "c"]
    - bool_b in [True, False]

例3: 変数の形式を定義

regex_search filterを用いることで、変数に格納されるべき文字列の形式を定義することが可能です。 これも汎用的な表現なら macaddr_def 等の変数に入れてしまう方が見やすいです。

追記: is match(<regexp>) が使えるっぽい

ipアドレスなら ipaddr Filterを通してassertする方法もあります。

- name: check vars
  assert:
    that:
    - mac_addr|regex_search('^[0-9A-F]{2}(:[0-9A-F]{2}){5}$')

まとめ

Role内での変数の取り扱いや変数が使用される優先度について深く理解しておかないと、tasksの実行中に予期せぬエラーが発生する恐れがあります。

Roleの開始時にassertによる変数の検査をすることで、Role外の要因による変数の汚染やtask失敗による中途半端なデプロイを防止することが可能です。

特に例3に挙げた変数を正規表現を用いてチェックする手法は有用です。また、assertを用いることでRole外からの上書きを前提とした変数についてもspecを定義できるので、group_vars等で明示的に指定されるべき変数や、Role間で共通的に使われることを想定した変数の定義ミスによるデプロイ事故を防ぐことができます。

assertをtaskとして定義しなくても変数のspecを書けるといいんですけどね... meta/とかに定義できるような機能があると面白そうなので、気が向いたらコード書いてみようかと思います。

関連

[Ansible] 変数の値が指定の範囲内の数値であることを assert モジュールでチェックする

© 2018 manji0.