Ansibleは、多数のサーバーや複数のクラウドインフラを統一的に制御できるオープンソースの構成管理ツールである。一般に人気のあるPuppetやChefなどを置き換えられる構成管理ツールとして、最近は採用する企業が増えている。
Ansibleでは、PlaybookというYAML形式のテキストファイルに定型業務をタスクとして記述し、それをAnsibleで実行させることで多様な処理を実現する。タスクはモジュールと呼ばれる処理プログラムと紐付いており、サーバーの構成管理だけではなく、ネットワークやロードバランサ、クラウドインフラなどの制御も可能だ。
Ansibleでの一連の処理はPlaybookという単位にまとめられている。Playbookは一般的なYAML形式のテキストファイルで記述されるので、プログラミング言語を用いる他の構成管理ツールと比べ、可読性が高く、学習が容易というメリットがある。
ただしAnsible Playbookでは属性のみでなくロジックもYAMLで表すため、一般的なプログラム言語でできることがそのまま実行できるとは限らない。そこで本稿では、Ansibleではどのようにロジックを扱えるのかという観点から、Ansible Playbookを記述するうえでのTipsを紹介する。
なお、PlaybookやInventoryの基本的な構造やYAML構造に関してはあまり触れていないので、注意してほしい。
PlaybookでのYAMLフォーマット
以下のようなタスクを例にして、同じ内容を他の書き方で表してみる。
YAMLフロースタイルは、インデントを使用せず、{}や[]を用いて、ハッシュや配列を記載する書式である。ブロックスタイルでのハッシュの値部分をフロースタイルで記述することで、大枠は可読性の高いブロックスタイルのまま、改行を減らせる。正規のYAML書式であり、yqなどのYAMLをフォーマットするユーティリティによっても正しく処理される。
初期のAnsibleでは=を用いて属性を指定する書式が使用されており、現在も使用できる。=を用いている箇所は、YAMLとしては単なる文字列として扱われ、フォーマッターによるフォーマット対象外となる。 -は以下のように、それのみで行を変えても問題ない。
このように、-のみの行を分離して記述することが可能である。分離しておくと、たとえばあとからname:やwhen:などを追加する場合、既存の行の変更が不要となる。
また以下のように、when:などは先頭でも問題ない。
タスクに付けられるname:やwhen:などは、タスク内にどのような順序で記載してもよい。when:などは先頭近くに置いたほうが、読むときに理解しやすいだろう。
ハッシュの値として“|”, “>”といった記号を記述すると、以下に続くインデントされた連続のブロックが文字列として受け渡される。“|”は各行の改行を含み、“>”は各行を1スペースで連結する。またこれらに“-”を追加した“|-”、“>-”を使用することで、最後の改行を取り除ける。
冪等性担保のための
事前情報収拾タスク
とくにshellやcommandモジュールなどで、実行するか省略するかを判断するために、事前に別途、情報収拾タスクを実行するような実装がよく見られる。その情報収拾タスクは、変更を行わないロジックとしたうえで、changed_when: false、failed_when: false、check_mode: falseを指定し、実処理ではrcなどで判断させるような実装が推奨される。
failed_when: falseの代わりに、ignore_errors: trueを使用する実装も可能である。しかし、Playbook適用時に問題がなくてもエラーがカウントされ得ることになり、混乱しやすい。check_mode: falseは、-checkモード(Dry run)時に実処理の実行可否判断を行うのに必要である。
変数の設定
play varsの有効範囲はplay、block varsはblock、task varsはtaskとなる。これらの「プレイブックオブジェクトスコープ」変数は、後続のオブジェクトでは使用できない。
一方、set_factやインベントリーにより設定された変数は、ホストスコープ変数と呼ばれ、すべてのPlaybookで利用できる。
{% %}や{{ }}は、Python用のテンプレートエンジンであるJinja2により処理される部分であることを意味する。 {% %}は、set文による値の設定、for文、if文などの制御文に使用され、{{ }}は値の参照に使用される。{%- -%}や{{- -}}などのように、“-”を付けることが可能で、その場合、その外側の空白や改行を取り除く。
ほかにもインベントリーやgroup_varsなど、変数を設定可能な箇所は多数存在する( 変数設定箇所およびその優先順位の詳細)。
変数の参照
前述したように、Ansibleでの変数VARの参照は、Jinja2を用いて、{{ VAR }}により行う。ここでは演算やフィルターが利用できる。フィルターとは、[0,1,2]|sumのsumのように、“|”の前の値に対する操作であり、多くのフィルターが提供されている(Filters – Ansible Documentation、Template Designer Documentation: List of Builtin Filters )。
Jinja2による条件分岐 (if、elif、else、endif)
値部分に、Jinja2テンプレートエンジンによるif文を使用できる。ただし、Playbookでのロジックを表す部分の構文をjinja2で変更することはできない。
Jinja2によるループ (for、endfor)
Jinja2によるforループの例を見てみよう。forループを用いて、以下のように配列を直接作成する。
ここでは、“[“, “]”の間に、forループより、文字列として“0,”“2,”“4,”を追加し、文字列“[0,2,4,]”を作成する。“,”が余計に付いているが、(JSONとは異なり)YAMLでは問題なく、配列[0,2,4]として解釈される。
次にforループの内側で配列へ追加し、最後に作成された配列を出力する例を見てみよう。
forループ内でsetにより設定した値をforループ外に持ち出すには、namespaceを使用する必要がある(class namespace)。
jinja2によるvarsの関数的な利用
これは変数xの値によって、変数yや変数zの値が変更される例である。task varsでxに1を設定すると、zは、z=(x+2)+3より6となる。x=1の設定はtask変数なので、次のタスクでは失われ、play varsのx: 5が有効となり、z=(x+2)+3よりzは10となる。
以下は、if文を使用する例である。
fxの値は、xが正の場合には2xに、xが0もしくは負の場合には0となる。
Ansibleによる条件分岐
以下を例にして、環境ごとに導入パッケージ名を変更する条件分岐を検討する。
when:を使用することで、タスク単位の実行可否を判断できる。when:の条件はand、or、notを用いて操作できる。配列に含まれるかどうか、文字列が含まれるかどうかを判断するPythonのin演算子なども使用できる。
blockに対してwhen:を使用することで、複数タスクへのwhen:をまとめたり、条件分岐をネストさせることが可能である。
Ansibleによる単一のタスクに対するループ(loop)
loop:は、与えられた配列の要素ごとにループを行う。ループ変数は、デフォルトでは {{ item }} が使用される。ループさせる要素をハッシュなどにすることで、多くの属性をまとめて設定できる。
Ansibleでは、loop:以外にも、with_items:などwith_で始まるいくつかのloop構文が存在するが、適切なフィルターと組み合わせることで、loop:に置き換えられる(Loops)。
Ansibleのループ(loop)からのbreak
ある項目以降は、すべてwhen:によりスキップさせることが可能である。ただしその条件に処理本体の結果を使用できないので、あまり使用する場面はないかもしれない。
この場合、itemに0から9までを設定してループが実行されるが、5以上ではskipとなる。
Ansibleによる単一のタスクに対するループ(until)
この例では、shellタスクのstdoutが0以下になるまで、もしくは最大でretries:指定の10回まで処理を繰り返す。retries:は省略可能だが、デフォルトは3回なので注意が必要である。条件には、コマンドのstdout、stderr、rcなどが使用可能である。コマンド実行結果を判断に使用するため、until:の条件を満たしていても最初の1回は実行される。
通常のプログラミング言語でのwhileループ、すなわち初回実行に関しても条件による実施判断を行わせるには、たとえば以下のように事前に情報収拾し、when:により判断させる対応が可能である。
untilループでは、breakはuntil:の条件を満たすことで対応可能である。たとえば上記の場合、echo 0; exit などとすればこのタスクを抜けられる。
ブロックに対するループ
loop:などは個別のタスクに対して実行するものであり、taskを集めたblock:に対しては実行できない。
またblockに対するループができないので、多重ループに関しては、通常の実装であると個別のタスクに複数のloop:を定義する必要が生じ、結果的には多重ループを実現できないことになる。
そのため必要に応じて、以下のような対応が必要となる。
❶別ファイルに分けたロジックを呼び出す
(include_tasks:などに対してループを行う)
(include_tasks:などに対してループを行う)
❷別のタスクをループさせる
(多重ループを目的としたケースを除く)
(多重ループを目的としたケースを除く)
ハッシュデータに対する多重loopの例
以下のsample_data.ymlから、iniファイル設定を行う例を考える。
ハッシュの階層ごとにループを行う例
include_tasks:に対してループを実行する。path.keyがファイルのパス名で、path.valueが属性になる。なおwith_dict: “{{ ini_data }}”は、loop: “{{ ini_data|dict2items }}”と書いてもよい。
include_tasks:に対して、以下のようにループを実行する。section.keyがセクション名で、section.valueがオプション名および値となる。
最後に、ini_fileモジュールに対してループを実行する。option.keyがオプション名で、option.valueが値となる。
以上、Ansible Playbookの書き方に関するtipsを見てきた。ロジックの表現には若干不向きなAnsibleであるが、シンプルな処理であれば、大きな効果をもたらす構成管理ツールである。適材適所で使用していくことを推奨したい。
著者|
小野寺 洋之氏
日本アイ・ビー・エム システムズ・エンジニアリング株式会社
オートメーション・プラットフォーム
シニアITスペシャリスト
オートメーション・プラットフォーム
シニアITスペシャリスト
1989年、日本IBM入社。1991年からPOWERプラットフォーム(当時はRS/6000)に関するフィールド・サポートを実施。その後日本アイ・ビー・エム システムズ・エンジニアリングに出向し、フィールド・サポートに加えてインフラ構築等プロジェクトサポートを実施。Chef、Ansible等による自動化、クラスタソフトウェア(PowerHA、TSAMP)をサポートしている。
[IS magazine No.27(2020年5月)掲載]