2025/8/26 zbpack 再デプロイ時の異常動作に関するインシデント報告

PanPan

最近、Zeabur は zbpack v1 既存インフラの廃止に伴い、新世代のビルドシステム(zbpack v2)への更新をすべてのユーザーにプッシュしています。移行の過程でいくつかの互換性の問題が判明しており、本記事では問題の背景、対応方法、そして同様の問題に遭遇した場合の緩和策について説明します。

なぜ新世代のビルドシステムを全量プッシュしたのか?

Zeabur の背後で動作するビルド基盤は、次のように設計・運用されています。

zbpack structure

「registry v1」は distribution/distribution を用いて構築していましたが、様々な問題が生じたため、自社設計の registry v2 への移行を進めました。

  • 初期は「ビルドマシンに随伴するレジストリ」(1 台のビルドマシンに 1 つのレジストリ)という方式でした。ビルドマシンは短命で、かつ「そのマシン上のコンテナが自分のレジストリのみに接続する」ことを強制する良い方法がなく、まれに「レジストリがビルド用コンテナより先に終了してしまう」ことでビルドが失敗する問題がありました(リトライも十分に効かない)。
  • その後はレジストリを長期運用のマシンへ移し、ビルドマシン上のコンテナはロードバランスされたレジストリへ直接接続する方式にしました。しかし、image manifest の書き込み時に、blob をアップロードしたレジストリ側で実はアップロードがまだ完了していなかったり、背後の Cloudflare R2 の整合に遅延があったりして、レジストリが blob unknown を返し manifest のアップロードを拒否し、起動時にイメージを取得できないことがありました。
  • さらに、distribution/distribution はグローバルな blob の重複排除を行う傾向があり、全 manifest を読み出さずに「不要な blob」を把握できませんでした。公式のガベージコレクションは stop-the-world が前提で、Zeabur 規模のレジストリでは実行不能です。その結果、R2 に blob が大量に蓄積し、深刻なパフォーマンス劣化を招いていました。
  • また、多くのユーザーから registry v1 経由のアップロードが非常に遅いというフィードバックもありました。

そこで registry v2 では、「OCI イメージをビルドした後、R2 バケットへ直接プッシュ」し、Cloudflare Workers を用いて、バケット内の OCI 構造を OCI Distribution Specification の Pull API と互換な read-only API として提供する設計にしました。これにより大幅な性能向上と multipart アップロードの最大化が可能になり、レジストリ由来の問題も回避できます。同時に、blob の重複排除をリポジトリ内に限定したため、「リポジトリを削除すれば関連する blob を GC できる」ようになり、stop-the-world なしで運用管理を大幅に簡素化できました。

ただし、この設計変更により registry v2 の push フローは大きく変わりました。zbpack v2 では実装済みですが、zbpack v1 は push フローが複雑で、かつ image ビルドを buildkit CLI に大きく依存しているため対応が難しく、直近 1 か月ほどは zbpack v1 のプロジェクトを引き続き zbpack v1(registry v1)でビルドし、ユーザーから「起動できない」という報告があった場合にのみ手動で zbpack v2 へ切り替える運用としていました。

しかし、registry v1 の負荷は増す一方で、エラー頻度も右肩上がりとなり、同様の問い合わせが急増したため、やむを得ず「イメージ関連の処理を zbpack v1 から zbpack v2 へ切り替える」判断を検討することになりました。

なぜ新しいビルドシステムで多くの問題が発生したのか?

鋭い開発者の皆さんはお気づきかもしれませんが、zbpack (v1) の GitHub リポジトリ は Archived からメンテナンス再開へと戻り、Dockerfile 関連の更新が多数入っています。これは、zbpack v1 を zbpack v2 へ接続するための「互換レイヤー」を準備していたためです。

私たちは、zbpack v1 の Dockerfile 生成機能は維持しつつ、Dockerfile 生成後のビルド処理は v1 の内蔵ロジックを使わずに v2 へ切り替える方針にしました。そこで、v1 の既存の Dockerfile 生成機能を公開関数化し、ビルドサービスがその関数で Dockerfile を生成してから、zbpack v2 を実行しているビルドマシンへ渡すようにしました。

zbpack migration plan

