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 に置いてリポジトリに入れない

お気軽にご相談ください

お見積りへ お問い合わせへ
▲