Fabricを使って複数のサーバを操作する

デプロイツールFabricを使って、SSH経由で複数のサーバを操作する方法のメモ。

同種のツールparallel-ssh(pssh)があるが、psshでsudoを行うためには/etc/sudoersを編集してパスワードなしでsudoできるようにしなければならない。 Fabricでは、一度入力したパスワードが以降のログイン/sudoパスワードとして自動で使い回されるため、特別な設定を加えることなくsudoを行うことができる。

fabricのインストール

Ubuntu 14.04.1 LTSの場合、apt-getでインストールできる。

$ sudo apt-get install fabric

サーバを指定してコマンド実行する

まずは単一のサーバに対し、uptimeコマンドを実行してみる。 次の内容をfabfile.pyというファイル名でカレントディレクトリに用意する。

from fabric.api import *

def uptime():
    run('uptime')

-Hオプションで対象となるサーバを指定し実行すると、ログインパスワードを求められた後結果が表示される。

$ fab -H user@localhost uptime
[user@localhost] Executing task 'uptime'
[user@localhost] run: uptime
[user@localhost] Login password for 'user':
[user@localhost] out:  10:22:55 up 18:41,  2 users,  load average: 0.08, 0.03, 0.05
[user@localhost] out:


Done.
Disconnecting from localhost... done.

実行中コマンドの表示を消す

--hide=runningオプションをつけることで、"Executing task ..." や "run: ..." のような実行中のコマンドについての表示を消すことができる。

$ fab -H user@localhost --hide=running uptime
[user@localhost] Login password for 'user':
[user@localhost] out:  10:25:51 up 18:44,  2 users,  load average: 0.00, 0.01, 0.05
[user@localhost] out:

Done.
Disconnecting from localhost... done.

コマンドラインから引数を渡す

特定のタスクに対し、コマンドラインから引数を渡すこともできる。 たとえば、fabfile.pyに次のような関数を追記する。

def ls(path):
    run("ls %s" % path)

そして、コマンドラインから次のように引数を指定する。 この例では、/usr が変数pathに入り、lsコマンドが実行される。

$ fab -H user@localhost ls:/usr
[user@localhost] Executing task 'ls'
[user@localhost] run: ls /usr
[user@localhost] Login password for 'user':
[user@localhost] out: bin  games  include  lib  local  sbin  share  src
[user@localhost] out:


Done.
Disconnecting from localhost... done.

疑似端末(pty)を使わない

Fabricはデフォルトで疑似端末のある状態でコマンドを実行する。 しかし、apt-getコマンドなどは疑似端末があるとき処理の進捗状況(パーセント表示)を一定間隔で繰り返し出力するため、余計な出力が多く表示されてしまう。

このような場合には、run/sudo関数にキーワード付き引数としてpty=Falseを与えることで、疑似端末を使わないようにできる。

def apt_update():
    sudo('apt-get update', pty=False)

実行すると、余計な進捗状況が出力されずに実行できていることが確認できる。

$ fab -H user@localhost apt_update
[user@localhost] Executing task 'apt_update'
[user@localhost] sudo: apt-get update
[user@localhost] Login password for 'user':
[user@localhost] out: sudo password:無視 http://jp.archive.ubuntu.com trusty InRelease
[user@localhost] out: 無視 http://jp.archive.ubuntu.com trusty-updates InRelease
[user@localhost] out: 無視 http://jp.archive.ubuntu.com trusty-backports InRelease
(snip)
[user@localhost] out: ヒット http://security.ubuntu.com trusty-security/restricted Translation-en
[user@localhost] out: ヒット http://security.ubuntu.com trusty-security/universe Translation-en
[user@localhost] out: 2,125 kB を 8秒 で取得しました (256 kB/s)
[user@localhost] out: パッケージリストを読み込んでいます...
[user@localhost] out:


Done.
Disconnecting from localhost... done.

標準出力を表示しないようにする

標準出力の表示が必要ない場合には次のようにすればよい。

def apt_update():
    with hide('stdout'):
        sudo('apt-get update', pty=False)
$ fab -H user@localhost apt_update
[user@localhost] Executing task 'apt_update'
[user@localhost] sudo: apt-get update
[user@localhost] Login password for 'user':

Done.
Disconnecting from localhost... done.

標準入力からサーバリストを読み込んでコマンド実行する

これまでの例では単一のサーバに対する操作だったが、-H user@192.168.0.2,user@192.168.0.3,user@192.168.0.4のようにカンマ区切りで並べることで複数のサーバに対して操作することもできる。 しかし、コマンドラインで対象となるサーバを列挙するのは煩雑なため、標準入力からサーバリストを読み込むことを考える。

具体的には、fabfile.pyの内容を次のようにする。

from fabric.api import *
import sys

lines = sys.stdin.read().splitlines()
env.hosts = filter(bool, lines)
print "target hosts: %r" % env.hosts

def uptime():
    run('uptime')

