Skip to content

本番環境のセットアップ

gunicorn、nginxまたはApache、TLS終端、Docker、セッションの永続化、そして共有サーバーに必要なアクセス制御を使って、Potatoを本番環境にデプロイします。

複数のユーザー、データの永続化、適切なセキュリティを備えた形で、実際のアノテーションタスクのためにPotatoをデプロイします。

サーバー設定

基本的な本番設定

yaml
server:
  host: 0.0.0.0
  port: 8000
  debug: false

serverブロックが受け付けるのはhost、port、debugだけです。ワーカー数とタイムアウトは設定ファイルではなく、Potatoを実行するWSGIサーバーの側で指定します。本番環境ではdebugをオフにしておいてください。デバッグモードでは、サーバー自身のマシンからのリクエストに対して管理ページがキーなしで開き、デプロイ先のホストではそこにリバースプロキシからのリクエストも含まれるためです。

HTTPSの使用

Potatoの設定にはTLSの項目がないため、証明書は設定ファイルでは指定しません。TLSはPotatoの前に置くリバースプロキシで終端します。以下のnginxとApacheのセクションがその設定です。あるいは、「SSL付き」で示すように証明書をgunicornに直接渡します。

Gunicornでの実行

Potatoはgunicornのワーカーをちょうど1つにして実行し、同時処理を増やしたいときはスレッド数を上げます。

bash
gunicorn "potato.flask_server:create_app('config.yaml')" \
  --workers 1 \
  --threads 8 \
  --bind 0.0.0.0:8000 \
  --timeout 120

ワーカーを1つにするのはチューニング上の選択ではなく、正しく動作させるための要件です。Potatoはアイテムプール、割り当てキュー、すべてのアノテーターの状態をプロセス内のメモリに保持しています。2つ目のワーカーはこの3つを自分用に別々に持つため、1つ目のワーカーがすでに割り当てたインスタンスを配布してしまいます。さらにuser_state.jsonは保存のたびに全体が書き直されるので、最後に保存したワーカーがもう一方のアノテーションを破棄します。この損失はどこにも報告されず、アノテーションはそのまま失われます。スレッドはその状態の1つのコピーを共有するため、--workersとは違い、--threadsなら安全に同時処理を増やせます。

SSL付き

bash
gunicorn "potato.flask_server:create_app('config.yaml')" \
  --workers 1 \
  --threads 8 \
  --bind 0.0.0.0:443 \
  --certfile /path/to/cert.pem \
  --keyfile /path/to/key.pem

リバースプロキシの設定

Nginxの設定

nginx
upstream potato {
    server 127.0.0.1:8000;
}
 
server {
    listen 80;
    server_name annotation.example.com;
    return 301 https://$server_name$request_uri;
}
 
server {
    listen 443 ssl;
    server_name annotation.example.com;
 
    ssl_certificate /etc/letsencrypt/live/annotation.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/annotation.example.com/privkey.pem;
 
    location / {
        proxy_pass http://potato;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
 
        # Long annotation sessions
        proxy_read_timeout 300;
        proxy_connect_timeout 300;
        proxy_send_timeout 300;
    }
}

Apacheの設定

apache
<VirtualHost *:443>
    ServerName annotation.example.com
 
    SSLEngine on
    SSLCertificateFile /path/to/cert.pem
    SSLCertificateKeyFile /path/to/key.pem
 
    ProxyPreserveHost On
    ProxyPass / http://127.0.0.1:8000/
    ProxyPassReverse / http://127.0.0.1:8000/
 
    <Proxy *>
        Order allow,deny
        Allow from all
    </Proxy>
</VirtualHost>

URLパスの下にマウントする

Potatoをドメインのルートではなくhttps://host/app1/で提供するには、手順が1つ増えます。アノテーションページは/static/...と/updateinstanceを公開ルートからの相対パスでリクエストするため、パスプレフィックスの下では、ページの枠組みは表示されてもアセットが404になり、アノテーションの保存に失敗します。

