- Kamal ProxyはWebアプリケーションの前段で動作する小さなHTTPプロキシで、進行中のトラフィックを切断せずに新しいインスタンスへデプロイを切り替えられるよう設計されている
- 新しいインスタンスは
kamal-proxy deployで登録され、プロキシがHTTPヘルスチェックを通過した後にのみ新しいトラフィックをそのインスタンスへルーティングする
- デプロイコマンドは以前のインスタンスのトラフィックがすべて抜けるまで待機するため、正常に戻った後は進行中のリクエストを中断することなく以前のインスタンスを削除できる
- 単一のプロキシで複数のアプリケーションを運用できるよう、ホストベースルーティング、パスベースルーティング、自動TLSと手動TLS証明書指定機能を提供する
- Kamal ProxyはKamalデプロイツールの一部として設計されているが、単体実行や他のデプロイツールの構成要素としても利用できる
Kamal Proxyが行うこと
- Kamal Proxyはゼロダウンタイムデプロイの調整を簡単にするための小さなHTTPプロキシである
- WebアプリケーションをKamal Proxyの背後で実行すると、進行中のトラフィックを中断せずに変更をデプロイできる
- この動作のためにアプリケーション側の特別な協力は必要ない
- Kamal Proxyは、コンテナのパッケージングとプロビジョニングを含むKamalの一部として動作するよう設計されている
- 単体で使うことも、他のデプロイツールの一部として使うこともできる
実行とデプロイの流れ
- プロキシインスタンスは
kamal-proxy runコマンドで起動する
- 設定ファイルはなく、デフォルト値がアプリケーションに合わない場合はオプションを指定できる
- デフォルトのHTTPポートは
80
- 別のポートで実行するには
kamal-proxy run --http-port 8080を使う
- すべてのオプションは
kamal-proxy help runで確認できる
- トラフィックをWebアプリケーションへ送るには、アプリケーションインスタンスをプロキシにdeployする
- デプロイ対象のインスタンスは
hostname:port形式で指定する
- 例:
kamal-proxy deploy service1 --target web-1:3000
- このコマンドは
web-1:3000をservice1というサービス名で登録する
ゼロダウンタイム切り替えの仕組み
- 新しいインスタンスが登録されると、Kamal Proxyは即座にHTTPヘルスチェックを実行し、到達可能性と動作状況を確認する
- ヘルスチェックが成功すると、新しいトラフィックを新しいインスタンスへルーティングし始める
- 新しいインスタンスが適切な時間内にhealthy状態になれない場合、
deployコマンドはデプロイを中止し、0以外の終了コードを返す
- 各デプロイは以前にデプロイされたインスタンスのすべてのトラフィックを新しいインスタンスへ引き継ぐ
deployコマンドは以前のインスタンスからトラフィックがすべて抜けるまで待ってから戻る
- 正常に戻った後は、以前のインスタンスを削除しても進行中のリクエストを切断しない
- 新しいインスタンスがhealthy状態のときだけトラフィックを受け取り、以前のインスタンスは完全にdrainされた後に削除されるため、デプロイはゼロダウンタイムで進行する
ヘルスチェック設定
- デフォルトのヘルスチェックは各サービスに対して1秒あたり1回、
/upにGETリクエストを送る
200レスポンスをhealthy状態とみなす
- アプリケーションに合わせてヘルスチェックを変更するには
deployフラグを使う
--health-check-path
--health-check-port
--health-check-timeout
--health-check-interval
- ヘルスチェックパス変更の例:
kamal-proxy deploy service1 --target web-1:3000 --health-check-path web/index.html
- メインサービスとは別のポートでヘルスエンドポイントを公開する場合:
kamal-proxy deploy service1 --target web-1:3000 --health-check-port 8080
ルーティング機能
-
ホストベースルーティング
- ホストベースルーティングを使うと、1つのKamal Proxyインスタンスで同じサーバー上の複数のアプリケーションにトラフィックをルーティングできる
- デプロイ時に特定のホストを指定できる
kamal-proxy deploy service1 --target web-1:3000 --host app1.example.com
- この方式でデプロイされたインスタンスは、指定されたホストのトラフィックだけを受け取る
- 各アプリケーションに個別のホストを指定すれば、ポート競合なしに同じサーバー上で複数のアプリケーションを実行できる
- 特定のホストは同時に1つのサービスにしかルーティングできない
- すでに
service1がapp1.example.comを使っている場合、service2が同じホストでデプロイされるとエラーが返される
service1を削除した後は、service2を同じホストにデプロイできる
-
パスベースルーティング
- パスベースルーティングは、リクエストパスに応じてトラフィックを別のサービスへ振り分けるアプリケーションで使う
- サービスは異なるパスプレフィックスの下にマウントできる
/apiで始まるリクエストをweb-1へ送る例: kamal-proxy deploy service1 --target web-1:3000 --path-prefix=/api
- 残りを
web-2へ送る例: kamal-proxy deploy service2 --target web-2:3000
- デフォルトでは、パスプレフィックスはupstreamへ渡される前に削除される
/api/users/123リクエストはweb-1に/users/123として渡される
- 元のパスをそのまま渡すには
--strip-path-prefix=falseを指定する
TLSサポート
- Kamal Proxyはアプリケーション用のTLS証明書を自動で取得・更新できる
- 自動TLSはデプロイ時に
--tlsフラグを追加して有効化する
kamal-proxy deploy service1 --target web-1:3000 --host app1.example.com --tls
- 自動TLSにはホスト指定が必要である
- 任意のホスト名に対して悪意を持って証明書を要求できないようにするための条件である
- パスベースルーティングでTLSオプションはルートパスに設定する必要がある
- 同じホストの別パスにデプロイされたサービスは、ルートパスに指定されたTLS設定を使用する
- 手動で取得したTLS証明書、独自認証局、Cloudflare origin certificateをインストールする必要がある場合は、証明書ファイルと秘密鍵のパスを直接指定できる
--tls-certificate-path cert.pem
--tls-private-key-path key.pem
環境変数とビルド
- Dockerコンテナ実行のような環境では、
runオプションを環境変数として指定できる
kamal-proxy runは設定された環境変数があれば、各オプション値を環境変数から読み取る
kamal-proxy run --http-port 8080
HTTP_PORT=8080 kamal-proxy run
- 環境変数名が既存の環境と衝突する場合は、
KAMAL_PROXY_プレフィックスを付けて区別できる
KAMAL_PROXY_HTTP_PORT=8080 kamal-proxy run
- ローカルビルドは、動作するGo環境で
makeにより実行する
- Dockerコンテナとしてビルドするには
make dockerを使う
- プロキシコマンドを試せるDocker Compose設定は
exampleフォルダにある
1件のコメント
Hacker News のコメント
アクション名に deploy を選んだのは変だと思う
すでに使われすぎている用語なので、「アプリがデプロイされたのか? いや、プロキシにデプロイされたのか?」みたいな混乱が起きそう
bind、intercept、単にproxyみたいな言葉も使えたはずなのに、なぜdeployなのか分からない最近はほかのサーバーでも ゼロダウンタイムデプロイ を簡単にできるのに、これを別アプリ全体として作った選択が興味深い
例えば Unix ソケットをサポートするアプリ+Web プロキシなら、ファイルを移動するだけで無停止にできる
アトミックだし、
curlでウォームアップリクエストも送れる登録システムまで作るのはやりすぎに見える
Kamal はクラウドから自社ハードウェアへ移行して数百万ドルを節約するために使われたデプロイツールだ(https://basecamp.com/cloud-exit)
参考になる記事があれば、もっと学んでみたい
これは主に Docker Swarm でサービスコンテナを入れ替える際にトラフィックが途切れる根本問題に対処しようとしているように見える
Cloud 66 で JAMStack サーバーを作ったときに同じ問題に遭遇し、自前プロキシではなく Caddy を使った。Traefik でも十分適していたと思う
Kamal がなぜ k8s や k3s ではなく Swarm を選んだのかは分からないが、複雑さはどこかに居場所を持たざるを得ず隠せないので、結局は自前プロキシに行き着いたように見える
実際に使ったことはないが、WebSockets、SSE、HTTP/3、各種圧縮と暗号化のサポートを追い続けることになりそうで、かなり懐疑的だ
単純に始まったリバースプロキシが、結局は避けようとしていた複雑さを吸収し続ける例に見える
広く拡張可能な実行システムには競合が必要だと思うが、Kamal は避けようとしていた複雑さへ後戻りしている作業のように見える
Kubernetes が desired state 管理 フレームワークとして持つ能力と柔軟性が、むしろより際立って見える
Docker を使い続けながらでも、プロキシには Caddy、Traefik、Envoy を使える
Kubernetes も結局、これらを Ingress Controller として使うことが多い
見たところ、Swarm は使っていないようだ
docker runコマンドを使っており、Swarm やオーケストレーション はない基本的には Capistrano を置き換えるシンプルなツールだ
コンテナオーケストレーションサービスの基本は、Ingress Controller で接続とブルー/グリーンデプロイを処理することだ
より正確には、数年、長ければ数十年前からこのユースケースを処理してきた数多くのリバースプロキシに与えられた役割名が Ingress Controller だ
ゼロダウンタイムデプロイ が一般的にどう動くのか、簡単に説明してもらえるとうれしい
2つのバージョンのアプリが同時に立ち上がっていて、新しいトラフィックが新バージョンにルーティングされる構造だと推測している
ただ、単一のデータベースを使っていて新しいアプリバージョンがスキーマ変更を伴う場合、起動時にマイグレーションスクリプトがスキーマを変えるはずだが、旧アプリバージョンは古いスキーマを期待している
これをどう処理するのか気になる
多くのフレームワークはコードのデプロイ時にマイグレーションを実行するが、実際には実行タイミングを手動で制御するほうがよい
各コードバージョンは現在のスキーマと将来のマイグレーション後のスキーマの両方で動作する必要があるため、実質的に後方互換なコードになる
流れは「現在/将来のスキーマの両方で動作する新コードをデプロイ → 確認 → マイグレーション実行 → 確認 → 消えたスキーマに対応するコードを整理」となる
ゼロダウンタイムデプロイやローリングデプロイをするために払う追加コストで、大企業はたいていこうしている
特に地域単位のユーザーを対象にしていて、静かな時間帯を選んでデプロイできるならなおさらだ
アプリの2つのバージョンを同時に実行できる
https://xata.io/blog/multi-version-schema-migrations
ロードバランサーが Server2 で接続を受け始め、Server1 では新しい接続を受けない
その後、Server1 のすべての接続が閉じたら Server1 を切り離す
別々のサーバーの場合もあれば、1台のサーバー上の複数ワーカーの場合もある
その期間は、ほかの回答にあるようにマイグレーションが後方互換である必要がある
Kamal 2 は現在 RC(https://github.com/basecamp/kamal/releases)で、自動 SSLに対応し、1台のサーバーで複数のアプリを Kamal で簡単に動かせるようになるとのことで期待している
autocertパッケージを使っているが、これは最小限の実装に近い脆弱で、Let's Encrypt の発行制限と同時証明書制限のため、プロキシインスタンスを水平スケールしにくい
Caddy/Certmagic は共有ストレージにデータを書き込み、証明書を1つだけ発行して全インスタンスがストレージ経由で再利用・調整することでこの問題を解決している
発行者の代替、発行制限の回避、ARI 対応もない
アップストリームが利用可能になるまでリクエストを保持することも、Caddy なら
reverse_proxyにtry_durationとtry_intervalを設定すればうまく処理できるプロキシヘッダー処理も信頼する IP を考慮していないため、有効化すると誰かが
X-Forwarded-Forを設定して IP を偽装できるデフォルトでオフなのは幸いだが、警告もない
シンプルさが目的なのは理解するが、現状では未成熟で、より良い選択肢があまりにも多く、実際に使いたいとは思わない
これをどう使うのかよく分からず、何か見落としている気がする
例では
webサービスのレプリカを4つ起動しているそのうち
example-web-1のようなレプリカ1つにデプロイしてサービスを作れるのだとしたら、残り3つのレプリカが何をしているのか分からないwebを更新して無停止デプロイをするには、webサービスでビルドコマンドを実行し、何らかの方法でこのサービスを起動したうえで、新しいターゲットに対して deploy を実行する必要がありそうだしかし
docker compose up --build --force-recreate webを実行すると既存のレプリカが停止するので、すべて無意味になるこのプロキシが DNS リクエストで返された複数レコードを対象に均等分散するかは確認していないが、少なくとも VIP ベースのロードバランシングなら期待どおり動作するはず
2つ目の更新部分はあまり明確ではない
同じネットワーク内で別名のサービスを立ち上げて
kamal-proxy deployすることを想定しているのかもしれないサービス名にバージョン番号を入れる形かもしれないし、素早くロールバックしたいなら以前のバージョンをホットな状態で維持しておくのも理にかなっている
[0]: https://docs.docker.com/reference/compose-file/deploy/#endpo...
k8s rollout restart deploymentを使わない理由が分からないあるいは DNS やルーターを2つのバックエンド間で切り替えてもいい
これがトラフィック一時停止パターンを実装したものなのか気になる
プロキシがトラフィックを数秒間実質的に止める方式で、入ってきたリクエストは普段より数秒長くかかるように見えるが、短い遅延の後に完了する
その数秒の間に小さな DB マイグレーションや、5秒以内に終えられる少し複雑なブロッキング型のインフラ変更を実行できる
かなり危険に聞こえるし、アプリサーバー企業で数年働いたが、このパターンは初めて聞いた
ただ、Django の共同創設者なら多くのデプロイを見てきただろうから、簡単には聞き流せない
以前このテーマで話した記憶がある
Traefik や検証済みの他のツールを使わず、独自プロキシを作ることにした理由をどこで説明しているのか気になる
なぜ切り替えて独自実装にしたのかは PR に文脈がある
https://github.com/basecamp/kamal/pull/940
ただし Traefik 以外との比較を公式に出したことはないように思う
タイムアウトを設定する方法があるのか気になる
https://github.com/basecamp/kamal-proxy/blob/main/internal/s...
https://github.com/basecamp/kamal-proxy/blob/main/internal/s...
https://blog.cloudflare.com/exposing-go-on-the-internet/
NIHだ。これ以上付け加えることはない