【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

グループ固有の変数はグループ変数に書く

インベントリに定義したグループ単位で、固有値をyaml形式で変数化します。プロジェクトディレクトリ直下にgroup_varsディレクトリを作成し、ファイル名は、<グループ名>.ymlとします。allは、全てのターゲットノードが暗黙的に所属する特殊なグループです。

ファイル構成例は以下の通りです。

<プロジェクトディレクトリ>
└── group_vars
    ├── all.yml
    ├── web.yml
    ├── ap.yml
    └── db.yml

ターゲットノードにSSHでリモートアクセスする際のユーザ名を指定する場合は、all.ymlに以下のように指定をします。

ssh_remote_user: ec2-user

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/<ロール名>/vars/main.yml)は、以下の内容で使い分けます。

  • インベントリ変数: ターゲットノード(またはグループ)固有のパラメータ
    • 適用パラメータ例: ホスト名、IPアドレス、システム名…etc
  • ロール変数: 同じ種別のサーバで共通するソフトウェアパラメータ
    • 例: ポート番号、設定ファイルの場所、最大接続数…etc

変数名の先頭にロール名を付ける

複数のロールを作成すると、ロール変数名が重複することがあります。後でどのロールの変数化区別しやすいように、変数名の先頭にはロール名を付けます。

apache_port: 80
mysql_port: 3306

タスク

タスクで管理する設定項目

業務でサーバを構築する場合、構成を管理するためのドキュメントとして、パラメータシート(または詳細設計書)と呼ばれるドキュメントを作成します。このドキュメントには、以下のようなカテゴリでサーバの設定情報を記載します。

  • ユーザ、グループ
  • インストールパッケージ
  • サービス起動状態
  • ディレクトリ構成
  • 各ソフトウェアの設定内容

設定手順書に沿って、パラメータシートの上記内容を実機に投入することで、サーバを構築することができるのです。

タスク(Task)は、この一連の流れを定義したファイルです。
ターゲットノードのあるべき姿(設定)を定義し、あるべき姿でない場合は、タスクに記載の内容で設定変更が行われます。

ロール内にタスクを作成するときは、従来パラメータシートに記載していた内容を記載対象として、ロールで管理するアプリケーションごとに項目を足し引きしていくと効率が良いです。

main.ymlに処理を書かない

tasksディレクトリ内のmain.ymlには、直接タスクの処理を書かないようにします。main.ymlと同じ階層に、処理の種別ごとにタスクファイルを作成し、import_tasksを使ってmain.ymlにインポートします。

ファイル構成例は以下の通りです。

<プロジェクトディレクトリ>
└── roles
    └── <ロール名>
        ├── files
        ├── handlers
        ├── tasks
        │   |── main.yml              # メインタスク
        │   |── user_group.yml        # ユーザ、グループ管理用タスク
        │   |── package.yml           # パッケージインストール状態管理用タスク
        │   |── directory.yml         # ディレクトリ構成管理用タスク
        │   |── config.yml            # 設定ファイル管理用タスク
        │   └── service.yml           # サービス起動状態管理用タスク
        ├── templates
        └── vars

main.ymlでは、import_tasksを使用して、同階層の別タスクファイルをインポートします。

---
- import_tasks: user_group.yml
- import_tasks: package.yml
- import_tasks: directory.yml
- import_tasks: config.yml
- import_tasks: service.yml

shellモジュールには冪等性対策を行う

前述の初期設定状態管理用タスクでは、ソフトウェア固有の初期設定を行うため、設定手順をshellモジュールで書くことが多いです。シェルスクリプトのように愚直に処理を書いてしまうと、Playbookをインストールするたびに初期設定が実行されてしまいます。タスクの冪等性を担保するために、初期設定状態管理用タスクでは、初期設定状態の確認、未設定時の場合のみ初期設定処理を意識してタスクを書きます

以下の例は、MySQLの初期設定状態管理用タスクです。
Check initializeのタスクでは、初期設定状態の確認として予め変数として定義しているmysql.root_passwodをパスワードに指定してMySQLにログインを試みます。コマンド実行結果は、レジスタ変数としてmysql_login_resultへ格納します。
Execute MySQL initializeのタスクでは、blockディレクティブを使用して複数のタスクを一つのブロックにしています。ブロックの最後には、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

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"

初期設定の処理は実行条件を指定する

ソフトウェアごとの初期設定は、どうしてもシェル操作に頼らないといけない場面が出てきます。shellモジュールを使用しつつPlaybookの冪等性を保つために、初期設定済みかチェックする処理を入れて、初期設定が未完了の場合のみ処理を継続するよう実行条件を指定します。

以下の例では、MySQLにおいて本来意図したパスワードでログインできるか確認を行なっています。ログイン結果の戻り値を判定し、ログインに失敗している場合はまだ初期設定が終わっていないとみなして初期設定を行います。初期設定の処理全体に実行条件を指定するため、処理をblockディレクティブでまとめています。

- name: Check MySQL initialize
  shell: mysql -uroot -p {{ mysql.root_passwod }}
  register: mysql_login_result
  changed_when: False

- name: Execute MySQL initialize
  block:
    ### 〜MySQL初期設定の処理〜 ###
  when: mysql_login_result.rc != 0

テンプレート (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: chrony
    state: restarted

コメントを残す

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