🎮

振り返り (2020 - 2022)

2023/03/05に公開

コロプラに 2020/3/1 に入社して、2023/2/28 付けで退職したので、丸々 3 年間勤務したことになります。本当の意味での大規模 Kubernetes 環境で貴重な経験をさせて貰い感謝しかないです。記憶が新しい内に、この 3 年間でやってきたことを公開できる範囲で整理しました。

GitOps 風なマニフェスト管理への移行

インフラチームで管理している監視ツールやアドオンなコンポーネントを Helm でインストールしていました。マルチクラスタな環境で手動インストールはスケールしないので、Helmfile で生成した各クラスタのマニフェストを Argo CD で同期する方式に移行しました。

  • 各クラスタで必要なコンポーネントのマニフェストを Helmfile で生成して Git 管理
    • Helmfile の ArgoCD Integration に書かれている理由から
    • Argo CD に問題が起きた場合に、kubectl apply で凌げる
    • いずれ Argo CD の利用を辞めることになった時に移行が楽なように
  • Argo CD が外部クラスタのリソースを GCP サービスアカウントの権限で操作
    • argoproj/argo-cd#9190 で upstream に対応を入れた
    • GCP サービスアカウントに対して Kubernetes 組み込みの cluster-admin 権限を紐付け
    • Workload Identity で application controller と argocd server の Pod に GCP サービスアカウントの権限を紐付け
  • Argo CD の Automated Sync Policy は使用していない
    • 複数のクラスタに徐々に反映して様子を見たいので、手動で同期する方が柔軟性があった
    • GitOps 風と表現しているのはそれが理由
  • Argo CD の ApplicationSet の導入
    • Application を各クラスタ各コンポーネント毎に作成するのが面倒だった
    • Git Generator の Directories で、Git 上のディレクトリ構成から Application を自動生成

Kubernetes 周辺エコシステムへのバージョン追従

各ゲームタイトルでゲームインフラを 1 から構築しており、各ゲームタイトルで使用する Helm chart のバージョンを把握できておらず、最新バージョンへの追従もできていませんでした。

コンテナイメージ

DockerHub は匿名ユーザーでのプルにレート制限があります。また、Cloud NAT の存在しない air-gapped クラスタでは外部のコンテナレジストリからイメージをプルすることができません。そのため、外部のコンテナイメージを Artifact Registry にミラーして参照するようにしていました。

GitHub 上のリポジトリに複数の Dockerfile を保存し、Dependabot と連携させて Docker イメージのバージョンを自動更新します。Dependabot が作成した PR が作成されると、GitHub と連携した Cloud Build が走り、crane を使ってコンテナイメージを Artifact Registry にコピーするようにしました。

Helm chart

公式やコミュニティ主導でメンテナンスされている Helm chart を使うように移行を進めました。この設定ができないから、このリソースがサポートされていないから Helm chart を自作するケースがありました。Helm chart を書くのもメンテナンスするのも本質的でなく面倒な作業です。公式やコミュニティ主導の Helm chart にコントリビュートして、良くしていく方が建設的です。特定のクラウドプロバイダーに依存したリソースは、raw 的なものを自作して Helmfile の Adhoc dependency で依存関係を注入しました。

公式やコミュニティ主導でメンテナンスされている Helm chart も Bitnami の一件があったため、Artifact Registry にミラーするようにしました。GitHub 上のリポジトリに Helmfile の設定ファイルを保存し、Renovate Apps と連携させて helmfile.yaml に定義された Helm chart のバージョン更新を検知して PR を作成します。Renovate が PR を作成すると、GitHub と連携した Cloud Build がトリガーされて、Helm chart をプルして Artifact Registry にプッシュします。

各ゲームタイトルで Renovate を CronJob で動作させ、Artifact Registry に同期した Helm chart のバージョンを検知して、helmfile.yaml で参照している Helm chart のバージョンを更新します。これにより、各プロジェクトで使用している Helm chart のバージョンとどのバージョンに上げることが可能かを追跡可能です。あとは、サードパーティ製のコンテナイメージや Helm chart のリリースノートを確認し、PR をマージして必要な修正を加えてマニフェストを生成するだけです。

GKE 更新の補助ツール開発

