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

文系プログラマによるTIPSブログ

文系プログラマ脳の私が開発現場で学んだ事やプログラミングのTIPSをまとめています。

【デプロイ】Fabricを学ぶ:Vol.01:基本的な使い方を学ぶ【python】

python fabric デプロイ

f:id:treeapps:20140204002445j:plain
javabashしか書けない私ですが、とうとうFabricの実践投入を進め始めたので、記事を書いてみようと思います。目標は「デプロイスクリプトに一切bashを使わない」です。

Fabricとは?

Welcome to Fabric’s documentation! — Fabric documentation
Fabricは、サーバのコマンドをPythonで行うツールです。
主にデプロイ用途で使われる場合がほとんどのようです。

競合ツールとして、Ruby製のCapistrano(カピストラーノ)があります。
A remote server automation and deployment tool written in Ruby.

両者の違いは以下の通りです。

名称 使用言語 インストール 習得
Fabric python 難しい 簡単
Capistrano ruby 簡単 大変

capistranoは色々覚える事があります。
fabricは覚える事は少ないです。

capistranoのインストールは簡単でした。
ruby1.8.7以上でruby gemsを入れて簡単にインストールできました。

一方Fabricですが、macの場合はbrew install easy_installでいとも簡単にインストールできます。

FabricをCentOSにインストール

macだとhomebrewで簡単にインストールできますが、linuxへのインストールは結構色々しないといけません。

依存ライブラリのインストール

sudo yum install zlib-devel sqlite-devel openssl-devel

python2.7のインストール

古いCentOSの場合、python2.4が標準インストールされています。
これと競合しないように、/usr/local/python2.7にインストールしていきます。

cd /usr/local/src
sudo wget --no-check-certificate http://www.python.org/ftp/python/2.7.7/Python-2.7.7.tgz
sudo tar zxvf ./Python-2.7.7.tgz
cd ./Python-2.7.7
sudo ./configure --prefix=/usr/local/python-2.7 --enable-shared
sudo make
sudo make install

python2.7のバイナリのパスを通す

sudo ln -s /usr/local/python-2.7/bin/python /usr/local/bin/python
sudo ln -s /usr/local/python-2.7/lib/libpython2.7.so.1.0 /lib64/

一旦sshを抜けて、再度sshして python -V でバージョンを確認すると、2.7になります。

easy_install-2.7のインストール

easy_install2.7はインストール直後だとパスが通ってないので、通します。

wget --no-check-certificate http://pypi.python.org/packages/source/d/distribute/distribute-0.6.35.tar.gz
tar xf distribute-0.6.35.tar.gz
cd distribute-0.6.35
sudo /usr/local/python2.7/bin/python2.7 setup.py install
sudo ln -sf /usr/local/python-2.7/bin/easy_install-2.7 /usr/bin/easy_install-2.7
easy_install-2.7 --help

fabricのインストール

pipからもインストールできますが、今回はeasy_installでfabricをインストールします。
easy_install2.7と同様にパスが通ってないので通します。

sudo easy_install-2.7 fabric
sudo ln -sf /usr/local/python-2.7/bin/fab /usr/bin

fabricのバージョンを確認

$ fab -V
/usr/local/python-2.7/lib/python2.7/site-packages/pycrypto-2.6.1-py2.7-linux-x86_64.egg/Crypto/Util/number.py:57: PowmInsecureWarning: Not using mpz_powm_sec.  You should rebuild using libgmp >= 5 to avoid timing attack vulnerability.
  _warn("Not using mpz_powm_sec.  You should rebuild using libgmp >= 5 to avoid timing attack vulnerability.", PowmInsecureWarning)
Fabric 1.9.0
Paramiko 1.14.0

警告が出ていますが、動作に影響は無いので無視してOKです。

fabricを実行してみる

test.py
from fabric.api import run

def getHostname():
  run("hostname")
test.pyを実行

以下のようにして実行できます。

fab -H localhost -u ユーザ名 -f test.py getHostname

Fabricの基本的な使い方を学ぶ

対象ホスト・ID・PASSの指定方法

2パターンあります。

fabコマンドの引数で指定する

以下のようにfabコマンド時に引数で指定する事ができます。
複数のホストに対して同じコマンドを実行する場合は半角カンマ区切りでいけます。

fab -H localhost -u user -p pass -f deploy.py taskName
*.pyでenvで指定する

以下のようにデプロイスクリプト内で、暗黙の組み込み変数「env」で指定する事もできます。

#coding:utf-8
from fabric.api import env

env.hosts = ['127.0.0.1']
env.user = 'user'
env.password = 'pass'

sshする

#coding:utf-8
from fabric.api import run

def doCmd():
    run('ls -lh')

runコマンドがsshになります。実行するには以下のようにコマンドを実行します。

fab -f deploy.py doCmd

sudoでsshする

#coding:utf-8
from fabric.api import sudo

def doCmd():
    sudo('/etc/init.d/httpd reload')

たったこれだけ!!
このsudoコマンドが非常に便利です!!
今までexpectで対話を無理やり突破していたあなた!!
もうそんな事をやめてFabricのsudoコマンドで楽になって下さい!!

