3 ポイント 投稿者 GN⁺ 2024-09-22 | 1件のコメント | WhatsAppで共有
  • 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:3000service1というサービス名で登録する

ゼロダウンタイム切り替えの仕組み

  • 新しいインスタンスが登録されると、Kamal Proxyは即座にHTTPヘルスチェックを実行し、到達可能性と動作状況を確認する
  • ヘルスチェックが成功すると、新しいトラフィックを新しいインスタンスへルーティングし始める
  • 新しいインスタンスが適切な時間内にhealthy状態になれない場合、deployコマンドはデプロイを中止し、0以外の終了コードを返す
  • 各デプロイは以前にデプロイされたインスタンスのすべてのトラフィックを新しいインスタンスへ引き継ぐ
  • deployコマンドは以前のインスタンスからトラフィックがすべて抜けるまで待ってから戻る
    • 正常に戻った後は、以前のインスタンスを削除しても進行中のリクエストを切断しない
  • 新しいインスタンスがhealthy状態のときだけトラフィックを受け取り、以前のインスタンスは完全にdrainされた後に削除されるため、デプロイはゼロダウンタイムで進行する

ヘルスチェック設定

  • デフォルトのヘルスチェックは各サービスに対して1秒あたり1回、/upGETリクエストを送る
  • 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つのサービスにしかルーティングできない
    • すでにservice1app1.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件のコメント

 
