2014年10月26日日曜日

Fabricはexecute APIで自由度の高いデプロイツールに

FabricはPythonの関数を直接呼び出す機能とSSH経由のコマンドを複数のホストに対して楽に発行する機能を持つライブラリです。
簡単な使い方はこちらのページで確認できます。(最近はFabricで検索すると日本語の紹介記事もヒットするようになりました。)
http://docs.fabfile.org/en/latest/tutorial.html

初歩的な使い方だとすぐに使えるようになるのがFabricのメリットです。
また結局はPythonスクリプトなので、サーバー障害時のサービスアウトからパッケージ更新等のデプロイまで幅広く使えます。

しかしある程度大規模で複雑なシステムが対象になると初歩的な使い方では間に合わなくなります。
たとえば、複数のDBサーバをサービスアウトするためには複数のAPサーバーの設定ファイルを書き換える必要がある時はどうすれば良いでしょうか?
あるいは、サービスアウト、デプロイ、サービスインの一連の手順を1台ずつ行いたいという場合はどう記述すれば良いでしょうか?

これらはfab -H [ホスト名] [タスク名]という単純なfab実行方法では不可能で、env.hostsを書き換えるのもタスク実行前に行わなければならないので難しいです。
そこで、fabric.apiのexecuteを使用します。
公式ドキュメントにはこのようなコードが載っています。

from fabric.api import run, execute, task

from mylib import external_datastore

def do_work():
    run("something interesting on a host")

@task
def deploy(lookup_param):
    host_list = external_datastore.query(lookup_param)
    execute(do_work, hosts=host_list)

http://docs.fabfile.org/en/latest/usage/execution.html#intelligently-executing-tasks-with-execute
Using execute with dynamically-set host listsより

host_listに対象となるホストのリストを与えてexecuteすることで、タスクごとに異なるホスト群を指定できることに注目してください。
また、タスク内でこのexecuteは何度も実行できますので、サービスイン・デプロイ・サービスアウトを順に行うタスクも記述可能です。(後述)
これを使えば、1つのタスク内で異なるホスト群に対していろいろなコマンドを発行することができるようになります。
例として、複数台のWebサーバ、Applicationサーバ、Databaseサーバで構成されるシステムについて以下の様なFabricスクリプトを書いてみました。(あくまでFabricのために書いたもので、サーバに対して発行するコマンドはechoのみにしてます。)
ファイル構成は以下の通り(fabfileディレクトリと__init__.pyはデフォルト設定では必須です。)

--fabfile
  |-- __init__.py
  |-- web.py
  |-- ap.py
  |-- db.py
  |-- common.py
  |-- conf.py

この例では各コンポーネントに発行するコマンドがだいたい同じだと想定してComponentという抽象クラスを用意し、各コンポーネントクラスはComponentを継承してservice_in, service_outなどのメソッドをオーバーライドしています。

実行例は以下の通り。(実行結果は長いので省きます)

# webサーバ1台ずつサービスアウト・パッケージ更新・サービスインの一連のタスクを行います
$ fab release:web,update_pkgs
# 障害が起きたwebサーバを1台指定してサービスアウトします
$ fab service_out:web01.example.com
# DBサーバそれぞれに対してバックアップを取ります
$ fab backup_db

__init__.py

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

import web, ap, db
from common import get_hosts, get_role
from fabric.api import sudo, execute
from fabric.decorators import task
from functools import partial

components = {
    'web': web.Web(),
    'ap': ap.Application(),
    'db': db.Database()
}

# デプロイ名を指定して各種デプロイを行う関数
def deploy(name):
    if name == 'update_pkgs':
        sudo('echo "Update packages"')
    else:
        raise Exception('Unknown deploy command %s' % name)

# 任意個のデプロイをFabricタスクとして呼び出す関数
def do_deploys(deploys, hosts):
    for d in deploys:
        execute(partial(deploy, d), hosts=hosts)

# 障害時などにホスト名を指定してサービスアウトを行うタスク
@task
def service_out(hostname):
    components[get_role(hostname)].service_out(hostname)

# 障害復旧時などにホスト名を指定してサービスインを行うタスク
@task
def service_in(hostname):
    c = components[get_role(hostname)]
    c.restart(hostname)
    c.service_in(hostname)

# パッケージ更新などのデプロイをコンポーネント指定でまとめて行うタスク
# サービスアウト、デプロイ、プロセス再起動、サービスインを順に行います
@task
def release(target, *deploys):
    c = components[target]
    # 一連のプロセスはサーバ1台毎に行います
    for host in get_hosts(target):
        c.service_out(host)
        do_deploys(deploys, host)
        c.restart(host)
        c.service_in(host)

@task
def backup_db():
    db.Database().backup(get_hosts('db'))

db.py

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

from common import Component, get_hosts
from fabric.api import run, execute
from functools import partial

def service_out(hostname):
    run('echo "service out %s"' % hostname)

def service_in(hostname):
    run('echo "service in %s"' % hostname)

def restart():
    run('echo "restart"')

def backup():
    run('echo "Backup database"')

class Database(Component):
    def service_out(self, *hosts):
        for host in hosts:
            execute(partial(service_out, host), hosts=get_hosts('ap'))
    def service_in(self, *hosts):
        for host in hosts:
            execute(partial(service_in, host), hosts=get_hosts('ap'))
    def restart(self, hosts):
        execute(restart, hosts=hosts)
    def backup(self, hosts):
        execute(backup, hosts=hosts)

web.pyとap.pyはdb.pyと同じような内容なので下記を参照
https://github.com/muumu/fabric-sample/blob/master/fabfile/web.py
https://github.com/muumu/fabric-sample/blob/master/fabfile/ap.py

common.py

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

import conf
from fabric.api import env
from abc import ABCMeta, abstractmethod

# 各コンポーネントが持っているべきメソッドを宣言します
# 実際のシステムでは全プロセスを止めるstopとかもあるべきかと思います
class Component:
    __metaclass__ = ABCMeta
    @abstractmethod
    def service_out(self, hosts): pass

    @abstractmethod
    def service_in(self, hosts): pass

    @abstractmethod
    def restart(self, hosts): pass

# 大規模システムだとサーバー情報を入れたDBからホスト名を引いてくるかと思いますが
# ここでは簡単のため@rolesデコレータとして利用可能なenv.roledefsを使用します
def get_hosts(role_name):
    if role_name not in env.roledefs:
        raise Exception('Invalid role name: %s' % role_name)
    return env.roledefs[role_name]

# ホスト名からロール名を取得する関数です
# これも簡単のため単にenv.roledefsから逆引き
def get_role(hostname):
    for role, hosts in env.roledefs.items():
        if hostname in hosts:
            return role
    raise Exception('Invalid hostname: %s' % hostname)

conf.py

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

from fabric.api import env

env.roledefs = {
    'web': ['web01.example.com', 'web02.example.com'],
    'ap': ['ap01.example.com', 'ap02.example.com'],
    'db': ['db01.example.com', 'db02.example.com']
}

# 1つのホストにタスク実行が終わる毎に切断して接続リソースを節約します
env.eagerly_disconnect = True
# ssh_configの設定内容を使用してssh接続を行います
env.use_ssh_config = True
https://github.com/muumu/fabric-sample
このようにFabricの自由度はかなり高いので、大規模で複雑なシステムに対して柔軟なコマンドを発行したい、という場合に手早く実装できてとても便利です。

0 件のコメント:

コメントを投稿