koukiblog

たぶんweb系の話題

kubernetesにRailsをデプロイするときには、l.gcr.io/google/rubyは使わない方がいい

運用しているGKE上にコンテナ化されていないRailsアプリをデプロイすることになり、コンテナ化を進めていたときに気づいた話。

デプロイしたいアプリはRailsアプリはGAE上で動いていて、GAEで動作するときにどうもコンテナ化されているらしい。

そこでたどり着いたのが、このレポジトリ。

github.com

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)
  })
}

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というのかどうかは微妙なところ)

詳しくはこちら

Installing Telepresence · Telepresence: Fast, realistic local development for Kubernetes and OpenShift Origin microservices

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