ぷろじぇくと、みすじら。

Real World .NET Core on Kubernetes という話をしました

Created at:

10月18日に AWS .NET Developer User Group 勉強会 #2 にて Real World .NET Core on Kubernetes というセッションで話させていただきました。このエントリーはそのフォローアップです。

Lifebear のサーバーアプリケーション (.NET Core) を Amazon EKS / Kubernetes 環境を構築してリリースするまでにメモしていたポイントまとめた形です。

「EKS/AKS/GKE で Kubernetes 立てて、.NET Core アプリをデプロイしてみた(使ってみた)」より一歩進んだ話が欲しいと思っていたのでそれがテーマでした。

.NET Core のアプリケーションを Kubernetes の上で動かしたという国内の実例というのは少なくて、自分で構築する際にも不安になったので、今後 Kubernetes でシステムを構築しようという人の参考になってほしいなと。…と思って書いたらちょっと盛りすぎてしまったので、Kuberentes 寄りの話と .NET Core 寄りの話を分ける方がよかったかなというのが反省点ですね。

知見いろいろ

ネタ帳にはあったものの時間の関係上話せなかったスライドに書いた以外のこともこの際なので書いておきます(主に Kubernetes)。

Docker build は BuildKit 使おう

BuildKit でビルドすると高速かつキャッシュできるので有効にするのがおすすめです。特に NuGet パッケージのリストアに効果があります。

環境変数で DOCKER_BUILDKIT=1 を指定して Dockerfile でおまじないを追加するだけです。

# syntax = docker/dockerfile:experimental
...

RUN --mount=type=cache,target=/root/.nuget/ \
    dotnet restore "WebApplication1.csproj" -c Release -o /app

Kuberentes のバージョンアップは大変

Kubernetes のクラスターのバージョンは1マイナーバージョンアップごとに上げることになります。これはバージョンジャンプが大変なので日々追いかけておくつもりが必要ということです。

kubectl や kubelet のバージョンポリシーもあり、例えば kubelet は api-server に対して前2マイナーバージョン、kubectl はプラスマイナス1マイナーバージョンといった制約がありますのでどのみち大ジャンプが難しいということです。

ノードにはアプリケーション以外のものも動いている

通常ノードを落とす時には kubectl drain するのが普通です。ある時 kubectl drain しなくても kubectl cordon してアプリを再デプロイして Pod を別なところに移せばそのままノードを落とせるのではと思い、やってみると CoreDNS 等のサービスがいなくなって死にました(もちろん対象のノードにいなければ無傷)。それはそう…。

Amazon EKS 用のノードは AMI は作らない

基本カスタマイズは UserData で起動時に何とかします。AMI作っても絶対管理できないですし、ノードはコンテナーを動かすためだけの代物なので。

/etc/eks/bootstrap.sh というものに kubelet のパラメータなどを渡せるのでそこでやりましょう。

デプロイ時にサービスから外れるのを待つ

Pod のシャットダウンと Service から外れるタイミングが異なるため、先に Pod がシャットダウンすると接続しようとしてしまうという話。preStop フックで数秒待って外れてからシャットダウンへ進むようにします。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  template:
    spec:
      containers:
      - name: myapp
        lifecycle:
          # シャットダウン前に少し待たないと Service から削除されるより先に Pod が終了してしまうことがある
          # https://qiita.com/superbrothers/items/3ac78daba3560ea406b2
          preStop:
            exec:
              command: ["sh", "-c", "sleep 5"]

Amazon EKS では Private Endpoint を使う

Kubernetes API のエンドポイントをインターネット側からアクセスできないようにする設定が追加されたので特にプロダクション環境などでは有効にするのがおすすめです。

ただし、当然外部サービスから Kubernetes API を呼び出す難易度が上がるのでその点は考慮する必要があります(その制約で Azure Pipelines の Environments を使えなかった)。

VPC のプライベートアドレスが枯渇

Amazon EKS では Pod に対してプライベートIPを割り当てるのですが、普通の EC2 インスタンスの気持ちで小さめのサブネットにしていると Pod がぽこぽこ増えた時に困ったことになります。

アドレス空間はある程度余裕を持った形にしておくのがよいです。

OOMKilled や CrashLoopBackOff を監視する

Kubernetes 上で発生したエラーイベントを監視しておきましょう。Datadog では Kubernetes のイベントも監視できるのでひっかけて通知するようにしていると異変に気付きやすくなります。

CrashLoopBackOff は発生していても、以前の Pod にはアクセスできるので他のきっかけがないと気づきにくくなります。

コンテナーの中で docker を動かす

Docker イメージを CI でビルドするのに CI のエージェントが Docker で動いているとそのままでは Docker 動かない問題にあたります。そこで俗にいう Docker in Docker (dind) です。

イメージをビルドするだけなので、ほかにも img とか Rootless Docker とか kaniko とか検討できるものはあるのですが無理しない方針で無難に Docker in Docker にしました。

privileged: true にしないといけないのでそこは微妙なのですが、今ならもしかすると Rootless でうまいこと privileged: true しなくてもいけるかもしれません。

Kubernetes の API を試すときは Pod に入って curl するのがオススメ

Kubernetes の API にアクセスする場合 ServiceAccount の設定をミスっていたりするとうまくアクセスできなくなったりするのですが、これをアプリデプロイして試すのは大変なので kubectl exec podname -it /bin/bash で入って curl で叩くのがおすすめです。

Pod が偏る

通常、Pod をデプロイするときには Kubernetes がリソースの具合を見て適度にバランスしてスケジューリングします。一方でデプロイ後に再スケジュールはしてくれません。

例えば、2つあるうちの1つのノードが死んだ場合はそのノードで動いていた分の Pod が生きているノードで起動されなおします。そして、その後ノードが復活してきたとしても Pod たちは元のノードに帰っていったりはしません。偏ったままです。

これを解決する手っ取り早い方法としては再デプロイかレプリカ数を上げたり下げたり、要するに Pod の再作成です。

他には Kubernetes の拡張として Descheduler という再スケジュールしなおすものが開発されているのでこちらの利用を検討してもいいかもしれません(が、鋭意開発中っぽい雰囲気があります)。

まとめ

振り返ってみると大変なのはアプリそのものよりは中小規模の Kubernetes クラスターを構築、運用するノウハウというところが大きい気がしました。

繰り返しにはなりますが、.NET Core を Kubernetes で実際に動かしてリリースまでもっていっているという事例の参考になって、事例がもっと出てきてほしいですね。

というわけでライフベアではほとんど好き勝手構築させていただいたので大変感謝しています。もし .NET Core (C#) + Kubernetes な環境ににご興味のある方はご一報いただくか、直接コンタクトしていただくと良いかと思います。

そして Cysharp では C# (サーバー、クライアント) に関するご相談を受け付けておりますのでお困りのことがあればぜひお問い合わせください