このコンテンツは自動機械翻訳サービスによる翻訳版であり、皆さまの便宜のために提供しています。原本の英語版と異なる誤り、省略、解釈の微妙な違いが含まれる場合があります。ご不明な点がある場合は、英語版原本をご確認ください。
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はディスク上の各ファイルおよびディレクトリエントリによって消費され、ファイルシステムに使用できる数はファイルシステムの作成時に渡されるパラメータによって決定されます。Kubernetesプラットフォームによって提供されるCeph永続ストレージの実装は、mkfsにフラグを渡す方法を公開していないため、デフォルト値に従うしかありません。ファイルシステムの拡張が利用可能なイノードを増やす唯一の方法であり、PVを再起動するには、Podの再起動が必要です。
先ほど、アラート期間の延長についてお話しましたが、それは問題を隠蔽するだけで、実際の問題への対応が遅れてしまいます。そこで、なぜ時間がかかるのか、正確に調査することにしました。
使用するシークレットの変更を適用するためにAtlantisのローリングリスタートを依頼された際、我々はkubectl rollout restart statefulset atlantisを実行しました。これにより、既存のAtlantisポッドは正常に終了し、新しいポッドが起動します。新しいポッドはすぐに表示されますが、実際に見てみると:
$ kubectl get pod atlantis-0
atlantis-0 0/1
Init:0/1 0 30m
...それでは、何が実現するのでしょうか?当然、最初にチェックするのは、そのポッドのイベントです。これは開始コンテナが実行されるのを待っているので、ポッドイベントがその理由を明らかにするのでしょうか?
$ 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が搭載されているのがわかりました。また、すべてのシークレットボリュームが問題なく増加することも確認されました。しかし、ログにはまだ大きな未説明のギャップがありました。Cloudflareの調査結果:
[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自体だけです。そこで、Kibanaに接続すると、PV名がユニークなため、検索条件として十分
[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://github.com/kubernetes/kubernetes/issues/69699
当初、私はイノードが不足すると言ったのを覚えていますか?つまり、このPVには多くのファイルがあるのです。PVがマウントされている場合、kubeletはchgrp -Rを実行して、このファイルシステム全体のすべてのファイルとフォルダのグループを再帰的に変更します。移行に時間がかかるのも不思議ではありません。これは高速ストレージですら大量のエントリーを作成することです!
ポッドのspec.securityContextfsGroup: 1が含まれており、GID 1の下で実行されているプロセスが、ボリューム上のファイルにアクセスできるようにしています。Atlantisは非ルートユーザーとして実行されるため、この設定がないとPVの読み取りや書き込み許可が得られません。Kubernetesはこれを実施する方法として、PV全体に対してマウントされるたびに再帰的に所有権を更新しています。
これを修正するのは、途方もない作業でした。バージョン1.20以降、Kubernetesはpod.spec.securityContextの追加フィールドをサポートしています。fsGroupChangePolicyというものです。このフィールドのデフォルトはAlwaysで、これがまさにここでの動作となります。別のオプションとして、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コミュニティまたはDiscordに参加して、情報交換しましょう。