はじめに

Dynamic Inventoryで独自書式を定義し、inventoryファイルを簡易化するで、AnsibleのDynamic Inventoryを使って独自の書式をinventoryに取り入れ、チームの運用にあったYAMLを書く方法を記載した。今回は前回定義したsetupsに加えて、MySQLのMasterSlave構成を簡潔に表せるYAMLのデータ構造を考えたい。

標準的なgroup_varsでは困る理由

MySQLのMasterSlave構成をYAMLで表現したい理由は、AnsibleのplaybookでMasterSlaveのセットアップまで行いたいからである。

まずは単純に以下のような変数(os_memory, mysql.master, mysql.backup.host, mysql.backup.schedule)を定義して一般的な解決方法ができないか考える。

---
production:
  vars:
  children:
    - usr_db

usr_db:
  hosts:
    - udb0001
    - udb0002
    - udb0003
  setups:
    - mysql
  vars:
    os_memory: 64GB
    mysql:
      master: udb0001
      backup:
        host: udb0003
        schedule:
          - minute: 00
            hour: 03

この例だと、udb0001がMasterで、udb0002とudb0003がSlave、udb0003は3:00にmysqldumpがcronから実行されるようにplaybookで設定を流すとする。
つまりmysql.masterには必ずMasterのサーバ名を書くこととし、mysql.masterに書かれていないサーバはSlaveになる。mysql.backupにhostとscheduleを書くとbackup用のcron設定を行ってくれる。 またOSのメモリは64GBなので、そこから最適なinnodb_buffer_pool_sizeを自動で計算してmy.cnfに設定するようにする。

特に問題ないように見えるが、よくある次のような構成を考えると、YAMLの見通しが悪くなってしまう。

  • DBを水平分割したいので、同じようなYAML定義を複数回繰り返さなければならない
  • MasterとSlaveでOSのスペック/メモリが異なり、group_varsとしてos_memory変数を定義できない

usr_dbを10セットに水平分割し、MasterとSlaveでメモリサイズを変えてみると次のようなYAMLになるが、複数セット分のos_memoryやbackupの時刻を繰り返している点が冗長だったり、MasterとSlaveの定義が別れてしまうので一つのセットがどれかも判別しづらいという問題点がある。

---
production:
  vars:
  children:
    - usr_db1m
    - usr_db1s
    - usr_db2m
    - usr_db2s
    (略)
    - usr_db10m
    - usr_db10s

usr_db1m:
  hosts:
    - udb0001
  setups:
    - mysql
  vars:
    os_memory: 64GB
    mysql:
      master: udb0001

usr_db1s:
  hosts:
    - udb0002
    - udb0003
  setups:
    - mysql
  vars:
    os_memory: 64GB
    mysql:
      master: udb0001
      backup:
        host: udb0003
        schedule:
          - minute: 00
            hour: 03

(以下10セット分繰り返し)

独自書式を導入する

水平分割時の冗長さを解決するためにdb_set, master, slave, backup_slaveという書式を導入する。
db_set内に定義するサーバは同一の論理DBとする。
db_set内には(master, slave, backup_slave)のセットを書くことができる。
またメモリの差異などをMasterSlaveごとに分けられるように、master, slave, backup_slaveと同じ名前でそれぞれ個別の変数もoverride定義できる項目を設ける。

backup時刻を複数記載する大変さを解決するためにstart_from_hour, start_from_minute, intervalという変数を導入する。定義されたサーバを上から順にinterval分の間隔をおいてbackupできるように、cronに設定する時刻を自動で計算するようにする。

実際にどのようなYAMLになるか、2セットに水平分割し、Masterだけメモリが倍の構成例を記載する。

---
production:
  vars:
  children:
    - usr_db

usr_db:
  setups:
    - mysql
  vars:
    os_memory: 64GB
    mysql:
      backup:
        start_from_hour: 03
        start_from_minute: 00
        interval: 10
  db_set:
    - master: udb0001
      slave:
        - udb0002
      backup_slave: udb0003
    - master: udb0004
      slave:
        - udb0005
      backup_slave: udb0006
# 各mysqlの役割ごとの変数。group_varsを上書きしたい場合に使用する
  master:
    os_memory: 128GB
#  slave:
#    os_memory: 64GB
#  backup_slave:
#    os_memory: 64GB

先ほど挙げた欠点が解消されたことがわかる。

ちなみに上記例には記載しないが、backup_masterという書式も導入する。MasterSlaveではなくMasterしかないサーバを設定するときに、Masterからバックアップを行うための書式で、masterの代わりに使用する。

独自書式を解釈するプログラム

独自書式を解釈するプログラムは以下のような実装とした。

$ cat d_inventory_db
#!/usr/bin/python
# -*- coding: utf-8 -*-

import yaml
import json
import sys
import copy
from datetime import datetime, timedelta