ここで、env.hostsは対象となるサーバを表す文字列の配列である。 つまり、コマンドラインからサーバを指定する代わりに、スクリプト中で標準入力から読み込んだリストを直接指定している。

サーバリストを作成し、コマンドを実行すると次のようになる。

$ cat servers.txt
user@192.168.56.2
user@192.168.56.3

$ cat servers.txt | fab uptime
target hosts: ['user@192.168.56.2', 'user@192.168.56.3']
[user@192.168.56.2] Executing task 'uptime'
[user@192.168.56.2] run: uptime
[user@192.168.56.2] Login password for 'user':
[user@192.168.56.2] out:  10:49:46 up 19:08,  2 users,  load average: 0.00, 0.01, 0.05
[user@192.168.56.2] out:

[user@192.168.56.3] Executing task 'uptime'
[user@192.168.56.3] run: uptime
[user@192.168.56.3] out:  10:49:46 up  1:20,  1 user,  load average: 0.00, 0.01, 0.02
[user@192.168.56.3] out:


Done.
Disconnecting from 192.168.56.3... done.
Disconnecting from 192.168.56.2... done.

パスワードは一度しか聞かれず、以降のログインにはそのパスワードが使われている。

ログイン/sudoパスワードを事前に設定する

ログイン/sudoパスワードは必要になったタイミングで問い合わせが行われるが、-Iオプションをつけることで事前にパスワードの問い合わせを行っておくことができる。

$ cat servers.txt | fab uptime -I
target hosts: ['user@192.168.56.2', 'user@192.168.56.3']
Initial value for env.password:
[user@192.168.56.2] Executing task 'uptime'
[user@192.168.56.2] run: uptime
[user@192.168.56.2] out:  10:53:33 up 19:11,  2 users,  load average: 0.00, 0.01, 0.05
[user@192.168.56.2] out:

[user@192.168.56.3] Executing task 'uptime'
[user@192.168.56.3] run: uptime
[user@192.168.56.3] out:  10:53:33 up  1:24,  1 user,  load average: 0.00, 0.01, 0.02
[user@192.168.56.3] out:


Done.
Disconnecting from 192.168.56.3... done.
Disconnecting from 192.168.56.2... done.

並列実行する

複数サーバに対し並列でタスクを実行したい場合は、関数に@parallelデコレータをつける。 たとえば、fabfile.pyの内容を次のようにする。

from fabric.api import *
import sys

lines = sys.stdin.read().splitlines()
env.hosts = filter(bool, lines)
print "target hosts: %r" % env.hosts

@parallel
def apt_update():
    with hide('stdout'):
        sudo('apt-get update', pty=False)

ここで、上の例のように途中でsudoコマンドなどにより入力が求められる場合、並列実行は失敗してしまう。 この問題は、-Iオプションをつけ事前にパスワードを設定しておくことで対処することができる。

$ cat servers.txt | fab apt_update -I
target hosts: ['user@192.168.56.2', 'user@192.168.56.3']
Initial value for env.password:
[user@192.168.56.2] Executing task 'apt_update'
[user@192.168.56.3] Executing task 'apt_update'
[user@192.168.56.3] sudo: apt-get update
[user@192.168.56.2] sudo: apt-get update

Done.

ファイルをアップロードする

put関数を用いることで、SFTPによりローカルにあるファイルをリモートサーバの指定したパスに置くことができる。

ファイルの内容を目的に合わせた形で書き換える

fabric.contrib.files.*で定義された関数を使うことで、ファイルの内容を直感的に書き換えることができる。 たとえば、append関数は指定した文字列を行としてファイルに追記するが、すでに同一の内容の行が存在している場合は何もしない。

凡例

いくつかの具体的なタスクについて、fabfile.pyを書いてみる。

import sys
from fabric.api import *
from fabric.contrib import files

lines = sys.stdin.read().splitlines()
env.hosts = filter(bool, lines)
print "target hosts: %r" % env.hosts

def uptime():
    sudo('uptime')

def sshd_dnsno():
    files.comment('/etc/ssh/sshd_config', '^UseDNS yes', use_sudo=True)
    files.append('/etc/ssh/sshd_config', 'UseDNS no', use_sudo=True)
    sudo('service ssh restart')

def update_hosts():
    sudo(r'sed -i -E "s/^(127.0.1.1\s+).*/\1$(hostname)/" /etc/hosts')

@parallel
def apt_install(name):
    with hide('stdout'):
        sudo('apt-get update', pty=False)
    sudo("apt-get install -y %s" % name, pty=False)

def do_something():
    with cd('/tmp'):
        run('pwd')

それぞれのタスクの内容は次の通り。

  • uptime: 疎通確認も兼ねたテスト用
  • sshd_dnsno: SSHサーバのDNS逆引きをオフにする
  • update_hosts: /etc/hostsで自分のホスト名とIPアドレスの対応が一致していないものを直す
  • apt_install: 引数で指定したパッケージをインストールする
  • do_something: 特定のディレクトリに移動して何かをする場合のテンプレート

関連リンク