Potatoに公開プレフィックスを伝えます。プロキシを自分で管理していて、転送ヘッダーを送れる場合は次のようにします。

bash
export POTATO_PROXY_FIX=1
potato start config.yaml -p 8000
nginx
location /app1/ {
    proxy_pass         http://127.0.0.1:8000/;   # trailing slash strips /app1/
    proxy_set_header   Host              $host;
    proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header   X-Forwarded-Proto $scheme;
    proxy_set_header   X-Forwarded-Prefix /app1;
}

POTATO_PROXY_FIXは、アプリにそのプロキシ経由でしか到達できない場合にだけ有効にしてください。この設定は転送ヘッダーを信頼するため、内部ポートに直接到達できるクライアントがプレフィックスを偽装し、生成されるすべてのURLを汚染できてしまいます。

ヘッダーを追加できないもののマウントパスはわかっている場合は、それを直接設定します。この場合も、プロキシは転送前にプレフィックスを取り除く必要があります。

bash
export POTATO_URL_PREFIX=/app1
potato start config.yaml -p 8000

セッションのないブラウザでサインインしてテストしてください。プレフィックスが欠けているときに最初に壊れるのはログインフォームのページです。

ライブエージェントとライブコーディングのビューアーはServer-Sent Eventsでストリーミングしますが、バッファリングするプロキシはストリームが終わるまでその内容を送りません。これらのルートではバッファリングをオフにしてください。

nginx
location /app1/api/ {
    proxy_pass            http://127.0.0.1:8000/api/;
    proxy_set_header      Host $host;
    proxy_buffering       off;
    proxy_read_timeout    3600s;
}

Dockerでのデプロイ

Potatoはビルド済みのイメージをGitHub Container Registryのghcr.io/davidjurgens/potatoで公開しています。中身はPotatoとその依存関係だけです。config.yamlとそのデータを含むプロジェクトフォルダーを/appにマウントするので、1つのイメージですべてのタスクを扱えます。

bash
docker run -p 8000:7860 \
  -v "$PWD/myproject:/app" \
  -e POTATO_SECRET_KEY="$(openssl rand -hex 32)" \
  ghcr.io/davidjurgens/potato:latest

latestタグと、2.12.1のようなリリースタグには、コアの依存関係だけが入っています。-allタグ(latest-all、2.12.1-all)はAI SDK、フォーマットリーダー、エクスポートと認証の追加パッケージを加えたもので、ビジョンモデルは含みません。

コンテナは、上のgunicornのセクションで説明した理由から、gunicornのワーカー1つとスレッド8つでタスクを提供します。GUNICORN_WORKERSを1より大きくすると、POTATO_ALLOW_MULTIWORKER=1も設定しない限り、コンテナは起動時に停止します。この変数を設定すると、実際の研究ではアノテーションが失われます。同時処理を増やすにはGUNICORN_THREADSを上げてください。POTATO_CONFIGでconfig.yaml以外の設定ファイルを指定でき、PORTでコンテナ内のポートを7860から変更できます。

コンテナはuid 1000として実行され、アノテーションの出力、potato.log、SQLiteデータベースを/appに書き込みます。Linuxでは、別のユーザーが所有するプロジェクトフォルダーをバインドマウントすると、サーバーは起動時に原因を示すエラーを出して停止します。sudo chown -R 1000:1000 myprojectでフォルダーの所有者をuid 1000に変更してください。MacのDocker Desktopはバインドマウントの所有者を無視するため、このエラーが出るのはLinuxだけです。

イメージ名の後に続く引数はサーバーのコマンドを置き換えます。一回限りのチェックに便利です。

bash
docker run -v "$PWD/myproject:/app" \
  ghcr.io/davidjurgens/potato:latest potato validate config.yaml --strict

Docker Compose