def merge_gvars_rvars(role, v):
  role_vars = v.get(role, {})
  group_vars = v.get("vars", {})
  base_vars = copy.deepcopy(group_vars)

  # mysql.masterを必ず定義するためにmysql変数は必須の変数とする
  base_vars.setdefault("mysql", {})
  # base_varsのmysql変数にあるbackupは算出用の変数なので削除する
  backup_info = base_vars["mysql"].pop("backup", {})
  # base_varsにbackupの設定があれば、backup時間を計算して変数に設定する
  if len(backup_info) != 0 and role in ["backup_slave", "backup_master"]:
    (hour, minute) = derive_cron_time(backup_info)
    base_vars["mysql"] = {"backup": {"schedule": [{"minute": minute, "hour": hour}]}}

  deepupdate(base_vars, role_vars)
  return base_vars

derived_count = 0
def derive_cron_time(backup_info):
  hour = backup_info["start_from_hour"]
  minute = backup_info["start_from_minute"]
  global derived_count
  delta = derived_count * backup_info["interval"]
  derived_count += 1

  now = datetime.today()
  start = datetime(now.year, now.month, now.day, hour, minute) + timedelta(minutes=delta)
  return (start.hour, start.minute)

def deepupdate(dict_base, dict_over):
  for k, v in dict_over.items():
    if isinstance(v, dict) and k in dict_base:
      deepupdate(dict_base[k], v)
    else:
      dict_base[k] = v


## 標準入力で受け取ったYAML内の独自定義のdb_set,master,slave,backup_slave,backup_master句の解析を行い、Ansibleで使用できるinventory形式に変換する
data = yaml.load(sys.stdin.read())

# dictの中を変換していくため、変換用にdeepcopyする
data_rep = copy.deepcopy(data)

for k, v in filter(lambda (k, v): "db_set" in v, data.items()):
  for db_set in v["db_set"]:
    # configuration check
    if "backup_master" in db_set:
      assert len(db_set.keys()) == 1, "when backup_master is defined, other hosts cannot be defined in one db_set."
    else:
      assert "master" in db_set, "whether backup_master or master must be defined in one db_set."

    # 1つのdb_set内の各独自roleを取得
    backup_master = db_set.get("backup_master")
    master = db_set.get("master")
    slaves = db_set.get("slave", [])
    backup_slave = db_set.get("backup_slave")

    # roleに対応する独自変数を取得してgroup_varsとマージし、_meta.hostvarsにhostvarsとして登録していく
    meta = data_rep.setdefault("_meta", {"hostvars": {}})
    hostvars = meta["hostvars"]
    if backup_master is not None:
      backup_master_vars = merge_gvars_rvars("backup_master", v)
      hostvars.setdefault(backup_master, {}).update(backup_master_vars)
      hostvars[backup_master]["mysql"]["master"] = backup_master
      hostvars[backup_master]["mysql"]["backup"]["host"] = backup_master
    else:
      # master
      master_vars = merge_gvars_rvars("master", v)
      hostvars.setdefault(master, {}).update(master_vars)
      hostvars[master]["mysql"]["master"] = master
      # slave
      if len(slaves) > 0:
        slave_vars = merge_gvars_rvars("slave", v)
      for slave in slaves:
        hostvars.setdefault(slave, {}).update(slave_vars)
        hostvars[slave]["mysql"]["master"] = master
      # backup_slave
      if backup_slave is not None:
        backup_slave_vars = merge_gvars_rvars("backup_slave", v)
        hostvars.setdefault(backup_slave, {}).update(backup_slave_vars)
        hostvars[backup_slave]["mysql"]["master"] = master
        hostvars[backup_slave]["mysql"]["backup"]["host"] = backup_slave

    # Ansibleの書式に合わせるため、hostsに全ホストを設定する
    data_rep[k].setdefault("hosts", []).extend([h for h in slaves + [master] + [backup_slave] + [backup_master] if h is not None])

  # hostvarsへ移行済みの項目である独自設定項目とvarsを削除する
  data_rep[k].pop("db_set")
  data_rep[k].pop("vars", {})
  data_rep[k].pop("backup_master", {})
  data_rep[k].pop("master", {})
  data_rep[k].pop("slave", {})
  data_rep[k].pop("backup_slave", {})

print json.dumps(data_rep)

ホスト単位で全て変数を_meta.hostvarsに展開している。

前回作成したd_inventoryからd_inventory_dbを呼び出す。

$ cat d_inventory
#!/usr/bin/python

# -*- coding: utf-8 -*-

import yaml
import json
import os
import copy
import subprocess

hosts_yml = os.path.join(os.path.abspath(os.path.dirname(__file__)), "hosts.yml")
f = open(hosts_yml, "r")

d_inventory_db = os.path.join(os.path.abspath(os.path.dirname(__file__)), "d_inventory_db")
proc = subprocess.Popen(d_inventory_db, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
stdout = proc.communicate(f.read())[0]
data = yaml.load(stdout)

(以下前回と同じため略)

Dynamic InventoryはJSONを標準出力するという仕様のため、あえて各Dynamic Inventoryプログラム間の結合を標準入出力で行っている。