【Ansible】メンテナンスしやすいPlaybookの書き方
Playbookは外部ファイルのインポートや条件分岐やループなどの制御構文が使えるため、プログラミングと同様にソースコード品質管理の考え方を活用できます。
本記事では、筆者がPlaybookを作成する際に、後々メンテナンスしやすいように気を付けていることを紹介します。(もっと良い方法がある場合は、コメントいただけると助かります)
インベントリ(Inventory)
ターゲットノードにはホスト名とIPアドレスを書く
インベントリ(Inventory)には、構成管理対象のノード(以下、ターゲットノード)を記載します。IPアドレス、ホスト名(FQDNも可)どちらの書き方も可能ですが、それぞれ以下の問題があります。
- IPアドレス: ターゲットノードが何のサーバなのか分からず可読性が悪い
- ホスト名: Ansibleを動かすサーバがホスト名を名前解決できる必要がある
上記の問題を解決するため、ターゲットノード名はホスト名で指定して、ホスト変数のansible_host
にIPアドレスを指定します。この方法であれば、ホスト名を明示しつつ、接続先IPアドレスを明確に指定できます。
### 改善前
192.168.1.1
192.168.1.2
### 改善後
web01.densan-hoshigumi.com ansible_host=192.168.1.1
web02.densan-hoshigumi.com ansible_host=192.168.1.2
ターゲットノードはサーバ種別でグループにまとめる
Playbookはサーバ種別ごとに分割するので、ターゲットノードも同じ単位でグループ化します。
[web]
web01.densan-hoshigumi.com ansible_host=192.168.1.1
web02.densan-hoshigumi.com ansible_host=192.168.1.2
[ap]
ap01.densan-hoshigumi.com ansible_host=192.168.1.3
ap02.densan-hoshigumi.com ansible_host=192.168.1.4
[db]
db01.densan-hoshigumi.com ansible_host=192.168.1.5
db02.densan-hoshigumi.com ansible_host=192.168.1.6
1台のホストが複数のグループに所属する場合は、ホストの定義とグループの定義箇所を分離します。
### ホスト定義 ###
web01.densan-hoshigumi.com ansible_host=192.168.1.1
web02.densan-hoshigumi.com ansible_host=192.168.1.2
### グループ定義 ###
[web]
web01.densan-hoshigumi.com
web02.densan-hoshigumi.com
[tokyo]
web01.densan-hoshigumi.com
[osaka]
web02.densan-hoshigumi.com
グループ固有の変数はグループ変数に書く
インベントリに定義したグループ単位で、固有値をYAML形式で変数化します。プロジェクトディレクトリ直下にgroup_vars
ディレクトリを作成し、ファイル名は、<グループ名>.yml
とします。all
は、全てのターゲットノードが暗黙的に所属する特殊なグループです。
ファイル構成例は以下の通りです。
<プロジェクトディレクトリ>
└── group_vars
├── all.yml
├── web.yml
├── ap.yml
└── db.yml
ターゲットノードにSSHでリモートアクセスする際のユーザ名を指定する場合は、all.yml
に以下のように指定をします。
ansible_user: ec2-user
認証情報はAnsible Vaultで暗号化する
前述のグループ変数ファイルなどに認証情報を記載する場合、セキュリティの観点からAnsible Vaultを使用して変数を暗号化します。
ansible-vault encrypt_string
コマンドを実行し、復号用のパスワードと暗号化したい文字列から、暗号化された文字列を生成します。
以下は、sampleP@ssword
という文字列を暗号化する例です。
$ ansible-vault encrypt_string
New Vault password: ← 復号用のパスワードを入力
Confirm New Vault password: ← 上記と同様のパスワードを入力
Reading plaintext input from stdin. (ctrl-d to end input, twice if your content does not already have a newline)
sampleP@ssword ← 暗号化したい文字列を入力する。入力を終える際はCtrl + D
!vault |
$ANSIBLE_VAULT;1.1;AES256
63343862653936343231353863376665303066316237646466653439636461346436613231343161
3337643438373462313166386632626462363163663434650a646661643732316538636333393032
30356535336133313832663431373232303139393438313436336336626138326230663035396661
6163346666363761610a626138636335326161313235633631613362363032636165343364343634
6235
Encryption successful
!vault |
の行から始まる暗号化された文字列を変数の値に指定します。
以下は、ansible_password
変数に暗号化した後の文字列を指定する例です。
ansible_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
63343862653936343231353863376665303066316237646466653439636461346436613231343161
3337643438373462313166386632626462363163663434650a646661643732316538636333393032
30356535336133313832663431373232303139393438313436336336626138326230663035396661
6163346666363761610a626138636335326161313235633631613362363032636165343364343634
6235
暗号化された文字列をPlaybookで使用する際は、ansible-playbook
コマンドの引数に--ask-vault-pass
を付与して復号用のパスワードを入力します。
$ ansible-playbook -i hosts playbook.yml --ask-vault-pass
Vault password: ←復号用のパスワードを入力
毎回復号用のパスワードを入力するのが煩雑な場合は、パスワードを記載したファイルを読み込むことも可能です。この場合、ansible-playbook
コマンド実行時に--ask-vault-pass
を指定する必要はありません。
プロジェクトフォルダ直下やホームディレクトリにansible.cfg
ファイルを作成し、デフォルトで読み込むパスワードファイルの場所を指定します。
$ vi ~/ansible.cfg
記載する内容は以下の通りです。以下の例では、ホームディレクトリに配置した.vault-password
をデフォルトで読み込むパスワードファイルに指定しています。
[defaults]
vault_password_file = ~/.vault-password
Playbook
Playbookはサーバ種別で分割する
あるシステムのPlaybookを1つのファイルにまとめてしまうと、他のシステム向けに再利用することが難しくなります。ansible-playbook
コマンドから直接呼び出すトップレベルPlaybookには、サーバ種別ごとのPlaybookをインポートする処理を記載します。具体的なPlaybookの内容は、サーバ種別ごとに分割したPlaybookに記載します。
ファイル構成例は以下の通りです。
<プロジェクトディレクトリ>/
├── site.yml # トップレベルPlaybookファイル
├── web.yml # Webサーバ用Playbookファイル
├── ap.yml # APサーバ用Playbookファイル
└── db.yml # DBサーバ用Playbookファイル
site.yml
の内容例は以下の通りです。
---
- import_playbook: web.yml
- import_playbook: ap.yml
- import_playbook: db.yml
具体的な処理内容はロールに書く
前述のサーバ種別ごとのPlaybookには、直接タスク(Task)やハンドラ(Handler)の処理は書きません。ターゲットノードとロール(Role)のみを指定して、実際の処理内容はロールに移譲します。
以下のPlaybookの例では、web
グループに対して、共通設定(common
)とApacheの設定(apache
)のRoleを実行するよう指定しています。
---
- hosts: web
roles:
- common
- apache
ロール単位でタグを付ける
ロールを指定するときに、roles
ディレクティブにrole
ディレクティブとtags
ディレクティブをネストすることで、メンテナンス時に変更が必要なロールの処理のみ行えます。
---
- hosts: web
roles:
- role: common
tags: common
- role: apache
tags: apache
特定のロールのみ処理を実行するときは、以下のように--tags
オプションでタグを指定してansible-playbook
コマンドを実行します。
$ ansible-playbook --tags "<タグ>" -i <インベントリ> <Playbook>
ロール
変数
設定パラメータは変数を使う
モジュールのアーギュメントや、テンプレートファイル内のパラメータは、直接値を書かずに変数を指定します。
Ansibleで使用可能な変数は種類が多く自由度が高いものの、運用を考えると混乱しがちです。個人的には、後述のインベントリ変数とデフォルト変数の2種類の使用をお勧めします。
インベントリ変数とデフォルト変数を使い分ける
インベントリ変数(host_vars/main.yml
、group_vars/main.yml
)とデフォルト変数(roles/<ロール名>/defaults/main.yml
)は、以下の内容で使い分けます。
- インベントリ変数: ターゲットノード(またはグループ)固有のパラメータ
- 適用パラメータ例: ホスト名、IPアドレス、システム名…etc
- デフォルト変数: 同じ種別のサーバ(ロール)で共通するソフトウェアパラメータ
- 例: ポート番号、設定ファイルの場所、最大接続数…etc
変数名の先頭にロール名を付ける
複数のロールを作成すると変数名が重複することがあります。後でどのロールの変数か区別しやすいように、変数名の先頭にはロール名を付けます。
apache_port: 80
mysql_port: 3306
タスク
タスクで管理する設定項目
サーバ構築業務では、構成を管理するためのドキュメントとして、パラメータシート(または詳細設計書)と呼ばれるものを作成します。このドキュメントには、以下のようなカテゴリの設定情報を記載します。
- ユーザ、グループ
- インストールパッケージ
- サービス起動状態
- ディレクトリ構成
- 各ソフトウェアの設定内容
設定手順書に沿って、パラメータシートの上記内容を実機に投入することで、サーバを構築することができるのです。
タスク(Task)は、この一連の手順を定義したファイルです。
ターゲットノードのあるべき姿(設定)を定義し、あるべき姿でない場合は、タスクに記載の内容で設定変更が行われます。
ロール内にタスクを作成するときは、従来パラメータシートに記載していた項目の設定手順を対象とします。ある程度共通のタスクをベースとして作成し、ロール化するアプリケーションごとに項目を足し引きしていくと効率が良いです。
main.ymlに処理を書かない
tasks
ディレクトリ内のmain.yml
には、直接タスクの処理を書かないようにします。main.yml
と同じ階層に、処理の種別ごとにタスクファイルを作成し、include_tasks
を使ってmain.yml
にインポートします。
ファイル構成例は以下の通りです。
<プロジェクトディレクトリ>
└── roles
└── <ロール名>
├── files
├── handlers
├── tasks
│ |── main.yml # メインタスク
│ |── user_group.yml # ユーザ、グループ管理用タスク
│ |── package.yml # パッケージインストール状態管理用タスク
│ |── directory.yml # ディレクトリ構成管理用タスク
│ |── config.yml # 設定ファイル管理用タスク
│ └── service.yml # サービス起動状態管理用タスク
├── templates
└── vars
main.yml
では、include_tasks
を使用して、同階層の別タスクファイルをインポートします。
---
- include_tasks: user_group.yml
- include_tasks: package.yml
- include_tasks: directory.yml
- include_tasks: config.yml
- include_tasks: service.yml
shellモジュールには冪等性対策を行う
ソフトウェアごとの初期設定は、どうしてもシェル操作に頼らないといけない場面が出てきます。shell
モジュールを使用しつつPlaybookの冪等性を保つために、初期設定済みかチェックする処理を入れて、初期設定が未完了の場合のみ処理を継続するよう実行条件を指定します。
以下の例は、初期設定済みかチェックを行うCheck initialize
タスクを定義しています。初期設定済みか確認するために、定義済みのパスワードを使用してMySQLにログインを試みます。コマンド実行結果は、レジスタ変数としてmysql_login_result
へ格納します。
Execute MySQL initialize
のタスクでは、when
ディレクティブでCheck initialize
の実行結果を判定しています。MySQLへのログインが失敗した場合(戻り値が0以外)は初期設定が未実施と判定し、ブロック内の初期設定タスク群を実行します。MySQLへのログインが成功した場合(戻り値が0の場合)は、ブロック内の処理はスキップします。
---
- name: Check initialize
shell: mysql -uroot -p {{ mysql.root_passwod }}
register: mysql_login_result
changed_when: False
- name: Execute MySQL initialize
block:
- name: Initialize step1
shell: (略)
- name: Initialize step2
shell: (略)
- name: Initialize step3
shell: (略)
when: mysql_login_result.rc != 0
また、ファイルの存在有無でタスクの実行条件を決めることもできます。
creates
にチェックしたいファイルのパスを指定すると、該当ファイルが存在しない場合のみタスクが実行できます。
---
- name: Initialize
shell: /opt/init-script.sh
args:
creates: /opt/foo/bar
1行が長い文字列は複数行に分割する
shellモジュールなどのアーギュメントで長い文字列を扱う場合、リテラルスタイル
と折りたたみスタイル
を使用して1行を複数行に分割することで、可読性をあげることができます。
リテラルスタイル
は、 |-
を文字列の先頭に記載して、残りは改行しながら書いていきます。各行の改行は維持され、最後の改行のみ無視されます。
[改善前]
- shell: echo "hello" ; echo "world"; echo "bye"
[改善後]
- shell: |-
echo "hello"
echo "world"
echo "bye"
折りたたみスタイル
は、 >-
を文字列の先頭に記載して、残りは改行しながら書いていきます。各行の改行はスペースに置換され、最後の改行のみ無視されます。
[改善前]
- shell: echo "hello" "world"
[改善後]
- shell: >-
echo
"hello"
"world"
テンプレート (templates)
設定ファイルの作成はテンプレートを使う
各種設定ファイルをターゲットノードに配置するときはテンプレートを使用します。パラメータを変数化しておくことで、パラメータの異なる複数システムで同じテンプレートファイルを使い回すことができます。
ファイル構成例は以下の通りです。
<プロジェクトディレクトリ>/
└── roles
└── common
├── files
├── handlers
├── tasks
├── templates
│ |── chrony.conf.j2 # chronyd設定ファイル
│ └── rsyslog.conf.j2 # rsyslog設定ファイル
└── vars
chronyd.conf.j2
の内容例は以下の通りです。
テンプレートのファイル内では、定義済みの変数を使用したり、変数のコレクション(配列)をfor文で展開することができます。ここでは、ntp_servers
に定義している時刻同期先NTPサーバを設定項目に展開しています。
# ntp_servers: 時刻同期先NTPサーバのIPアドレスのコレクション
{% for ntp in ntp_servers %}
server {{ ntp.name }} iburst
{% endfor %}
driftfile /var/lib/chrony/drift
makestep 1.0 3
rtcsync
logdir /var/log/chrony
タスクファイルでtemplate
モジュールを使用するときは、以下のように記載します。
---
- name: Deploy chrony config
template:
src: chrony.conf.j2
dest: /etc/chrony.conf
owner: root
group: root
mode: 0644
backup: yes
notify: Restart chronyd daemon
ハンドラ (handlers)
ソフトウェア設定変更後の再起動はハンドラで処理する
タスクの冪等性を確保するために、ソフトウェア設定変更後の再起動はハンドラで処理します。タスクの実行結果がcahnged
であった場合に、同タスクのnotify
ディレクティブで指定した名前のハンドラが呼び出されます。ハンドラは、主にタスクで設定ファイルを変更した後のOSやサービスの再起動処理を記載します。
なお、ハンドラはタスク処理中に都度実行されるのではなく、タスク処理の最後にまとめて実行されるので注意が必要です。これは、タスク処理の中で何度も同じハンドラが実行されることを防ぐためです。
よくあるケースとして、タスク内で設定ファイルを配置するときに、ファイルに変更が生じた場合にハンドラを呼び出します。以下の例では、handlers
ディレクト内に存在するハンドラのうち、name
ディレクティブにRestart chronyd daemon
が指定されているタスクを呼び出します。
---
- name: Deploy chrony config
template:
src: chrony.conf.j2
dest: /etc/chrony.conf
owner: root
group: root
mode: 0644
backup: yes
notify: Restart chronyd daemon
main.ymlに処理を書かない
Taskと同様にmain.yml
には直接処理を書かず、import_tasks
ディレクティブを使用して同じディレクトリ階層の別ファイルをインポートするようにします。
ファイル構成例は以下の通りです。
<プロジェクトディレクトリ>/
└── roles
└── common
├── files
├── handlers
│ |── main.yml # メインHandler
│ |── restart_chronyd.yml # chronyd再起動用Handler
│ └── restart_rsyslog.yml # rsyslog再起動用Handler
├── tasks
├── templates
└── vars
main.yml
の内容例は以下の通りです。
---
- import_tasks: restart_chronyd.yml
- import_tasks: restart_rsyslog.yml
restart_chronyd.yml
の内容例は以下の通りです。
---
- name: Restart chronyd daemon
systemd:
name: chronyd.service
state: restarted