Google Cloud 提供の GKE 自動更新の機能は実施日時を細く設定することができません。特にコロプラはマルチクラスタ構成を取っており、各 GKE クラスタの前段に HAProxy MIGs が存在します。HAProxy で流量を操作しており、47.5%/47.5%/5% でトラフィックを流していました。そのため、流量の少ない GKE クラスタから順番に更新したい要望がありました。GKE の機能では難しいので、Temporal と go-sdk でワークフロー化して実行する形で開発しました。

  • GKE クラスタの更新に時間が掛かるので、耐障害性が欲しい
    • コントロールプレーンは各ゾーンでフェイルオーバーしながら更新していくので、3 ゾーンのリージョンクラスタで 27 分程度掛かる (9 分 x 3 ゾーン?)
    • ワークフローを実行しているワーカーが死んでも途中から処理を再開できる
  • GKE 更新の前後で処理を追加
    • Surge Upgrademax-surge-upgrade をパーセント指定で動的に計算して台数を更新
    • GKE 更新中はノードの自動修復を無効化
      • GKE 更新中に修復不可能な問題が起きても自動修復を繰り返してしまうから
      • 自動修復を繰り返すよりノードプールやクラスタを切り離す方が良い
    • メンテナンス除外期間があれば削除
    • ノードイメージの変更にも対応
  • 複数のクラスタを直列・並列で更新可能
  • CronJob でスケジュール実行も可能
  • kubernetes-sigs/kubetest2 を使用した E2E テストの導入

GKE 更新作業

運用中のゲームタイトルの GKE のバージョンや新しい機能への移行作業を行いました。

GKE 固有の問題

  • GKE 1.20 に更新後に Node Problem Detector (NPD) が起動できなくなる問題
    • Google Cloud のサポートに問い合わせて、Google Issue Tracker に Issue を作って貰った
    • containerd (Ubuntu) のノードイメージが Ubuntu 18.04.5 LTS から Ubuntu 20.04.2 LTS に変更した影響で Python 2 がなくなり、Python 2 に依存した shebang が書かれた NPD の GKE 固有の plugin が起動できず再起動を繰り返していた
    • サポートから Python 3 にシンボリックリンクを貼る DaemonSet を起動すると良いよと言われたが、ホストマウントかつ特権コンテナを起動するのが怖くて諦めた
    • サポートから NPD がなくても大抵の問題は kubelet が検知できるから動かなくてもクリティカルじゃないと言われたのと、GKE 1.20 に修正がバックポートされなさそうだったので、仕方なく放置して更新した
  • GKE 1.20 の一部と GKE 1.21 以降全てのパブリッククラスタkubectl logs/execAPI Aggregation Layer を使ったサービス (e.g. metrics-server, Agones) が正常に動作しなくなる問題が発生
    • パブリッククラスのみ Konnectivity service (a.k.a. kubernetes-sigs/apiserver-network-proxy) が導入されて、kubelet -> kube-apiserver の通信経路が変わった (January 22, 2021)
    • SSH トンネル時代は GKE 側で必要なファイアウォールルールを自動生成していたが、Konnectivity service で必要なファイアウォールルールは自動生成してくれなかった
    • 社内のセキュリティ統制で外向きの通信は必要なポート番号しか空けてなかったのが原因
    • Google Cloud の公式ドキュメントの Konnectivity proxy のセクションに Firewall ルールで許可しているか確認する項目を作って貰った
    • GKE 1.21 からという話だったが、GKE 1.20 でも一部ロールアウトしていて運用中のクラスタが 1 つ引き当てて影響を受けた
  • Dataplane v2 で IP 枯渇する問題
  • metrics-server の sidecar の Addon Resizer 1.8.11-gke.0 の CPU 使用率の高騰
    • Google Cloud のサポートに CPU プロファイルを送ったりして調査依頼したけど解決はしなかった
    • kubernetes/autoscaler#3833 で解消してるっぽい

