kubernetesにRailsをデプロイするときには、l.gcr.io/google/rubyは使わない方がいい
運用しているGKE上にコンテナ化されていないRailsアプリをデプロイすることになり、コンテナ化を進めていたときに気づいた話。
デプロイしたいアプリはRailsアプリはGAE上で動いていて、GAEで動作するときにどうもコンテナ化されているらしい。
そこでたどり着いたのが、このレポジトリ。
RubyとNode.jsがインストールされていて、Railsのための設定もすでに行われているのでそのまま使えそうです。 ruby-baseディレクトリのREADMEにもGKEで動作させるため記載がありました。
# Use the Ruby base image FROM l.gcr.io/google/ruby:latest # Copy application files and install the bundle COPY . /app/ RUN bundle install && rbenv rehash # Default container command invokes rackup to start the server. CMD ["bundle", "exec", "rackup", "--port=8080"]
https://github.com/GoogleCloudPlatform/ruby-docker/tree/master/ruby-ubuntu16
ところが、この通りDockerfileを記述してビルドすると、bundlerが存在しないエラーになります。
この事象は下記Issueで報告されていました。 github.com
Issueを読むと、こんな記述があります
Hmm. Looks like that readme is incorrect: I don't think gcr.io/google/ruby is the correct image. Generally, we should probably remove that section on GKE from the readme, as this docker image has generally evolved to be pretty app engine specific. If you want to roll your own docker image, I recommend using the canonical ruby images from DockerHub.
要約すると、gcr.io/google/rubyを使うこと自体が間違い。READMEから削除すべき。gcr.io/google/rubyは、GAE独自の仕様にあわせたコンテナになっている。ということっぽい。
google/rubyを仕様を調べれば対応できるとは思うけど、このコンテナ自体汎用的な用途を想定しているわけではなく、GAE用のランタイムとして設計されているようなので、使うべきではないと判断し、DockerHubにあるrubyのオフィシャルイメージ使って自前で構築することにした。
Pumaはあるサイズ以上のデータをPOSTされると一時ファイルを作成する
表題の通り。Pumaはあるサイズ以上のデータをPOSTされると一時ファイルを作成します。
puma/server.rb at 482ea5a24abaccf33c49dc9238a22e2a9affe288 · puma/puma · GitHub
# Use a Tempfile if there is a lot of data left if remain > MAX_BODY stream = Tempfile.new(Const::PUMA_TMP_BASE) stream.binmode else # The body[0,0] trick is to get an empty string in the same # encoding as body. stream = StringIO.new body[0,0] end
GKE上で書き込み禁止にして運用していたところ、ある条件下でファイル作成しようとしてエラーになったので調べてみたらこれが原因でした。
Node.jsでsleep相当のことをする
非同期処理が意図通り動いているのか確認するときに便利です。 resolveAfter2Secondsという関数を定義し、完了を待ちます。
async function slowSomething(){ await resolveAfter2Seconds() } function resolveAfter2Seconds() { console.log('starting slow promise') return new Promise(resolve => { setTimeout(function() { resolve('slow') console.log('starting slow promise') }, 2000) }) }
セキュアなパスワードを生成するワンライナー
いつも忘れて検索してるのでここに残しておく
ruby -r securerandom -e "puts SecureRandom.urlsafe_base64"
GKEでスティッキーセッション(Session Affinity)を利用する
GKE環境でスティッキーセッションを利用するときの方法です。WebSocketなど利用していて、ブラウザからPodと接続を確立したあとはそのPodと通信し続けたい場合に必要になると思います。
k8s環境でスティッキーセッションってそもそもできるのか?と最初不安だったのですが、思ったより簡単にできました。 GCPやそのほかのドキュメントではSessionAffinityと呼ばれていたので以降SessionAffinityにします。
解決策
GKEデフォルトのIngress Controllerには、SessionAffinityの機能があり、この機能を有効にすることで実現することができます。接続元の判断は、クライアントIP、Cookieのどちらかの方法を選ぶことができます。
Configuring a backend service through Ingress | Kubernetes Engine Documentation | Google Cloud
ドキュメントに書いてある通りなのですが、まずbackend configを作成し
apiVersion: cloud.google.com/v1beta1 kind: BackendConfig metadata: name: my-bsc-backendconfig spec: timeoutSec: 40 connectionDraining: drainingTimeoutSec: 60 sessionAffinity: affinityType: "CLIENT_IP"
サービス側でそのbackend configを利用することで実現できます。
ind: Service metadata: name: my-bsc-service labels: purpose: bsc-config-demo annotations: cloud.google.com/neg: '{"ingress": true}' beta.cloud.google.com/backend-config: '{"ports": {"80":"my-bsc-backendconfig"}}' ...
サービス側で指定するNEGは必須です。 また、ServiceがNEGを利用していなかった場合、NEGに切り替わるまでの間通信断が発生するので本番環境に反映する場合は注意が必要です。
仕組み
GCPのLoadBalncerにSession Affinity ( https://cloud.google.com/load-balancing/docs/backend-service#generated_cookie_affinity )という仕組みがあり、これをそのままGKEでも利用する形です。
通常のk8s環境の場合、Ingressとして作成されたLBのバックエンドはGCEインスタンスであり、GCEインスタンスに来たリクエストをk8sがPodに転送するので一見SessionAffinityの実現は困難なように思えます。そこで、必要になるのがNEG( https://cloud.google.com/kubernetes-engine/docs/how-to/container-native-load-balancing )です。
NEGを利用すると、LBのバックエンドをGCEインスタンスではなく、Podにすることが可能になります。こうすることで、LoadBalancerのSession Affinityの機能をk8s環境でもそのまま利用することができます。
NEGはGKE独自の機能なので、GKE以外の環境では、Nginx Ingress Controllerを利用する必要がありそうでした
Telepresenceを使ってk8s環境の開発をより快適にする
Telepresence(https://www.telepresence.io/)をちょっと試してみたのでそのメモ
Telepresence?
ローカルPCとリモートのk8sクラスタを接続し、マイクロサービス 環境での開発を支援してくれるツールです。 出来ることは大きく2つで、
どちらも双方向のプロキシがk8sクラスタにPodとしてデプロイされ、k8sクラスタとローカルPCを接続してくれます。
Install
Macであれば
brew cask install osxfuse brew install datawire/blackbird/telepresence
だけでインストールできます。
WindowsはWSL上で動作させる形になっています。(これをWindows Supportというのかどうかは微妙なところ)
詳しくはこちら
Deploymentの差し替え
おそらくこれがメインの用途です。 k8sクラスタ上のアプリケーションでデバッグ情報を取得する場合、これまではデバッグ用の設定を追加したコンテナを作成しそれをデプロイする必要がありましたが、その必要がなくなります。
たとえば、service_aというdeploymentのapp というcontainerをローカルに存在するapp:debug というコンテナに差し替える場合
telepresence --swap-deployment service_a:app --docker-run --rm -it app:debug
というコマンドで差し替えることができます。Containerの指定は、Pod内のContainerが1つしかないなら不要です。SidecarなどでPod内に複数のContainerが存在しているケースが多いと思うので、Containerは必ず指定しておいた方が良いと思います。
注意点としては、このDeploymentの差し替えはTelepresence利用者だけに影響があるものではありません。k8sクラスタのDeploymentのそのものに影響を与えています。複数の開発者でクラスタを共有している場合は注意が必要です。
クラスタが外部からアクセス可能な場合、外部からローカルPC上のコンテナにアクセスできてしまいます。デバッグ用のコンテナとはいえ、不用意にファイルを配置しないように気をつけましょう。特にボリュームマウントする場合は注意が必要です。
ある程度の規模のクラスタになると、CI/CDの観点から各自が自由にkubectl applyはできなくなり、CIツールのチェックが通ったものが反映されるようなフローになることが多くなってくると思います。それはそれで正しいのですが、ちょっとした検証のためにそのフローを通さないといけないのは非効率です。このdeploymentの差し替えを有効に利用すれば、別のサービスとの連携の検証などが素早く行えそうです。
k8sクラスタへの接続
引数なしでtelepresenceコマンドを実行すると、ローカルPCとk8sクラスタを接続することができます。
telepresence
デフォルトではkubectlのcurrent contextが利用されます。context パラメタとnamespaceパラメタをそれぞれ指定することができます。
telepresenceによる接続が有効な間は、ローカルPCがk8sクラスタ内に存在しているようなイメージになります。サービスの名前解決を行うことができ、port-forwardで直接Podに接続するよりもより実際の動作に近い挙動を確認できます。また、ローカルPCにインストールされている様々なツールがそのまま利用できます。
まとめ
これまでのk8sクラスタの開発ではリモートのk8sクラスタをデバッグする場合、kubectl port-forward で特定のPodで接続したり、デバッグ用のPodを準備しておいてそれをデプロイしそのPodのターミナルからデバッグを行うというプラクティスが紹介されることが多かったですが、telepresenceも選択肢の1つとして用意しておくとより快適な開発を行えそうです。
GKEでノードをダウンタイムなしで更新する場合
GKEを利用していて、ノードをダウンタイムなしで更新したい場合、ノードプールを新規作成し、既存のノードプールからワークロードを移行することで実現することができます。
ノードのスペックやサービスアカウント の情報は作成後変更はできないため、本番環境で運用を始めると必要になるケースが多そうです。
ワークロードの移行は、既存のNode全てにcordonを実行しUnscheduleにしてから、既存のNode全てをdrainすることでワークロードを新しいノードプールに移行できます。 このオペーレションをダウンタイムなしで実現するには、PodDisruptionBudgetが適切に設定されている必要があるので注意が必要です。
詳しくは↓に全て書いてあります。 cloud.google.com