読者です 読者をやめる 読者になる 読者になる

ほわいとぼーど

ぷろぐらまのメモ帳

fabricのroleで試行錯誤した話

普段は構築はChefで行うのですが、
コマンド実行もサポートしたい要件があったのでfabricを試してみました。
ただ、自分のやりたいことに対してroleの挙動が難しかったので
試行錯誤の様子をメモとして残します。
fabric初心者なのでこれが良い方法かどうかはわかりません。

どんな事がしたかったか?

複数の構成要素(client、server1、server2、、、)に1コマンド実行したい。
ただし、手元のVMで動作確認する場合に一部の構成要素をひとまとめにすることがある。
(server1、server2をserverとして扱い両方の機能を1台に押し込める)

環境

今回は説明のために実際よりは簡略化し、種類としては「client」「server1」「server2」
の3つとし、更に「server1」「server2」両方の機能をもつものを「server」とします。
実施内容としては、

  • file_common.binを全台に配布する。
  • 各機能に合わせたファイルをそれぞれに配布する。(file_client.bin, file_server1.bin, file_server2.bin)

というわけで書いてみます。


[deploy1.py]

from fabric.api import env, put
from fabric.decorators import task, roles

env.user = "vagrant"
env.password = "vagrant"

@task
def env_full():
    env.roledefs = {
       'client'  : ['192.168.100.171'],
       'server1' : ['192.168.100.172'],
       'server2' : ['192.168.100.173'],
    }

@task
def env_small():
    env.roledefs = {
       'server' : ['192.168.100.170'],
       'client' : ['192.168.100.171'],
    }

@task
def deploy():
    _deploy_common()
    _deploy_client()
    _deploy_server1()
    _deploy_server2()

@roles('server', 'client', 'server1', 'server2')
def _deploy_common():
    put('resources/file_common.bin', '/tmp/file_common.bin')

@roles('client')
def _deploy_client():
    put('resources/file_client.bin', '/tmp/file_client.bin')

@roles('server', 'server1')
def _deploy_server1():
    put('resources/file_server1.bin', '/tmp/file_server1.bin')

@roles('server', 'server2')
def _deploy_server2():
    put('resources/file_server2.bin', '/tmp/file_server2.bin')

そして実行

vagrant@fabric:$ fab -f deploy1.py env_small deploy                                                     
No hosts found. Please specify (single) host string for connection: 

roleを設定したつもりでしたが、実行対象が無いと怒られました。
@rolesをつけただけでは実行対象として認識されないようです。
自分が試行錯誤した結果、「taskとして実行される」「executeを使って実行される」
ないと@rolesが認識されないようです。

excuteを使ってdeployメソッドを書き換えます。


[deploy2.py]

from fabric.api import env, put, execute
...中略

@task
def deploy():
    execute(_deploy_common)
    execute(_deploy_client)
    execute(_deploy_server1)
    execute(_deploy_server2)

...中略
vagrant@fabric:$ fab -f deploy2.py env_small deploy

Fatal error: The following specified roles do not exist:
    server1
    server2

Aborting.

vagrant@fabric:$ fab -f deploy2.py env_full deploy

Fatal error: The following specified roles do not exist:
    server

Aborting.

今度はroleの一部がないと言われます。
両方実行した結果を載せたので何となく想像がつくと思いますが、
@rolesで記載したものが存在しないと言われているようです。
自分は@rolesをフィルタリング的な意味合として書きましたが、
単純に実行対象を環境変数から取得して実行するだけの仕組みということかも?

仕方が無いので@rolesを付けるメソッドは分けることにします。
構成を色々いじる必要があり、試行錯誤して以下になりました。


[deploy3.py]

from fabric.api import env, put, execute
from fabric.decorators import task, roles

env.user = "vagrant"
env.password = "vagrant"

def _env_small():
    env.roledefs = {
       'server' : ['192.168.100.170'],
       'client' : ['192.168.100.171'],
    }

def _env_full():
    env.roledefs = {
       'client'  : ['192.168.100.171'],
       'server1' : ['192.168.100.172'],
       'server2' : ['192.168.100.173'],
    }

@task
def deploy_small():
    _env_small()
    execute(_deploy_common_small)
    execute(_deploy_server)
    execute(_deploy_client)

@task
def deploy_full():
    _env_full()
    execute(_deploy_common_full)
    execute(_deploy_client)
    execute(_deploy_server1)
    execute(_deploy_server2)

@roles('server', 'client')
def _deploy_common_small():
    _deploy_common()

@roles('client', 'server1', 'server2')
def _deploy_common_full():
    _deploy_common()

def _deploy_common():
    put('resources/file_common.bin', '/tmp/file_common.bin')

@roles('client')
def _deploy_client():
    put('resources/file_client.bin', '/tmp/file_client.bin')

@roles('server1')
def _deploy_server1():
    put('resources/file_server1.bin', '/tmp/file_server1.bin')

@roles('server2')
def _deploy_server2():
    put('resources/file_server2.bin', '/tmp/file_server2.bin')

@roles('server')
def _deploy_server():
    _deploy_server1()
    _deploy_server2()

実行部分が@rolesで分かれるのでenv.roledefs定義も
中で呼ぶように変更しました。
_deploy_commonも分ける必要があったのは2重に呼ばないためです。
@runs_onceという1回しか呼ばないためのdecoratorもあるのですが、
これを付けたら本当に全体で1回しか呼ばれませんでした。
やりたいのは各ノードで1回にしたいということです。

