概要
fabric2において事前に定義したホストグループ名を指定することで並列にタスクを実行できるfabfileを作ったので紹介します。
また最初にそのfabfile作成に至った経緯としてfabricとfabric2について感じていることを書きます。
インストールと基本的な使い方
以下の記事を参考にさせてもらいました。
fabric2のインストール手順と簡単な使い方
fabricの用途
fabricでは以下の2つの用途が想定されていたと思います。
- シェルコマンドを含んだ処理をタスクとして登録・実行する
- 複数のサーバに対して直列または並列に一連のコマンドを実行し処理を行う
1についてはシェルコマンドとPythonのコードを組み合わせることで複雑な処理をわかりやすく書けるのが大きな利点。
2についてはsshコマンドを並列実行する時に様々な便利機能や拡張機能を提供してくれるのが利点。たとえばsudoパスワードや特定のキーワードを含むプロンプトに対する入力を1回で済ませられるようにできたり、それぞれのサーバにおける実行結果をPython上で受け取って条件分岐や繰り返し、集計と言った複雑な処理できるなどです。
最近はAWSやKubernetesなどが普及し個々のサーバにsshしてコマンドを実行することが減ってきていると思いますが、大企業のレガシーなシステムではいまだ個々のサーバ、それもたくさんのサーバをメンテナンスしなければならない状況は残っており、2019年でも2の用途でfabricが活躍するシーンはあると感じています。
fabric2の個人的な印象
(まずPython3に移行するにはfabric2への移行が必要というのは前提として)
用途1がinvokeに分離されました。
そのためfabric2は用途2のみになってターゲットが分かりやすくなりました。
また、fabricではsshコマンドを実行するときに出てくるexecute, run, env.hostと言った関数/変数が初学者にとっては分かりづらかったのがfabric2ではConnection/SerialGroup/ThreadingGroupと言った接続先を表すインスタンスに対してrunを実行するという分かりやすいインターフェースになりました。
ただ、2019年2月13日時点ではfabricではできていたことをfabric2でどのように実現すればいいのかわからない細かな点が多く、移行コストが大きいと感じています。
fabric2のソースを見てもTODOになってるコメントが多く、その細かな点が移行できるのか不安です。(まだ把握しきれておらず、調査中です。)
そして残念ながら個人的にfabricからfabric2になって素晴らしくなった!と思う点はありません。fabric2しかPython3をサポートしないから移行しないといけないという消極的な理由で移行を進めています。
fabric2に満足できない点
大きな点としては以下の2つが満足できません。
- hosts引数に対する並列実行オプションが無い
- 特定のホスト群に対して名前をつけて指定する機能が無い
1について、fabric2では以下のようにhostsオプションでタスクを実行するホストリストを指定できるのですが、
$ fab --hosts host1,host2,host3 taskA taskB
参考: http://docs.fabfile.org/en/2.4/cli.html#runtime-hosts
fabricではこのホスト群に対してparallelオプションの指定でタスクの並列実行ができていたのに、fabric2ではできなくなっています。(2019年2月13日現在)
2についてはfab実行時にホストリストはhosts引数で与えるしかなく、事前にファイルに保存しているホストリストを指定することができません。
fabricではroledefsという変数に定義して-Rオプションで指定できたのですが、2019年2月13日時点ではfabric2に同等の機能が無いようです。
fabric2ではせっかくGroupというクラスができたのにそれに名前をつけてfabコマンドのオプションで指定できないのは残念です。
多種類かつ多数のサーバにタスク実行したい場合は自分でその仕組みを記述する必要があります。
自作のfabfileで実現したいこと
- 定義したホストグループ名を指定してタスクを並列実行できること
- ホストグループ名に含まれるコンポーネント名によって同じタスクでも必要に応じて具体的な処理が分岐されるようになっていること
1は上述したfabric2に満足できない点を満たすための機能です。
2はfabricにも無い個人的に欲しい機能で、たとえば異なるコンポーネントのサーバでもstop
という同名のタスクでサーバのサービスアウト処理をできるようにするためです。
作ったfabfile (fabric2-parallel-sample)
ということで作成したのがfabric2-parallel-sampleです。
こんなサーバーグループ群を定義して、
# <component_name>.<environment>[.<group_number>]
HostGroups = {
'web.dev': ['web.dev.example.com'],
'ap.dev': ['ap.dev.example.com'],
'web.prod.1': ['web001.example.com'],
'web.prod.2': ['web{:03d}.example.com'.format(n) for n in range(2,4)],
'web.prod.3': ['web{:03d}.example.com'.format(n) for n in range(4,6)],
'ap.prod.1': ['ap001.example.com'],
'ap.prod.2': ['ap{:03d}.example.com'.format(n) for n in range(2,4)],
'ap.prod.3': ['ap{:03d}.example.com'.format(n) for n in range(4,6)]
}
こんな感じで実行します。
$ fab hosts web.prod.2 run --command=hostname
...
web002.example.com: hello
web003.example.com: hello
$ fab hosts web.prod.2 stop
...
Stopping web server...
Stopping web server...
$ fab hosts ap.prod.2 stop
...
Stopping application server...
Stopping application server...
$ fab group web001.example.com stop
...
Stopping web server...
まずgroup web.prod.2
という指定で実行対象をweb001.example.comとweb002.example.comの2台にしています。(細かい話ですがわざわざ2台に絞る名前にしているのは、全てのサーバを同時にサービスアウトしてはいけないという想定をしているためです。)
コード上では自作のgroupcontext.py内にあるset_group
という関数でグローバル変数のThreadingGroupインスタンスに実行対象を保存します。
そしてrunというタスク内ではそのグローバル変数を取ってきてrunを呼び出します。
from groupcontext import set_group, get_group
@task
def group(c, name):
print(name)
group = set_group(name)
print('hosts: [' + ','.join(group.hosts) + ']')
print('component: ' + str(group.component))
print('environment: ' + group.environment)
@task
def run(c, command=None, warn=False, print_result=True):
c = get_group()
r = c.run(command, warn=warn)
if print_result:
for connection, result in r.items():
print(connection.host + ': ' + result.stdout.strip())
return r
また、groupタスク内では実行対象のサーバー群が属するコンポーネントもグローバル変数のcomponentプロパティに保存しておきます。
そしてコンポーネント毎に処理内容を変えたい関数を実装しておきます。
class Ap:
def build(self, c):
return c.run('echo "Building application server..."')
def start(self, c):
return c.run('echo "Starting application server..."')
def stop(self, c):
return c.run('echo "Stopping application server..."')
def __str__(self):
return 'ap'
class Web:
def build(self, c):
return c.run('echo "Building web server..."')
def start(self, c):
return c.run('echo "Starting web server..."')
def stop(self, c):
return c.run('echo "Stopping web server..."')
def __str__(self):
return 'web'
@task
def stop(c):
c = get_group()
# グローバル変数内のcomponentプロパティに保存されていたインスタンスからbuildを呼び出す
# componentがwebならWebインスタンスのstopが呼び出される
return c.component.stop(c)
最後に、ホストグループの辞書とホスト名からコンポーネント名を逆引きする辞書を持つことで、ホストグループ名指定 (web.prod.2など) とホスト名指定 (web001.example.comなど) の両方に対応しています。
※ホスト名指定のコマンド再掲: $ fab group web001.example.com stop
詳細はソースで: https://github.com/muumu/fabric2-parallel-sample
なおサンプルコードはコード中のホスト名を/etc/hosts
で以下のようにlocalhostのIPを指定することで試せます。
127.0.0.1 web001.example.com
127.0.0.1 web002.example.com
127.0.0.1 web003.example.com
127.0.0.1 web004.example.com
127.0.0.1 web005.example.com
127.0.0.1 web006.example.com
127.0.0.1 ap001.example.com
127.0.0.1 ap002.example.com
127.0.0.1 ap003.example.com
127.0.0.1 ap004.example.com
127.0.0.1 ap005.example.com
127.0.0.1 ap006.example.com
TODO
カンマ区切りで複数のホストやホストグループを指定したり一部のホストを除外するとかの細かい機能は未実装です。
それと中身のコードの話ですが、(groupというタスクでセットしたホスト群を実行対象とするために)全てのタスクの冒頭でc = get_group()
とcを書き換えている処理はdecoratorにしたいのですがtaskの引数をfabに認識されるように記述することができなくて保留にしてます。
最後に
fabric2はまだソース上にTODOが多く残っていたりcommitログも続いていたりプルリクが集まっていたりと開発途上のようなので、今後の改善に期待したいです。