【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.ymlgroup_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

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です