Kubernetes の問題

  • UDP パケット喪失問題
    • ワークロードの再起動後に Service type LoadBalancer を経由した UDP 通信が Pod まで届かなくなる問題が発生
    • conntrack のエントリから古い Pod IP の情報が削除されずに間違った情報として残り続け、パケットがブラックホールに飲み込まれているところまで同僚と確認
    • k/k#98305 で NodePort を使用した場合の UDP の古い conntrack エントリを削除するように修正されたが、k/k#103857 で Service type LoadBalancer の場合に問題が起きている報告が出た。最終的に k/k#104009 で修正された
    • 諸般の事情により完全に修正されたか確認できていない
    • k/k#106274 で CLusterIP でも同様の問題がある報告があったが、こちらは問題が再現できずにクローズされた
    • Kubernetes 上で UDP 通信を行う場合の闇を見た
  • k8s.gcr.io から registry.k8s.io への移行
    • registry.k8s.io: faster, cheaper and Generally Available (GA)
    • プライベートクラスタかつ Cloud NAT の存在しない air-gapped クラスタでは registry.k8s.io からコンテナイメージがダウンロードできない
    • Kubernetes エコシステム周りのイメージが移行を開始 (e.g. kubernetes/kube-state-metrics) したので対応
    • GKE 上の pause コンテナが k8s.gcr.io のコンテナイメージを使用していたのでサポートに問い合わせたが、registry.k8s.io に移行予定はなく GCR からイメージを取得し続けると回答があった
    • pause コンテナのイメージとかはノードイメージ内に同梱されているので問題ないが、kubelet のイメージ GC で消されるとまずい
    • k8s.gcr.io Image Registry Will Be Frozen From the 3rd of April 2023k8s.gcr.io に新しくコンテナイメージを追加しなくなることが決定
    • containerd/containerd#7944 で pause コンテナのイメージもハッシュ値でピン留めして kubelet から GC されないようにする感じかも

Kubernetes の更新以外で大規模環境ならではの問題もいくつか経験することができました。(e.g. NodeLocal DNSCache の起動順の落とし穴、[未解決] Secret から環境変数が読み込めてなさそうな Pod)

RabbitMQ MIGs への移行

GKE クラスタを年 3 回バージョン更新する中で、GKE 上のデータベース (MySQL, Redis, RabbitMQ) の手動オペレーションが辛くなってきました。データベースを GKE 外で動かすように移行が始まり、私は RabbitMQ を Managed Instance Groups (MIGs) 上で動かす担当になりました。

  • RabbitMQ を非同期処理のキューとして使用
  • IL4LB のヘルスチェックで AMQP で死活監視したい
    • TCP ポート監視だと RabbitMQ は起動できているがメッセージが書き込めない/読み取れない問題が起きる可能性がある
    • IL4LB から Go 製のサーバに HTTP リクエストでヘルスチェックを送り、Go 製のサーバから AMQP で RabbitMQ にメッセージを enqueue/dequeue できるか確認
  • メッセージの enqueue/dequeue でロードバランサーを分離
    • 外部サービスの長期メンテナンスなどでディスク容量が枯渇した時に、enqueue だけしないようにしたいケースに対応
  • MIGs の更新時などインスタンスが削除される際に他の RabbitMQ にメッセージを転送するデーモンを実装
    • MIGs の状態を定期的にポーリングして、Shovel Plugin でメッセージを転送
    • IL4LB にぶら下がったインスタンスから IL4LB を経由してパケットを投げると、必ず自分自身のインスタンスにパケットが流れてしまう仕様のため、MIGs 内の別インスタンスの IP を取得して転送する形で実装

Elasticsearch on GKE への移行

Elasticsearch をこれまで VM 上で運用していましたが、Log4j などの脆弱性対応など運用コストが高かったです。また、これまで使ってきた Elasticsearch の VM イメージが Google Marketplace から提供されなくなり、新規に Elasticsearch を構築することができなくなりました。Elasticsearch のマネージドサービス (Elastic Cloud, AWS OpenSearch Service) の利用も検討しましたが、技術的な部分以外でのハードルが高く、GKE 上でセルフホストする方針に決めました。最初は Elasticsearch の公式の Helm chart でインストールしていましたが、elastic/helm-charts#1731 でコミュニティに譲渡されることになったため、Elastic Cloud on Kubernetes で管理するように移行しました。利用用途が限定的だったため、Elasticsearch をクラスタ化せずにシングルノードクラスタで動かしていました。

Kubernetes 勉強会

社内の Kubernetes に対する知識が人によって差がありました。インフラチームが Kubernetes の運用を全て担当していたので、一部を SRELCE などの横断チームに移譲できないかと考えた時期がありました。Kubernetes に興味を持ってもらうための勉強会を実施しました。

  • GKE をどう利用しているか
  • GKE と EKS の違い
  • Kubernetes や周辺エコシステムの動向
  • Kubernetes 上でのハマりポイント
    • DNS 周りや Load Balancer からのサービスアウト、コンテナのライフサイクル
  • ...

外部登壇

その他

Discussion