yaml
services:
  potato:
    image: ghcr.io/davidjurgens/potato:latest
    ports:
      - "8000:7860"
    volumes:
      - ./myproject:/app
    environment:
      - POTATO_SECRET_KEY=${POTATO_SECRET_KEY}
      - POTATO_ADMIN_API_KEY=${POTATO_ADMIN_API_KEY}
    restart: unless-stopped
 
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./certs:/etc/ssl/certs
    depends_on:
      - potato
    restart: unless-stopped

Composeネットワークの中では、nginxはサービス名とコンテナのポートでPotatoに到達します。そのため、上のリバースプロキシのセクションにあるupstreamブロックは、server 127.0.0.1:8000;ではなくserver potato:7860;になります。

プロジェクトフォルダーはイメージにコピーされるのではなくマウントされるため、Potatoがそこに書き込んだアノテーションは、コンテナを置き換えてもホスト上に残ります。

アクセス制御

登録できるユーザーを制限する

Potatoはデフォルトで自己登録を許可しています。公開サーバーでは、サインアップできるユーザーを列挙してください。

yaml
user_config:
  allow_all_users: false
  users:
    - annotator1@example.com
    - annotator2@example.com

どちらのキーもuser_configの下に置きます。トップレベルに書くと無視されるため、アクセスリストのように見えても実際には全員を受け入れてしまいます。

管理者アクセス

admin_api_keyはX-API-Keyヘッダーで送る共有キーです。設定しない場合はPotatoが生成して{task_dir}/admin_api_key.txtに書き込むので、本番ホストでは自分で設定し、リポジトリには含めないでください。

yaml
admin_api_key: "your-shared-key"
 
rbac:
  enabled: true
  user_role_assignments:
    lead@example.com: admin

rbacを設定すると、指名された人は共有キーを受け渡すのではなく、通常どおりログインして/adminにアクセスできます。権限の一覧はユーザー管理を参照してください。

セッション

yaml
secret_key: "a-long-random-string"
session_timeout_minutes: 60
persist_sessions: true
session_lifetime_days: 7
max_session_seconds: 7200

secret_keyはセッションCookieに署名するキーで、代わりにPOTATO_SECRET_KEYから与えることもできます。設定しないと、Potatoは起動のたびに新しいキーを生成するため、再起動のたびに作業中の全員がログアウトされます。デプロイのたびに再起動するサーバーでは、アノテーションが失われる原因として最も多いのがこの設定です。

session_timeout_minutesは、その時間アイドル状態が続いたセッションを消去します。時間はリクエストのたびにリセットされます。この設定が実際に適用されるのはPotato 2.11が最初のリリースで、デフォルトは480分です。この例のように60にすると、1時間席を外したアノテーターは、戻ったときに再ログインが必要になります。

アノテーションの出力

yaml
output_annotation_dir: /var/potato/annotations/
export_annotation_format: [csv, jsonl]
auto_export_interval: 300

Potatoはoutput_annotation_dirの下にアノテーターごとに1つのディレクトリを作り、それぞれにその人のuser_state.jsonを置きます。export_annotation_formatは定期エクスポートで書き出す形式を指定し、空の値にすると自動エクスポートは一切行われません。auto_export_intervalはエクスポートの間隔を秒単位で指定し、デフォルトは60です。

データベースバックエンド

アノテーションはデフォルトでディスクに保存されます。データベースが必要なデプロイでは、database.typeにmysqlまたはfileを指定できます。

yaml
database:
  type: mysql
  host: localhost
  port: 3306
  database: potato_db
  username: potato
  password: ${DB_PASSWORD}

キーはnameとuserではなく、databaseとusernameです。接続文字列は代わりにPOTATO_DB_CONNECTIONから与えることもでき、そうすれば認証情報を設定ファイルに書かずに済みます。

ドライバーはpip install 'potato-annotation[mysql]'でインストールしてください。2.10.2より前はMySQLバックエンドが一度も有効にならず、MySQLを指定した設定でも、アノテーションは何の警告もなくディスクに保存されていました。2.10.2以降は、ドライバーがない場合やサーバーに接続できない場合に起動が停止し、エクスポート、バックアップ、potato deploy pullはデータベースを読みます。

