2026年9月4日
2026年9月4日
FabricでPythonデプロイスクリプトを書く方法|リリース切り替えとロールバック
Fabric は、SSH 越しにサーバーでコマンドを実行する処理を Python で書けるライブラリです。手順書を見ながら打っていたデプロイ作業を fab -H サーバー deploy の1コマンドにまとめ、失敗時には1つ前のリリースへ戻せるようにできます。この記事では、Fabric 2 系以降(現行の 3 系を含む)での fabfile の書き方、リリースディレクトリを切り替えるデプロイスクリプト、ヘルスチェックとロールバック、sudo と秘密情報の扱いまでを説明します。
Fabric のバージョンに注意する
検索すると古い記事が多く見つかりますが、Fabric 1 系と 2 系以降では書き方がまったく違います。
- Fabric 1 系:
from fabric.api import run, envのように書く。Python 2 時代の設計で、現在は使わない - Fabric 2 系以降:
from fabric import task, Connectionのように書く。Python 3 に対応し、内部で Invoke(タスク実行)と Paramiko(SSH)を使う
env.hosts や fabric.api が出てくるコードは 1 系向けなので、そのままでは動きません。Fabric3 という名前の非公式フォークもありますが、公式の後継ではない点に注意してください。この記事のコードは 2 系以降の書き方で、現行の 3 系でも使える API に絞っています(2026年10月現在)。
インストールと最初のタスク
手元のPCに、プロジェクトごとの仮想環境を作って入れます。
python3 -m venv .venv
source .venv/bin/activate
pip install fabric
fab --version
作業ディレクトリに fabfile.py を作り、接続確認用のタスクを書きます。
# fabfile.py
from fabric import task
@task
def uptime(c):
c.run("uptime")
c.run("df -h /", hide=False)
fab -H deploy@203.0.113.10 uptime
-H で接続先を指定すると、タスクの引数 c にはそのサーバーへの接続(Connection)が渡されます。fab --list で、定義したタスクの一覧を表示できます。
Fabric は既定で ~/.ssh/config を読み込むため、ホスト名・ユーザー・鍵ファイルを SSH の設定に書いておけば、fab -H web1 uptime のように短く指定できます。パスワードでのSSHログインではなく、鍵認証を使ってください。
デプロイの設計:releases と current
デプロイスクリプトを書く前に、サーバー上のディレクトリ構成を決めます。ここでは Capistrano などでも使われる、次の構成にします。
/srv/myapp/
├── current -> releases/20261018093000 # 公開中のリリース
├── releases/ # デプロイごとのディレクトリ
└── shared/
└── .env # 秘密情報など、リリースをまたぐファイル
新しいリリースは releases/ の下で準備し、すべて成功した最後に current のシンボリックリンクを切り替えます。準備の途中で失敗しても公開中のアプリには影響せず、戻すときはリンクを前のリリースへ向け直すだけで済みます。git pull をその場で行う方式だと、依存パッケージの更新に失敗したときに中途半端な状態で止まるため、この差は大きくなります。
アプリは systemd のサービス(例: myapp.service)として /srv/myapp/current から起動する前提です。
デプロイタスクを書く
# fabfile.py
import shlex
from datetime import datetime, timezone
from fabric import task
from invoke import Exit
APP_DIR = "/srv/myapp"
RELEASES = f"{APP_DIR}/releases"
CURRENT = f"{APP_DIR}/current"
REPO = "git@github.com:your-org/myapp.git"
SERVICE = "myapp"
KEEP_RELEASES = 5
def switch_to(c, release):
# 一時リンクを作ってから mv で置き換え、切り替えを一瞬で済ませる
c.run(f"ln -sfn {release} {APP_DIR}/current_tmp && mv -Tf {APP_DIR}/current_tmp {CURRENT}")
c.sudo(f"systemctl restart {SERVICE}")
def healthy(c):
result = c.run("curl -fsS --max-time 5 http://127.0.0.1:8000/healthz", warn=True, hide=True)
return result.ok
@task
def deploy(c, branch="main"):
ts = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")
release = f"{RELEASES}/{ts}"
previous = c.run(f"readlink {CURRENT} || true", hide=True).stdout.strip()
c.run(f"mkdir -p {RELEASES} {APP_DIR}/shared")
c.run(f"git clone --depth 1 --branch {shlex.quote(branch)} {REPO} {release}")
with c.cd(release):
c.run("python3 -m venv .venv")
c.run(".venv/bin/pip install --quiet -r requirements.txt")
c.run(f"ln -sfn {APP_DIR}/shared/.env .env")
switch_to(c, release)
if not healthy(c):
if previous:
print("ヘルスチェックに失敗したため、直前のリリースに戻します")
switch_to(c, previous)
raise Exit("デプロイに失敗しました。ログを確認してください", code=1)
cleanup(c)
print(f"デプロイ完了: {release}")
@task
def cleanup(c):
# 新しい順に並べ、KEEP_RELEASES 件より古いものを削除する
c.run(f"cd {RELEASES} && ls -1t | tail -n +{KEEP_RELEASES + 1} | xargs -r rm -rf --")
実行例です。タスクの引数は --引数名 で渡します。
fab -H deploy@203.0.113.10 deploy
fab -H deploy@203.0.113.10 deploy --branch=release-1.2
コードの要点を順に説明します。
- **ブランチ名を
shlex.quoteで囲む**:コマンドライン引数をそのまま文字列に埋め込むと、想定外の文字でシェルのコマンドが変わってしまいます。外から受け取る値はクォートしてください。 - リリースごとに仮想環境を作る:時間はかかりますが、前のリリースの環境を壊さずに済みます。時間が気になる場合は、依存パッケージのキャッシュを使う、ビルド済みの成果物を配る、などの方法があります。
- **
warn=True**:コマンドが失敗しても例外にせず、結果(result.ok)で判定します。それ以外のc.runは、失敗すると例外が出てタスクが止まります。途中で止まってもcurrentは切り替わっていないので、公開中のアプリはそのまま動きます。 - **
lnとmv -T**:リンクを直接上書きするより、一時リンクを作って置き換えるほうが、切り替え中にリンクが存在しない瞬間を作りません(mv -Tは GNU coreutils のオプションです)。
cleanup は rm -rf を使うので、RELEASES の値が誤っていると関係のないファイルを消すおそれがあります。最初は検証用サーバーで、rm -rf を echo に置き換えて対象を確認してから使ってください。
データベースのマイグレーションがあるアプリでは、switch_to の前にマイグレーションを実行するタスクを追加します。その場合、コードを戻してもデータベースの構造は戻らないため、古いコードでも動く形でマイグレーションを分けて出すのが安全です。本番データベースに変更を加える前には、バックアップを取っておいてください。
手動ロールバックのタスク
ヘルスチェックでは検出できない不具合に備えて、手動で1つ前に戻すタスクも用意します。
@task
def rollback(c):
current = c.run(f"readlink {CURRENT}", hide=True).stdout.strip()
releases = sorted(c.run(f"ls -1 {RELEASES}", hide=True).stdout.split())
paths = [f"{RELEASES}/{r}" for r in releases]
if current not in paths or paths.index(current) == 0:
raise Exit("戻せる前のリリースがありません", code=1)
target = paths[paths.index(current) - 1]
switch_to(c, target)
print(f"ロールバック完了: {target}")
リリース名を日時にしているので、名前順に並べればそのまま古い順になります。cleanup で残す件数を決めておくと、何世代前まで戻れるかが明確になります。
sudo と秘密情報の扱い
c.sudo() はサーバー上で sudo を使ってコマンドを実行します。パスワードを求められる設定なら、fab --prompt-for-sudo-password で実行時に入力できます。パスワードを fabfile や設定ファイルに書くのは避けてください。
自動化を進めるなら、デプロイユーザーに必要なコマンドだけをパスワードなしで許可する方法が扱いやすいです。visudo -f /etc/sudoers.d/deploy で、たとえば次のように限定します。
deploy ALL=(root) NOPASSWD: /usr/bin/systemctl restart myapp
アプリの秘密情報(データベースのパスワードや API キー)は、リポジトリに入れず、サーバーの shared/.env に置いてリリースからリンクします。ファイルの権限は 600 にし、Git の .gitignore にも .env を入れておきます。
複数台へのデプロイ
-H にカンマ区切りで複数のホストを指定すると、ホストごとに順番にタスクが実行されます。
fab -H web1,web2 deploy
1台目で失敗すると例外で止まるため、残りのサーバーには古いリリースが残ります。ロードバランサーの配下にある構成なら、1台ずつ外して更新する流れにすると、利用者への影響を抑えられます。台数が増えたり、サーバーの構築まで自動化したくなったりした場合は、Ansible などの構成管理ツールへの移行も検討してください。
まとめ
- Fabric 1 系と 2 系以降は書き方が違う。
from fabric import taskの形で書く releases/で準備し、最後にcurrentを切り替える構成にすると、失敗しても公開中のアプリは止まらない- 外から受け取る値は
shlex.quoteで囲み、失敗を判定したい箇所だけwarn=Trueを使う - ヘルスチェックに失敗したら自動で前のリリースへ戻し、手動の
rollbackタスクも用意する - sudo は必要なコマンドだけ許可し、秘密情報は
shared/.envに置いてリポジトリに入れない