とはいえ、zbpack v1 は元来ビルドマシン上で動作する前提で設計されており、実装やブリッジが必要な部分が多数存在しました。

  • zbpack v1 は多くの環境変数に依存しており、ビルドサービス側の環境変数を変更することは現実的ではない。
  • zbpack v1 は本来「単発実行の CLI」として設計されており、ビルドサービスに直結すると内部の panic ロジックに当たる可能性がある。
  • zbpack v1 の背後で動くビルドマシンからは、zbpack v1 に多くの特化パラメータが渡されており、互換レイヤー側で一つひとつ実装する必要がある。

このため、私たちは zbpack v1 互換レイヤーを実装し、ビルドマシンからの v1 呼び出しを移植しました。コード読取りのような明白な問題は全体配信前に解決しており、テストマシンでの内部テストでも誤判定はありませんでした。また、ロールアウトの影響を継続監視する on-call エンジニアも配置していました。しかし実際に全マシンへ配信したところ、互換レイヤーで考慮漏れとなっていた問題が多数見つかりました。例えば次の通りです。

  • zbpack が必要とする環境変数が正しく伝搬されず、ZBPACK_ で始まる変数が無効になっていた。
  • プロジェクトの「ルートディレクトリ」が zbpack v1 に正しく伝わらず、常にルートの内容で判定してしまっていた。
  • 互換レイヤーで使用していた zbpack v1 のバージョンは Dockerfile 周りのロジックが大きく変更されており、テストが十分にカバーできていなかったため、ZBPACK_DOCKERFILE_NAME の挙動が以前と一致しなかった。
  • 多くのユーザーが重視する plan type / plan meta の表示が、互換レイヤーでは従来のように build logs に正しく出力されなかった。
  • 中国本土から registry v2 への接続が非常に遅かった。

これらは、対応するテスト環境を用意しにくいケースがあったこと、またロールバックの影響がより大きくなる可能性があったことも踏まえ、on-call 時には「修正 → テスト → 配信」を素早く回す判断をしました。同時に、お客様へは一時的な回避策(workaround)も提供しました。8/26〜8/27 の期間で、zbpack の互換レイヤーをおおむね完成させ、plan type / plan meta の表示にも対応し、さらに registry v2 のダウンロードを高速化するため本土向け CDN を設計・導入しました。

our new ui and infrastructure

互換レイヤーでカバーし切れていなかったエッジケースをご報告いただいたすべてのお客様に、心より感謝申し上げます。いただいたご報告により、私たちは迅速に調査・修正を進めることができました。

今後この問題は再発するのか?

上記で挙げた問題は、互換レイヤーにおいて実装済みまたは修正済みです。もし他にも不足があれば、ぜひチケットを作成いただき、エンジニアにお知らせください。

現在判明している既知の問題は次の通りです。

  • 新規デプロイ時に公開ポートがデフォルトの 8080 に戻ってしまうことがあります。原因は調査中です。暫定的に PORT=<公開ポート>(例: PORT=8080)を手動で設定することで緩和できます。
  • Dockerfile デプロイをご利用で、Dockerfile が正しく読み取られない場合は、「設定 > Dockerfile」で使用したい Dockerfile の内容を手動で入力してください。なお、この問題はすでに修正済みの見込みですので、原因調査のためチケットの作成にもご協力ください。

今回のインシデントの核心原因は、主に次の点にあります。

  • 内部テストのケースが不足しており、プロジェクトルートや Dockerfile 名に関するテストが欠けていたため、「互換レイヤーであらゆる環境をカバーできている」と誤認し、小さな変更として全体配信してしまった。
  • 挙動変更を伴う機能(例: 「zbpack v1 互換レイヤー」)は、zbpack v2 のように feature flag による選択的な有効化を実装・遵守すべきであり、一斉配信すべきではなかった。
  • 中国本土のネットワーク事情への理解が不足しており、海外の前提をそのまま当てはめてしまった結果、事前に CDN を用意していなかった。
  • まずは小規模なお客様の範囲でテストし、事前にフィードバックを収集すべきだった。

今回の影響を受けられたお客様には、影響時間に応じたクレジットによる補償をご提供いたします。今後この分野の機能を配信する際には、より一層慎重に進めてまいります。互換レイヤーの問題をご指摘いただいたすべてのお客様に、重ねて感謝申し上げます。