ヘルスチェックとログ

PotatoはすべてのデプロイでGET /healthを提供しています。このルートは認証を必要としないため、ロードバランサーやオーケストレーターのレディネスプローブとして使えます。

bash
curl -sf https://annotation.example.com/health

ログの詳細度は、loggingブロックではなく、設定ファイルまたはコマンドラインで指定します。

yaml
verbose: true
debug_log: server

debug_logはall、ui、server、noneのいずれかを取ります。1つのサブシステムを調べているときは、環境変数POTATO_LOGGERSで出力を名前付きのロガーに絞り込めます。

スケーリング

Potatoはアイテムセット全体をメモリに保持する単一プロセスとして動作するため、サーバーの台数ではなくマシンの性能を上げてください。ロードバランサーの後ろで複数のレプリカを動かしても、作業は分割されません。各レプリカが割り当てキューとアノテーターの状態をそれぞれ別に持つため、上のgunicornのところで説明したデータ損失がそのまま起こります。

データセットとともに増えるリソースはメモリです。アイテムが100万件前後になったら、ペイロードをSQLiteキャッシュに移せます。

yaml
item_store:
  backend: paged      # default: memory
  cache_size: 2048

有効にする前に測定してください。ページングによる削減はビジョンのアイテムで約28%、テキストで約51%で、桁違いに減るわけではありません。さらに、コーパスの読み込みは約2.5倍遅くなります。このファイルはキャッシュで、起動のたびにデータファイルから再構築されるため、削除しても安全です。

大規模なプロジェクトで効果があるのは、per_annotator_quotaで1人のアノテーターに割り当てる量に上限を設けること、batch_assignmentでコーパスをコホートに分割すること、そして起動時にすべてを読み込むのではなくdata_directoryでデータファイルを順次追加することです。

バックアップ戦略

バックアップの対象は、アノテーションの出力ディレクトリだけでは足りません。タスクディレクトリには最大で3つのSQLiteデータベースもあり、どれもデータファイルから作り直すことはできません。project.sqlite(メモ、コードブック、レビューのワークフロー)、datasets.sqlite、そしてラベリングエンジンを使っている場合のengine.sqliteです。サインアップフォームから作成されたアカウントは、Potatoがuser_config.jsonに保存します。サーバーが書き込んでいる間、データベースはWALモードで動いているため、データベースファイルをtarやcpでコピーすると、壊れたコピーや直近の作業が抜け落ちたコピーができることがあり、どちらのコマンドも警告を出しません。

組み込みのバックアップ

バージョン2.10以降、Potatoのbackupブロックは、アノテーションの出力とデータベースの.backupスナップショットをサーバーの外にコピーし、サーバーが空のディスクで起動したときにそれを復元します。復元ではアノテーションと一緒にアカウント一覧も戻り、ディスク上にすでにあるアノテーションが上書きされることはありません。下のブロックは、デフォルトの間隔である5分ごとにコピーをS3バケットに送ります。そのため、ディスクを失ってもアノテーターが失う作業は最大で5分です。

yaml
backup:
  schedule_minutes: 5
  restore_on_boot: true
  sinks:
    - type: s3
      bucket: lab-potato-backups
      prefix: pilot-study
      endpoint_url: https://s3.example.edu   # leave out for AWS S3

endpoint_urlを使うと、大学のオブジェクトストア、MinIO、Cloudflare R2、Backblaze B2など、S3 APIに対応した任意のサービスを送り先にできます。認証情報はサーバーの環境のPOTATO_S3_ACCESS_KEY_IDとPOTATO_S3_SECRET_ACCESS_KEY、またはboto3の通常の探索順序から取得されます。そのバケット1つに範囲を限定したキーを使えば、サーバーがアクセスできる範囲を制限できます。repo_idを付けたtype: huggingfaceの送り先を使うと、代わりにHF_TOKENを使って非公開のHugging Faceデータセットにコピーを送ります。どちらの送り先にもhosting extra(pip install 'potato-annotation[hosting]')が必要です。

