Ansibleの設計方針

ベストプラクティスを語るにはまず前提としている設計方針を明確にしなければならない。

Ansibleのバージョンは2.7で、目標とするAnsibleの使い方は以下の通り。

  1. Webサーバ、DBサーバ、Cacheサーバなど多種多様なサーバの構築をひとつのPlaybookで対処できること
  2. WebサーバのFQDNやポート、DBサーバのレプリケーション構成などを変数として定義し、各プロジェクトで使いまわせるようにすること

1つめのひとつのPlaybookで各種サーバを構築するというのは、よく例で挙げられる構成とは違うと思う。Webサーバを構築するのであればwebserver.ymlでNginxとFluentdをインストールし、DBサーバを構築するのであればdbserver.ymlでMySQLをインストールするといったように、各サーバ種別ごとにPlaybookを用意しているのを例でよくみる。

しかし実装したAnsibleを複数のチームメンバに複数のプロジェクトで使用してもらうには、実行するPlaybookがひとつの方がインタフェースとしてわかりやすい。

2つめの各プロジェクトで使いまわせるようにするというのは、大量のプロジェクトを抱えているチームでは当然の要求となるだろう。複数のプロジェクト使いまわせるように、変数はできるだけ多く定義できるようにする必要がある。そのため、変数定義の方法が簡単で、かつ視認性・閲覧性が高いようにする必要がある。

Ansibleのベストプラクティスな構成

実際の構成を見ながら説明していく。

ディレクトリ構成

まずはディレクトリ構成から。

.
├── group_vars
│   ├── general.yml
│   └── iptables.yml
├── inventories
│   ├── group_vars
│   │     └── all -> ../../group_vars
│   ├── development.yml
│   └── production.yml
├── playbooks
├── roles
└── setup.yml # メインのplaybook

変数定義

変数定義はgroup_vars/allディレクトリ配下に作成する。group変数はallレベルのもののみ定義し、例えばwebserverグループやdbserverグループなどの単位でのgroup変数はgroup_varsディレクトリ配下に作成しない。(all以外のgroup変数はInventoryの箇所で記載する。)

group_varsディレクトリはPlaybookと同じ階層かInventoryと同じ階層に置かなくてはいけない。しかし実際に変数を記載・確認するときのパスがinventories/group_vars/all/general.ymlでは階層が深すぎてユーザビリティが落ちるので、inventories/group_vars/allをトップ階層のgroup_varsディレクトリへのsymlinkとしている。

allレベルのgroup変数なのでOS層にかかわることやミドルウェアのバージョンなど各環境や用途にかかわらない内容の定義を行う。allディレクトリ以下のYAMLファイルはすべてgroup変数として扱ってくれるため、変数の内容に合わせてファイルを適宜分ける。

$ cat group_vars/general.yml
略
timezone: "Asia/Tokyo"

mysql:
  version: mysql57
  password:
    root: rootpasswd
    slave: slavepasswd
略

Inventory

inventoriesディレクトリの下にInventoryを環境ごとに作成する。

さきほど「all以外のgroup変数はInventoryの箇所で記載する」と記載した通り、Inventoryには積極的にgroup変数とhost変数を記載していく。group_varsディレクトリの下にall以外のgroup変数用ディレクトリやファイルを作成しない理由は、Inventoryに書いたほうが、Inventoryを見るだけでサーバの設定がすべてわかるのが記述性も閲覧性も高いと考えているから。変数をかなり書くため、iniではなく、階層構造が簡単に書けるYAMLで書く。

inventories/production.ymlは長くなるので後掲するが、工夫しているポイントは以下の三点。

  1. ホストは必ずグループに属させる
  2. グループはall.childrenに属さなければいけないが、all.childrenを書かなくてもAnsibleが自動で補完してくれるようなので可読性のためall.childrenは書かない
  3. グループの書式はユニークな任意のグループ名@実行したいPlaybook1&実行したいPlaybook2&...とする

2の工夫は本当だったら以下のように階層が深くなりすぎて読みづらくなるところを解消している。

all:
  children:
    group1:
      hosts:
        server0001:
        server0002:

3の工夫は本題の一番特殊なところ。Playbookの書き方と密接にかかわるのでPlaybookのところで再度取り上げる。

inventories/production.ymlは以下の通り。

---
api@nginx&td-agent:
  vars:
    munin:
      group: production
      subgroup: api
    nginx:
      sites:
        - server_name: api.example.com
          listen: 80
          https: false
  hosts:
    api0001:
    api0002:
    api0003:

maindb@mysql:
  vars:
    munin:
      group: production
      subgroup: maindb
  hosts:
    # shard 1
    maindbm0001:
    maindbs0001:
      mysql:
        master_host: maindbm0001
    maindbb0001:
      mysql:
        master_host: maindbm0001
        backup: { minute: 00, hour: 03 }
    # shard 2
    maindbm0002:
    maindbs0002:
      mysql:
        master_host: maindbm0002
    maindbb0002:
      mysql:
        master_host: maindbm0002
        backup: { minute: 00, hour: 04 }

Playbook

ひとつのPlaybookのみとするというルールから、setup.ymlという名前をエントリーポイントのPlaybookとした。setup.ymlからはplaybooksディレクトリ配下にあるnginx.ymlやmysql.ymlなどの目的別のPlaybookをimportしている。

$ head -4 setup.yml
---
- import_playbook: playbooks/common.yml
- import_playbook: playbooks/nginx.yml
- import_playbook: playbooks/mysql.yml

setup.ymlから目的別のPlaybookを必ず呼び出すが、Webサーバを作るときは当然mysql.ymlを実行してほしくない。

これを実現するために、Playbookのhostsで処理対象とするかどうかのフィルタリングしている。

---
- hosts: ~.+@(.+&)*nginx(&.+)*
  become: yes
  roles:
    - nginx

hostsにはグループ名やホスト名がかけるだけではなく、ワイルドカードや正規表現が使える。正規表現を使うには~から始まっていなければいけない。

ここでInventoryファイルでグループの書式を特殊なものにしたことが活きている。@の後ろに&区切りで実行したいPlaybookを記載したので、それにマッチするような正規表現をhostsに書けば、記載されているグループに属するホストに対してだけ各Playbookが実行される。

まとめ

hostsへの正規表現導入とグループ名の書式ルール設定が肝になり、エントリーポイントのPlaybookをひとつにしてユーザビリティを高めている。

しかし変数をどこに定義するかというのもユーザビリティにはかなり重要で、今の分類が一番わかりやすいと考えている。

  • 変数定義が散らばらない
  • 一つのファイルを見ればそのサーバで何が設定されるのかわかる

これらの観点で変数定義をどうするかを決めると、一プロジェクト全体にかかわる変数定義の場所(つまりgroup_vars/all/)と、各ホストの定義と同じファイル(つまりinventories/production.ymlのgroup変数とhost変数)のみとするのがいい。