Terraformの変更を計画・適用するために使用しているツールであるAtlantisを再起動するたびに、復旧まで30分間待機する必要がありました。その間、Atlantisが管理するリポジトリでは計画も適用もインフラストラクチャの変更も一切できません。認証情報のローテーションやオンボーディングのために月に約100回の再起動があり、毎月50時間以上のエンジニアリング時間がブロックされ、その度にオンコールエンジニアにページが送信されていました。
これは最終的に、Atlantisが使用する永続ボリュームが数百万のファイルに成長したことで、Kubernetesの安全なデフォルト設定が静かにボトルネックになったことが原因でした。問題を追跡し、1行の変更で修正した方法をご紹介します。
謎の遅い再起動
私たちはAtlantisを使用してGitLabマージリクエスト(MR)で数十のTerraformプロジェクトを管理しており、計画と適用を処理しています。一度に1つのMRのみがプロジェクトを変更できるようにロックを強制します。KubernetesでシングルトンStatefulSetとして実行され、Kubernetes PersistentVolume(PV)に依存してディスク上のリポジトリ状態を追跡します。
Terraformプロジェクトのオンボーディングやオフボーディング、またはTerraformが使用する認証情報が更新されるたびに、これらの変更を取得するためにAtlantisを再起動する必要があります。このプロセスには30分かかることがあります。
遅い再起動は、最近Atlantisが使用する永続ストレージのinodeが不足し、ボリュームのサイズ変更のために再起動を余儀なくされたときに明らかになりました。inodeは各ファイルとディレクトリエントリによって消費され、ファイルシステムで利用可能な数は作成時に渡されるパラメータによって決定されます。Kubernetesプラットフォームが提供するCeph永続ストレージ実装はmkfsにフラグを渡す方法を公開していないため、デフォルト値に依存しています。ファイルシステムを拡張することがinodeを増やす唯一の方法であり、PVの再起動にはポッドの再起動が必要です。
アラートウィンドウを延長することも検討しましたが、それは問題を隠すだけで、実際の問題への対応を遅らせるだけです。代わりに、なぜそんなに時間がかかるのかを正確に調査することにしました。
悪い動作
Atlantisが使用するシークレットの変更を取得するためにローリング再起動を求められたとき、kubectl rollout restart statefulset atlantisを実行し、新しいポッドを起動する前に既存のAtlantisポッドを正常に終了させます。
新しいポッドはほぼ即座に表示されますが、確認すると以下のように表示されます:
$ kubectl get pod atlantis-0
atlantis-0 0/1 Init:0/1 0 30m
何が起こっているのでしょうか?
当然、最初に確認すべきはそのポッドのイベントです。initコンテナの実行を待機しているので、ポッドイベントが理由を明らかにしてくれるかもしれません:
$ kubectl events --for=pod/atlantis-0
LAST SEEN TYPE REASON OBJECT MESSAGE
30m Normal Killing Pod/atlantis-0 Stopping container atlantis-server
30m Normal Scheduled Pod/atlantis-0 Successfully assigned atlantis/atlantis-0 to 36com1167.cfops.net
22s Normal Pulling Pod/atlantis-0 Pulling image "oci.example.com/git-sync/master:v4.1.0"
22s Normal Pulled Pod/atlantis-0 Successfully pulled image "oci.example.com/git-sync/master:v4.1.0" in 632ms (632ms including waiting). Image size: 58518579 bytes.
ほぼ正常に見えますが...ポッドのスケジューリングとinitコンテナのイメージプルの実際の開始の間で何がそんなに時間がかかっているのでしょうか?
残念ながら、これがKubernetes自体から得られるすべてのデータでした。しかし、なぜポッドの実行開始にそんなに時間がかかるのかを教えてくれるものが他にもあるはずです。
より深く調査
Kubernetesでは、各ノードで実行されるkubeletというコンポーネントがポッドの作成、永続ボリュームのマウント、その他多くのことを調整する責任があります。Kubernetesチームでの経験から、kubeletはsystemdサービスとして実行されるため、そのログはKibanaで利用可能であることを知っています。
ポッドがスケジュールされているので、関心のあるホスト名がわかり、kubeletからのログメッセージには関連するオブジェクトが含まれているため、atlantisでフィルタリングして、興味深いログメッセージに絞り込むことができました。
ポッドがスケジュールされた直後にAtlantis PVがマウントされるのを観察できました。また、すべてのシークレットボリュームが問題なくマウントされるのも観察しました。しかし、ログには依然として大きな説明のつかないギャップがありました。
以下のようなログが見られました:
[operation_generator.go:664] "MountVolume.MountDevice succeeded for volume \"pvc-94b75052-8d70-4c67-993a-9238613f3b99\" (UniqueName: \"kubernetes.io/csi/rook-ceph-nvme.rbd.csi.ceph.com^0001-000e-rook-ceph-nvme-0000000000000002-a6163184-670f-422b-a135-a1246dba4695\") pod \"atlantis-0\" (UID: \"83089f13-2d9b-46ed-a4d3-cba885f9f48a\") device mount path \"/state/var/lib/kubelet/plugins/kubernetes.io/csi/rook-ceph-nvme.rbd.csi.ceph.com/d42dcb508f87fa241a49c4f589c03d80de2f720a87e36932aedc4c07840e2dfc/globalmount\"" pod="atlantis/atlantis-0"
[pod_workers.go:1298] "Error syncing pod, skipping" err="unmounted volumes=[atlantis-storage], unattached volumes=[], failed to process volumes=[]: context deadline exceeded" pod="atlantis/atlantis-0" podUID="83089f13-2d9b-46ed-a4d3-cba885f9f48a"
[util.go:30] "No sandbox for pod can be found. Need to start a new one" pod="atlantis/atlantis-0"
最後の2つのメッセージは、最終的にポッドが適切に起動するまで数回ループしました。kubeletはポッドが他の点では準備完了と考えているが、起動せず、何かがタイムアウトしています。
欠けているピース
ポッドに関する最低レベルのログでは何が起こっているかわかりませんでした。他に何を調べることができるでしょうか?
ハングする前の最後のメッセージは、PVがノードにマウントされることです。通常、PVにマウントの問題がある場合(例:他のノードにまだマウントされたまま)、それはイベントとして表面化します。しかし、ここでは何かが起こっており、詳しく調べることができるのはPV自体だけです。
そこで、PV名は十分にユニークで良い検索語になるため、それをKibanaに入力すると...すぐに何かが目に飛び込んできました:
[volume_linux.go:49] Setting volume ownership for /state/var/lib/kubelet/pods/83089f13-2d9b-46ed-a4d3-cba885f9f48a/volumes/kubernetes.io~csi/pvc-94b75052-8d70-4c67-993a-9238613f3b99/mount and fsGroup set. If the volume has a lot of files then setting volume ownership could be slow, see https:
冒頭でinodeが不足したと言ったことを覚えていますか?つまり、このPVには多くのファイルがあります。PVがマウントされるとき、kubeletはchgrp -Rを実行して、このファイルシステム全体のすべてのファイルとフォルダのグループを再帰的に変更しています。高速フラッシュストレージでも、それは膨大なエントリを走査することになるため、時間がかかるのも当然です!
ポッドのspec.securityContextにはfsGroup: 1が含まれており、これによりGID 1で実行されるプロセスがボリューム上のファイルにアクセスできるようになります。Atlantisは非rootユーザーとして実行されるため、この設定なしではPVの読み書き権限がありません。Kubernetesがこれを強制する方法は、マウントされるたびにPV全体の所有権を再帰的に更新することです。
修正
この修正は英雄的に...退屈でした。バージョン1.20以降、Kubernetesはpod.spec.securityContextにfsGroupChangePolicyという追加フィールドをサポートしています。このフィールドはデフォルトでAlwaysになっており、ここで見られる正確な動作につながります。もう1つのオプションOnRootMismatchがあり、PVのルートディレクトリに適切な権限がない場合のみ権限を変更します。
PVでファイルがどのように作成されるかを正確に知らない場合は、fsGroupChangePolicy: OnRootMismatchを設定しないでください。
PV内の何もグループを変更すべきでないことを確認してから、そのフィールドを設定しました:
spec:
template:
spec:
securityContext:
fsGroupChangePolicy: OnRootMismatch
現在、Atlantisの再起動には約30秒かかり、開始時の30分から大幅に短縮されました。
まとめ
デフォルトのKubernetes設定は小さなボリュームには適切ですが、データが成長するとボトルネックになる可能性があります。私たちにとって、fsGroupChangePolicyへの1行の変更により、月に約50時間のブロックされたエンジニアリング時間を取り戻しました。これは、チームがインフラストラクチャの変更を待つのに費やしていた時間であり、オンコールエンジニアが誤報に対応するのに費やしていた時間でした。年間約600時間が生産的な作業に戻されたのです。診断よりもデプロイに時間がかからない修正からです。
Kubernetesの安全なデフォルトは、小さくシンプルなワークロード向けに設計されています。しかし、スケールするにつれて、徐々にボトルネックになる可能性があります。大きな永続ボリュームでワークロードを実行している場合は、このような再帰的な権限変更が静かに再起動時間を食っていないかチェックする価値があります。
securityContext設定、特にfsGroupとfsGroupChangePolicyを監査してください。OnRootMismatchはv1.20以降で利用可能です。
すべての修正が英雄的で複雑である必要はなく、通常は「なぜシステムはこのように動作するのか?」と問うことが価値があります。
スケールでのインフラストラクチャ問題のデバッグに興味がある場合は、採用中です。Cloudflare CommunityやDiscordで技術的な話をしましょう。
Cloudflareの接続クラウドは企業ネットワーク全体を保護し、顧客がインターネット規模のアプリケーションを効率的に構築し、あらゆるWebサイトやインターネットアプリケーションを高速化し、DDoS攻撃を防御し、ハッカーを寄せ付けず、Zero Trustへの道のりを支援します。任意のデバイスから1.1.1.1にアクセスして、インターネットをより高速で安全にする無料アプリを始めましょう。より良いインターネットの構築を支援するという私たちの使命について詳しく知るには、こちらから始めてください。新しいキャリアの方向性をお探しの場合は、求人情報をご確認ください。