自分のストレージに残すコピー

データ利用契約でデータをサーバーから出せない場合は、代わりに夜間のジョブでローカルストレージやマウントしたストレージにコピーできます。下のスクリプトは出力ディレクトリをアーカイブし、各データベースをファイルとしてコピーするのではなくSQLiteの.backupコマンドでスナップショットを取り、7日分のコピーを保持します。sqlite3コマンドラインツールが必要です。

bash
#!/bin/bash
# backup.sh
set -e
TASK_DIR=/var/potato
DEST=/var/backups/potato/$(date +%Y%m%d_%H%M%S)
mkdir -p "$DEST"
 
tar -czf "$DEST/annotations.tar.gz" -C "$TASK_DIR" annotations
for db in project.sqlite datasets.sqlite engine.sqlite; do
  if [ -f "$TASK_DIR/$db" ]; then
    sqlite3 "$TASK_DIR/$db" ".backup '$DEST/$db'"
  fi
done
cp "$TASK_DIR/config.yaml" "$DEST/"
if [ -f "$TASK_DIR/user_config.json" ]; then cp "$TASK_DIR/user_config.json" "$DEST/"; fi
 
find /var/backups/potato -mindepth 1 -maxdepth 1 -mtime +7 -exec rm -rf {} +

cronで毎晩午前2時に実行します。

bash
0 2 * * * /var/potato/backup.sh

夜間のコピーでは最大で1日分の作業を失う可能性があるため、アノテーターの多い研究の間はもっと頻繁に実行してください。研究を始める前に、コピーを1つテスト用のサーバーに復元し、最後のアノテーションが含まれていることを確認してください。

セキュリティチェックリスト

  • 有効な証明書を使い、プロキシでTLSを終端する
  • 再起動後もセッションが維持されるよう、secret_keyまたはPOTATO_SECRET_KEYを設定する
  • 生成されたファイルに頼らず、admin_api_keyを自分で設定する
  • user_config.usersで登録を制限する
  • デバッグモードではリバースプロキシからのリクエストがキーなしで管理ページに届くため、debugをオフにしておく
  • gunicornはワーカー1つで実行する
  • 認証情報は設定ファイルではなく環境変数に置く
  • POTATO_PROXY_FIXは信頼できるプロキシの後ろでのみ有効にする
  • 出力ディレクトリ、アカウントファイル、データベースをバックアップし、復元をテストする
  • 管理者ロールを持つユーザーを見直す

環境変数

bash
# .env file
POTATO_SECRET_KEY=your-super-secret-key
POTATO_ADMIN_API_KEY=your-admin-key
POTATO_DB_CONNECTION=mysql://potato:password@localhost:3306/potato_db

署名キーは次のコマンドで生成します。

bash
python3 -c 'import secrets; print(secrets.token_hex(32))'

Docker Composeで読み込みます。

yaml
services:
  potato:
    env_file:
      - .env

トラブルシューティング

ログの確認

bash
# Gunicorn logs
journalctl -u potato -f
 
# Docker logs
docker-compose logs -f potato

よくある問題

502 Bad Gateway:Potatoが実行中で、プロキシの転送先ポートにバインドされているか確認してください。

デプロイ後に全員がログアウトされる:secret_keyが設定されていないため、起動時に新しい署名キーが生成されています。

アノテーションが消える:ワーカー数を確認してください。gunicornのワーカーが2つ以上あると、何も報告されないままアノテーションが破棄されます。

パスプレフィックスの下でアセットが404になる:POTATO_PROXY_FIXまたはPOTATO_URL_PREFIXを設定し、プロキシが転送前にプレフィックスを取り除いていることを確認してください。

大規模なコーパスで遅い:サーバーを増やす前にメモリを増やし、アイテムが約100万件を超える場合はitem_store.backend: pagedを検討してください。

次のステップ

実装の詳細は、リバースプロキシのガイドとスケーリングのガイドを参照してください。