GN⁺ 2024-09-22
Hacker News のコメント
  • アクション名に deploy を選んだのは変だと思う
    すでに使われすぎている用語なので、「アプリがデプロイされたのか? いや、プロキシにデプロイされたのか?」みたいな混乱が起きそう
    bindintercept、単に proxy みたいな言葉も使えたはずなのに、なぜ deploy なのか分からない

    • 「デプロイ済み」は、実行中で プロキシに登録済み という意味だと考えればよい
    • 流入トラフィックがプロキシから来るなら、deploy が「プロキシのトラフィックが新しいアプリインスタンスへ流れ始めた」という意味以外に何になり得るのか分からない
  • 最近はほかのサーバーでも ゼロダウンタイムデプロイ を簡単にできるのに、これを別アプリ全体として作った選択が興味深い
    例えば Unix ソケットをサポートするアプリ+Web プロキシなら、ファイルを移動するだけで無停止にできる
    アトミックだし、curl でウォームアップリクエストも送れる
    登録システムまで作るのはやりすぎに見える

    • これは Kamal(https://kamal-deploy.org) のごく小さな一部にすぎない
      Kamal はクラウドから自社ハードウェアへ移行して数百万ドルを節約するために使われたデプロイツールだ(https://basecamp.com/cloud-exit)
    • この方式についてもう少し説明してもらえるとうれしい
      参考になる記事があれば、もっと学んでみたい
  • これは主に Docker Swarm でサービスコンテナを入れ替える際にトラフィックが途切れる根本問題に対処しようとしているように見える
    Cloud 66 で JAMStack サーバーを作ったときに同じ問題に遭遇し、自前プロキシではなく Caddy を使った。Traefik でも十分適していたと思う
    Kamal がなぜ k8s や k3s ではなく Swarm を選んだのかは分からないが、複雑さはどこかに居場所を持たざるを得ず隠せないので、結局は自前プロキシに行き着いたように見える
    実際に使ったことはないが、WebSockets、SSE、HTTP/3、各種圧縮と暗号化のサポートを追い続けることになりそうで、かなり懐疑的だ

    • Kamal は「Kubernetes は複雑すぎる」という前提の上に作られているように感じるし、その正当化から出発して Kubernetes が担っているかなりの部分を作り直している
      単純に始まったリバースプロキシが、結局は避けようとしていた複雑さを吸収し続ける例に見える
      広く拡張可能な実行システムには競合が必要だと思うが、Kamal は避けようとしていた複雑さへ後戻りしている作業のように見える
      Kubernetes が desired state 管理 フレームワークとして持つ能力と柔軟性が、むしろより際立って見える
    • オーケストレーションとプロキシを混ぜて考えているように見える
      Docker を使い続けながらでも、プロキシには Caddy、Traefik、Envoy を使える
      Kubernetes も結局、これらを Ingress Controller として使うことが多い
    • Kamal が Docker Swarm を使っているというのはどこで見たのか分からない
      見たところ、Swarm は使っていないようだ
    • Kamal は普通の docker run コマンドを使っており、Swarm やオーケストレーション はない
      基本的には Capistrano を置き換えるシンプルなツールだ
    • それが理由なら、この話題についてごく基本的な調査すらしていない印象だ
      コンテナオーケストレーションサービスの基本は、Ingress Controller で接続とブルー/グリーンデプロイを処理することだ
      より正確には、数年、長ければ数十年前からこのユースケースを処理してきた数多くのリバースプロキシに与えられた役割名が Ingress Controller
  • ゼロダウンタイムデプロイ が一般的にどう動くのか、簡単に説明してもらえるとうれしい
    2つのバージョンのアプリが同時に立ち上がっていて、新しいトラフィックが新バージョンにルーティングされる構造だと推測している
    ただ、単一のデータベースを使っていて新しいアプリバージョンがスキーマ変更を伴う場合、起動時にマイグレーションスクリプトがスキーマを変えるはずだが、旧アプリバージョンは古いスキーマを期待している
    これをどう処理するのか気になる

    • 最初のステップは マイグレーションとデプロイを分離 することだ
      多くのフレームワークはコードのデプロイ時にマイグレーションを実行するが、実際には実行タイミングを手動で制御するほうがよい
      各コードバージョンは現在のスキーマと将来のマイグレーション後のスキーマの両方で動作する必要があるため、実質的に後方互換なコードになる
      流れは「現在/将来のスキーマの両方で動作する新コードをデプロイ → 確認 → マイグレーション実行 → 確認 → 消えたスキーマに対応するコードを整理」となる
    • マイグレーションは 後方互換 でなければならず、データベーススキーマがアプリの両方のバージョンをサポートできる必要がある
      ゼロダウンタイムデプロイやローリングデプロイをするために払う追加コストで、大企業はたいていこうしている
    • 本当にゼロダウンタイムデプロイが必要な場合の方法はほかの回答が説明しているが、ほとんどの組織とほとんどのマイグレーションでは、DB マイグレーションによる停止時間は実質的に 0 と区別できない
      特に地域単位のユーザーを対象にしていて、静かな時間帯を選んでデプロイできるならなおさらだ
    • 直接使ったことはないが、Xata は少なくとも Postgres では DB マイグレーション にかなりきれいな解法を作ったように見える
      アプリの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_proxytry_durationtry_interval を設定すればうまく処理できる
      プロキシヘッダー処理も信頼する IP を考慮していないため、有効化すると誰かが X-Forwarded-For を設定して IP を偽装できる
      デフォルトでオフなのは幸いだが、警告もない
      シンプルさが目的なのは理解するが、現状では未成熟で、より良い選択肢があまりにも多く、実際に使いたいとは思わない
  • これをどう使うのかよく分からず、何か見落としている気がする
    例では web サービスのレプリカを4つ起動している
    そのうち example-web-1 のようなレプリカ1つにデプロイしてサービスを作れるのだとしたら、残り3つのレプリカが何をしているのか分からない
    web を更新して無停止デプロイをするには、web サービスでビルドコマンドを実行し、何らかの方法でこのサービスを起動したうえで、新しいターゲットに対して deploy を実行する必要がありそうだ
    しかし docker compose up --build --force-recreate web を実行すると既存のレプリカが停止するので、すべて無意味になる

    • 最初の質問の別レプリカについては、Docker が VIP または DNS リクエストで複数の IP を返す方式でロードバランシングする[0]
      このプロキシが 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 の共同創設者なら多くのデプロイを見てきただろうから、簡単には聞き流せない
    • 正直、危険に生きるパターンのように聞こえる
    • Caddy はこれをやる
      以前このテーマで話した記憶がある
  • 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だ。これ以上付け加えることはない