SFTPする(ファイルを送る)

#coding:utf-8
from fabric.api import put

def doCmd():
    put('/var/lib/jenkins/workspace/deploy/build/dist/distribution.zip', '/var/deploy/dist')

こんな感じです。マニュアルにはSFTPプロトコルを使っていると書かれていました。
sudoでputするオプション、chmodするオプションもあり、中々有能です。

SFTPする(ファイルを取得する)

#coding:utf-8
from fabric.api import get

def doCmd():
    get('/usr/tomcat/logs/catalina.out', '/home/hoge')

putの逆ですね。

コマンドの実行結果をpythonの変数にセットする

#coding:utf-8
from fabric.api import run

def doCmd():
    result = run('wc -l /usr/tomcat/logs/catalina.*')
    if (7 < int(result):
        print 'tomcatのログが一杯あるから消して!!'

runの返り値に標準出力がそのまま入っているようです。

cdを維持してコマンドを実行する

cdした後にrmしてmkdirして、等といった作業を継続したい場合に有効なコマンドがあります。

#coding:utf-8
from fabric.api import run, cd

def doCmd():
    with cd('/usr/tomcat/webapps')
        run('rm -rf ROOT.war')
        run('cp /var/deploy/dist/ROOT.war .')
        run('unzip -oq ROOT.war -d ROOT')

with と cdを組み合わせる事でcdを継続させる事ができます。
pythonのインデントと見事にマッチしており、cdしている箇所が大変解りやすくなっています。

今実行しているサーバのホストを取得する

#coding:utf-8
from fabric.api import env

def doCmd():
    if env.host == '127.0.0.1':
        print 'ローカルの場合は◯◯の処理をスキップします。'

env.hosts等で指定したホストの値がそのまま「env.host」で取得できます。

コマンドがエラーコードを返したら即座に中断

#coding:utf-8
from fabric.api import env

env.warn_only = False

env.warn_onlyはデフォルトでFalseになっており、例えばlsしたファイルが存在しない場合は即座にabortされます。abortされると以降の処理は全て中断されます。

コマンドのエラーハンドリングを手動で行う

#coding:utf-8
from fabric.api import env

env.warn_only = True

def doCmd():
    isAlreadyBuild = run('! test -d /var/build/2014-02-03')
    if isAlreadyBuild.failed:
        abort('{0}の{1}は既に存在します。'.format(env.host, '/var/build/2014-02-03'))

env.warn_onlyを有効にすると、前述の存在しないファイルにlsしてもabortされず、後続する処理は続行されます。勿論そのままだと問題なので、↑フォルダが存在する場合は◯◯する、等のエラーハンドリングを自分で行う事ができます。エラーかどうかはfailedで判定可能です。

コマンドのエラーコードを取得する

エラーコードは「exit 1」の事です。

#coding:utf-8
from fabric.api import put, env

env.warn_only = True

def doCmd():
    result = put('/var/lib/jenkins/workspace/deploy/build/dist/distribution.zip', '/var/deploy/dist')
    if result.failed:
        abort('{0}へのSFTPに失敗しました。エラーコード={1}'.format(env.host, result.error_code))

result.error_codeでエラーコードを取得できます。

コマンドに色(カラー)を付ける

一瞬???と思いますね。
なんとFabricは出力するコンソールの文字列の色を7色自由に変える事ができます。

  • blue
  • cyan
  • green
  • magenta
  • red
  • white
  • yellow

実際に実行すると、以下のようにカラフルに出力できます。
f:id:treeapps:20140203234610p:plain
実行したコードは以下の通りです。

#coding:utf-8
from fabric.colors import blue, cyan, green, magenta, red, white, yellow

def color():
    print '指定無し'
    print blue('blueを指定') + 'してみました。'
    print cyan('cyanを指定') + 'してみました。'
    print green('greenを指定') + 'してみました。'
    print magenta('magentaを指定') + 'してみました。'
    print red('redを指定') + 'してみました。'
    print white('whiteを指定') + 'してみました。'
    print yellow('yellowを指定') + 'してみました。'

成功したコマンドはグリーン、警告はイエロー、アボートはレッド、と色を変えると視覚的に結果が把握できるのでオススメです。このcolorコマンドは見ての通り部分的に適用できるのも面白いですね。

このカラー機能はjenkinsのコンソール表示にも有効です。
AnsiColor Plugin - Jenkins - Jenkins Wiki
このAnsiColorプラグインを入れた状態でカラーコマンドを実行すると、jenkinsのログがカラー表示されます!!視覚的に成功か失敗かを判別できるので、カラー機能を使う場合は絶対入れておきたいプラグインです!!

Fabricでハマった点と対応策

abortコマンドが?

最初はabortする際に以下のようなコードを書いていました。

#coding:utf-8
from fabric.api import env

def abort():
    abort('ホストは{0}です。'.format(env.host))

これの実行結果は以下の通りです。

treemacpro:work tree$ fab -H localhost -f abort.py abort
[localhost] Executing task 'abort'
Traceback (most recent call last):
  File "/Library/Python/2.7/site-packages/Fabric-1.8.0-py2.7.egg/fabric/main.py", line 743, in main
    *args, **kwargs
  File "/Library/Python/2.7/site-packages/Fabric-1.8.0-py2.7.egg/fabric/tasks.py", line 368, in execute
    multiprocessing
  File "/Library/Python/2.7/site-packages/Fabric-1.8.0-py2.7.egg/fabric/tasks.py", line 264, in _execute
    return task.run(*args, **kwargs)
  File "/Library/Python/2.7/site-packages/Fabric-1.8.0-py2.7.egg/fabric/tasks.py", line 171, in run
    return self.wrapped(*args, **kwargs)
  File "/Users/tree/work/abort.py", line 5, in abort
    abort('ホストは{0}です。'.format(env.host))
TypeError: abort() takes no arguments (1 given)

あれ、abortのメッセージがformatで展開されてない?あれ???

原因はまあ簡単で、以下のようにすると解消します。

#coding:utf-8
from fabric.api import env
from fabric.utils import abort

def abort():
    abort('ホストは{0}です。'.format(env.host))

(;^ω^)あ、fabricのabort呼べてなかったのか・・
という事で、きっちり「from fabric.utils import abort」でfabricのabortが呼ばれるようにしましょう。

tomcatの起動コマンドが終了しない

以下のように起動スクリプトで起動しようとしました。

def startTomcat():
    run('/etc/init.d/tomcat start')

結果は・・・正常起動したようにみせかけてすぐダウンするのです。
ptyオプションをFalseにすると、tomcatが起動しだしました。

def startTomcat():
    run('/etc/init.d/tomcat start', pty = False)

が、なんかfabコマンドが一向に終了してくれません。あれ???
ぐぐってみると、以下の記事に答えが書いてあります。

webアプリのデプロイにjenkinsを利用するように変更しているのだが、タイトルのような状態に。これだといつまでたってもジョブが終わらない><

すべてのtomcatがそうではなく、一部のtomcatがstartup.shの応答を返さない。

その差を見ると、/bin/catalina.shのログを出力する箇所に差があった。

応答を返すもの

>> "$CATALINA_BASE"/logs/catalina.out 2>&1 &
応答を返さないもの

2>&1 | /usr/sbin/rotatelogs "$CATALINA_BASE"/logs/catalina.out.%Y%m%d 86400 540 &

http://d.hatena.ne.jp/n593977/20120315/1331781713

お?どうもFabricの問題ではなくtomcatの起動スクリプトに問題がありそう??
もっと調べてみると以下が見つかりました。

次のコマンドでは、ssh の実行がすぐ終了することを期待していますが、実際には 10 秒待たされます。
% ssh localhost "nohup sleep 10 &"
# 10秒 待たされる
%
解決方法
標準入力、標準出力、標準エラー出力 (入出力ストリーム)を リダイレクト すると期待通り動きます。
% ssh localhost "nohup sleep 10 < /dev/null > /dev/null 2> /dev/null &"
原因
SSH はコネクションの切断を「入出力ストリーム が EOF を返すまで」待つそうです。
nohup で バックグラウンドプロセスを実行する場合、子プロセスが 入出力ストリーム を参照し続けるため、SSH がコネクションを切断しないということが起きています。
上記のように /dev/null など他のファイルを指定することにより、子プロセスが入出力ストリームの参照を持たないため、期待通りの実行結果となります。

http://d.hatena.ne.jp/takihiro/20081002/1222947161

ほっほーぅ。そうですか。
rotatelogsがバックグラウンド実行して延々と標準出力をリダイレクトしてるせいでfabコマンドが終わらないのか。試しにfabricを外して以下のようにリモートでtomcatを起動したところ、sshが終了しない状況が再現しました。

ssh /etc/init.d/tomcat start

入出力を切れば終了するのだな、よし。

def startTomcat():
    run('/etc/init.d/tomcat start < /dev/null > /dev/null 2> /dev/null', pty=False, timeout = 60)

よし、リモートでtomcatが起動できた。
(;^ω^)このやり方絶対間違ってるよな・・
標準出力を捨ててしまっているので実行結果が全く解らないという・・・
どなたかもっといい方法があれば是非教えて下さい。

雑感

Fabric、簡単に習得できますね。
なんかbashでデプロイスクリプトを作っていたのが本気でアホらしく感じました。
単なるpythonスクリプトなので、リストの作成もループも簡単です。

capistranoと比較して本当に覚える事は少ないので、次回はデプロイスクリプトbash使いたくないなと思っている方にうってつけです。capistranoは重厚さが導入の妨げに感じている人が多いかと思いますが、このFabricは見の通り軽量で簡単なので、是非使ってみて下さい!

次回はparallel(並列)デプロイも試したいところですが、並列でデプロイできるものってhtdocsくらいしか無いんですよね・・・





継続的デリバリー 信頼できるソフトウェアリリースのためのビルド・テスト・デプロイメントの自動化

継続的デリバリー 信頼できるソフトウェアリリースのためのビルド・テスト・デプロイメントの自動化

Quick backups with Fabric and Python (artymiak.com)

Quick backups with Fabric and Python (artymiak.com)

Railsデプロイ

Railsデプロイ