vagrant@fabric:$ fab -f deploy3.py deploy_small
[192.168.100.170] Executing task '_deploy_common_small'
[192.168.100.170] put: resources/file_common.bin -> /tmp/file_common.bin
[192.168.100.171] Executing task '_deploy_common_small'
[192.168.100.171] put: resources/file_common.bin -> /tmp/file_common.bin
[192.168.100.170] Executing task '_deploy_server'
[192.168.100.170] put: resources/file_server1.bin -> /tmp/file_server1.bin
[192.168.100.170] put: resources/file_server2.bin -> /tmp/file_server2.bin
[192.168.100.171] Executing task '_deploy_client'
[192.168.100.171] put: resources/file_client.bin -> /tmp/file_client.bin

Done.
Disconnecting from 192.168.100.170... done.
Disconnecting from 192.168.100.171... done.

vagrant@fabric:$ fab -f deploy3.py deploy_full
[192.168.100.171] Executing task '_deploy_common_full'
[192.168.100.171] put: resources/file_common.bin -> /tmp/file_common.bin
[192.168.100.172] Executing task '_deploy_common_full'
[192.168.100.172] put: resources/file_common.bin -> /tmp/file_common.bin
[192.168.100.173] Executing task '_deploy_common_full'
[192.168.100.173] put: resources/file_common.bin -> /tmp/file_common.bin
[192.168.100.171] Executing task '_deploy_client'
[192.168.100.171] put: resources/file_client.bin -> /tmp/file_client.bin
[192.168.100.172] Executing task '_deploy_server1'
[192.168.100.172] put: resources/file_server1.bin -> /tmp/file_server1.bin
[192.168.100.173] Executing task '_deploy_server2'
[192.168.100.173] put: resources/file_server2.bin -> /tmp/file_server2.bin

Done.
Disconnecting from 192.168.100.173... done.
Disconnecting from 192.168.100.171... done.
Disconnecting from 192.168.100.172... done.

うまくいきましたね!
しかし@roles呼び分けるためにラップするだけのメソッドが増えてしまいました。
また、単にputしてるだけなのでわかりづらいのですが、この実行は直列実行です。
異なるノードに対しては並列で実行したいですよね。


並列に扱うには@parallelを付ければいいのですが、
付けたメソッドに対して並列実行したい対象が通る形にしないと並列になりません。
例えば上記のdeploy3.pyだと_deploy_common_full()@parallelを付ければ
「client」「server1」「server2」に対して並列実行されますが、
_deploy_client()_deploy_server1()_deploy_server2()@parallelを付けても、
並列には実行されないのです。
というわけで大改造します。


[deploy4.py]

from fabric.api import env, put, execute
from fabric.decorators import task, roles, parallel

...中略

@task
def deploy_small():
    _env_small()
    execute(_deploy_roles_small)

@task
def deploy_full():
    _env_full()
    execute(_deploy_roles_full)

@roles('server', 'client')
@parallel
def _deploy_roles_small():
    _deploy_roles()

@roles('client', 'server1', 'server2')
@parallel
def _deploy_roles_full():
    _deploy_roles()

def _deploy_roles():
    _deploy_common()
    
    current_role = _get_current_role()
    if current_role == 'server':
        _deploy_server1()
        _deploy_server2()
    elif current_role == 'client':
        _deploy_client()
    elif current_role == 'server1':
        _deploy_server1()
    elif current_role == 'server2':
        _deploy_server2()
    else:
        print "invalid role: {0}".format(current_roles)

def _get_current_role():
    for role in env.roledefs.keys():
        if env.host_string in env.roledefs[role]:
            return role
    return None

def _deploy_common():
    put('resources/file_common.bin', '/tmp/file_common.bin')

def _deploy_client():
    put('resources/file_client.bin', '/tmp/file_client.bin')

def _deploy_server1():
    put('resources/file_server1.bin', '/tmp/file_server1.bin')

def _deploy_server2():
    put('resources/file_server2.bin', '/tmp/file_server2.bin')

並列処理したい処理部をroleを判別して挙動を変えるメソッドに押し込めてます。
判別するためのroleはenv.host_stringで現在実行中のhost対象が取れるので、
それを元にenv.roledefsから逆引きして特定しています。
このやり方はstackoverflowで見つけました。
@rolesの付け替えのためのラップメソッドはどうしても残りますが、
全体的には前よりもわかりやすくなったのではないでしょうか。

では実行してみましょう。

vagrant@fabric:$ fab -f deploy4.py deploy_small
[192.168.100.170] Executing task '_deploy_roles_small'
[192.168.100.171] Executing task '_deploy_roles_small'
[192.168.100.171] put: resources/file_common.bin -> /tmp/file_common.bin
[192.168.100.170] put: resources/file_common.bin -> /tmp/file_common.bin
[192.168.100.171] put: resources/file_client.bin -> /tmp/file_client.bin
[192.168.100.170] put: resources/file_server1.bin -> /tmp/file_server1.bin
[192.168.100.170] put: resources/file_server2.bin -> /tmp/file_server2.bin

Done.

vagrant@fabric:$ fab -f deploy4.py deploy_full
[192.168.100.171] Executing task '_deploy_roles_full'
[192.168.100.172] Executing task '_deploy_roles_full'
[192.168.100.173] Executing task '_deploy_roles_full'
[192.168.100.173] put: resources/file_common.bin -> /tmp/file_common.bin
[192.168.100.173] put: resources/file_server2.bin -> /tmp/file_server2.bin
[192.168.100.171] put: resources/file_common.bin -> /tmp/file_common.bin
[192.168.100.171] put: resources/file_client.bin -> /tmp/file_client.bin
[192.168.100.172] put: resources/file_common.bin -> /tmp/file_common.bin
[192.168.100.172] put: resources/file_server1.bin -> /tmp/file_server1.bin

Done.

できてそうですね。

ということで@roles@parallelと格闘してみたメモでした。
本当は更に個別のノード指定もサポートしたいのですが、そこは徐々にということにします。
しかしメソッド名とかセンス無くて泣きそう。