懐疑論者が初めてKubernetesに触れる (2024年)
A skeptic's first contact with Kubernetes (2024) (blog.davidv.dev)
要約
長年システム管理に携わってきた著者が、当初は複雑すぎると敬遠していたKubernetesに初めて触れた体験を綴る記事。Kubernetesの基本的な構成要素に加え、その価値の源泉である「コントロールループ」と「サービス」の概念に焦点を当て、実用的な視点から解説しています。特に、Podの管理やストレージの永続化についても触れ、Kubernetesの理解を深める内容となっています。
全文翻訳
懐疑論者が初めてKubernetesに触れる
私は長年、システム管理/エンジニアリング/インフラストラクチャ開発に携わってきましたが、なぜかKubernetesとの関わりを避けてきました。この回避は偶然ではなく、私の仕事では物理的な場所へのワークロード配置を非常に厳密に制御する必要があったためです。
時が経つにつれて、私はKubernetesに対して懐疑的な見方をするようになりましたが、それはしっかりした根拠に基づくものではなく、「物事の新しいやり方」や「不必要な複雑さ」といった漠然とした考えに基づいたものでした。
自分の意見を認識ではなく事実に基づいて形成すべき時が来たと判断し、ここで学んだことを書いています。
Kubernetesクラスターの使用法や運用、そのコンポーネントの基本的な説明をカバーするチュートリアルは数多くありますが、それらは私にはあまり役立ちませんでした。これらのリソースの多くは、コンポーネントが何をするかを軽く触れるだけで、その根本的な理由やトレードオフを見落としがちです。
この記事は、私が有用だと感じただろう視点からこれらの概念をカバーすることを目的としており、網羅的であることを目指すものではありません。
「標準的」な説明
Kubernetesは、任意のワークロードを実行することを可能にし、以下の機能を提供します:
- 要件(CPU、ディスク、メモリ、インスタンス数など)の指定
- インスタンス数の動的なスケーリング
しかし、より重要なのは、Kubernetesがあなたのために何をしてくれるかです:
- アプリケーションを実行するホストの選択
- 「セルフヒーリング」(クラッシュしたインスタンスの再起動、障害のあるホストからのインスタンスの自動移動)
- DNSベースのディスカバリのためのサービス公開
そして、ワークロードをDockerイメージとしてパッケージ化することを要求するだけです。これは支払う価値のある、妥当な対価のように思えます。
クラスターを構成する基本的な構成要素は、次のように定義できます:
- Pod: Dockerイメージとその設定のセットからなるワークロードの単位
- Node: Kubernetesノードエージェント(kubelet)を実行するコンピューター。Podを実行します。
- Cluster:
Nodeの論理的な集まりとコントロールプレーン。 - Service: Podのセットの論理的なグループ化
- Namespace:
Clusterの論理的なサブディビジョン。名前のスコープ(DNS検索ドメインなど)を提供します。
これらの概念の関係は、図で観察する方が簡単です:
[画像: Kubernetesのコンポーネント間の関係を示す図]
これはまあまあな説明かもしれませんが、私にとって本当に役立つものではありませんでした。多くのインフラストラクチャプラットフォームはこの図と何らかの点で似ており、特別な点はありません。
私が見つけた重要なこと
私の意見では、Kubernetesの価値の大部分は、わずか2つの概念から得られています:
コントロールループ
ワークロードは、一連のコントロールループ(コントローラーと呼ばれる)によって管理されます。
コントロールループは、望ましい状態を達成するために、必要に応じてアクション(コントロール要素を介して)を継続的に実行し、特定の変数(センサーを介して)を観測することによってそれを行います。
興味深い詳細は、コントロール要素がセンサーが観測するものに直接影響を与えるとは限らないということです。
この説明は非常に一般的であるため、いくつかの例を挙げます:
- キューからイベントを処理する必要があるワークロード
- 望ましい状態:空のキュー
- センサー:キューの深さ
- コントロール要素:イベント処理サーバーの数
- ユーザー負荷を処理するために水平スケーリングする必要があるワークロード
- 望ましい状態:P90レイテンシをXミリ秒未満に維持する
- センサー:バックエンドからのレイテンシメトリクス
- コントロール要素:需要に対応するための新しいPodの起動
- 一般的なヘルスモニタリング
- 望ましい状態:すべてのノードが正常である
- センサー:ノードからのヘルスメトリクス
- コントロール要素:クラスターからのノードの削除
[画像: コントロールループの概念図]
サービス
Serviceは、一連のバックエンドPodへの安定したアクセスを提供するネットワーキングレベルの概念であり、以下の機能を提供します:
- 安定したID: 不変の(仮想)IPアドレスとDNSレコード
- ロードバランシング: Pod全体への着信トラフィックを分散します
- サービスディスカバリ: 他のコンポーネントがサービスを見つけて通信できるようにします
「従来の」クラスターと比較して、Serviceは以下の組み合わせと同様の抽象化を提供します:
- Keepalived + NGINX(動的なバックエンドセットを持つ)を実行するN個のノード
- 仮想(Keepalived)IPのCNAMEレコード
新しいServiceが作成されるたびに:
- 仮想IPがそれに割り当てられます
- すべてのノードは、各Serviceの仮想IP宛てのパケットをバックエンドPodに転送するようにネットワーク設定(
netfilter)を更新します。
ネットワーク設定は、各ノードで実行されるコントローラーによって更新されます:kube-proxy。その主な責任は、すべてのServiceオブジェクトの状態を監視し、変更ごとにnetfilterルールを更新することです。
この設計により、すべてのノードがServiceトラフィックのロードバランサーとして機能するため、Serviceトラフィック転送における単一障害点がなくなり、トラフィック転送容量はクラスターサイズとともにスケーリングします。
私にとって、この設計の興味深い特性は、すべてのノードがすべてのServiceのルールを受け取ることです。これにはいくつかの影響があります:
kube-proxyの非常にシンプルな実装- ルールの数が多くなる可能性があり、特定のトラフィック転送実装(
iptablesは各ルールを線形に評価する)では問題となる可能性があります。 - 一部のシナリオではトラフィックが複数回転送され、追加のクラスター負荷と引き換えに高可用性を提供します。
- Podが別のノードに移動されると、古いDNSエントリが期限切れになるまでトラフィックは2回転送されます。
kube-proxyレベルでのDNSエントリのキャッシュ無効化を処理する必要がありません。
- 問題のあるクライアント(例:再接続前にDNSを再解決しないクライアント)は引き続き機能します。
- Podが別のノードに移動されると、古いDNSエントリが期限切れになるまでトラフィックは2回転送されます。
「Pod 1」から「Service 1」へ移動するパケットのライフサイクルを見てみましょう:
[画像: PodからServiceへのパケットフローを示す図]
ワークロード管理
または「物事がいつ、どこで、どのように実行されるか」
以下のようなYAML定義を使用して、Podを手動で作成できます:
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
しかし、そうすることで、このPodの実行を手動で一度だけスケジュールすることになります。これは簡単ですが、いくつかの厄介な制限があります:
- Podをホストしているノードがダウンした場合、Podは再スケジュールされません。
- ノード上のリソース不足(エビクションによる)によりPodが終了した場合も、再スケジュールされません。
- 「このPodを2つ欲しい」という要求はできません。
Podを直接作成する代わりに、ReplicaSet(コントローラー)を作成できます。そのコントロールループの一部として、常に適切な数のPodレプリカが実行されていることを保証します。これにより、ノード障害やエビクションによるPodの消失の問題が解決されます。
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: nginx-replicaset
spec:
replicas: 3
template:
spec:
containers:
- name: nginx
image: nginx:1.14.2
ReplicaSetを使用すると、レプリカの数を変更することはできますが、それ以外のことはできません(例:nginxのバージョンを更新したり、別のポートを公開したりする場合)。その場合はリソースを削除して再作成する必要があります。
これらの制限に対処するために、Deploymentを使用できます。これはコントローラーであり、最終的に適切なイメージを持つ適切な数のPodが実行されていることを保証します。
最終的にという部分は非常に重要です。なぜなら、Deploymentを使用すると、Podが実行するイメージ(またはバージョン)を更新し、変更のレートを選択できるため、「バージョンアップグレード」の標準的なワークフローをカバーできるからです。
言い換えれば、Deploymentを更新すると、新しいReplicaSetが作成され、古いReplicaSetをスケールダウンしながら徐々にスケールアップすることで、ローリングアップデートが可能になります。
ストレージ管理
または「データがどのように永続化され、Podとともに移動するか」
デフォルトでは、Pod内のコンテナはストレージ/ファイルシステムを共有しません。
Pod内のコンテナ間でストレージを共有したい場合は、Ephemeral Volumeを使用できます。これはPodのライフサイクルに紐づいており、Podがシャットダウンするとデータは削除されます。
Ephemeral Volumeは無用に見えますが[1]、いくつかのユースケースがあります:
- サイドカーからのログ転送
- サイドカーがメインアプリケーションのキャッシュを生成する
実際に永続的なストレージが必要な場合は、Persistent Volumeを使用できます。これはPodのライフサイクルから切り離された[2]ライフサイクルを持ちます。
以下のようなClaimでこのようなボリュームを定義できます:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: nginx-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
そして、Pod内で以下のように使用できます:
apiVersion: v1
kind: Pod
metadata:
name: nginx-with-pv
spec:
containers:
- name: nginx
image: nginx
volumeMounts:
- name: html-volume
mountPath: /usr/share/nginx/html
volumes:
- name: html-volume
persistentVolumeClaim:
claimName: nginx-pvc
興味深い点がいくつかありました:
- 同じClaimの複数のユーザー(Pod)はストレージを共有しますが、複数の書き込みがある場合は常にリスクが伴います。
- PersistentVolumeはさまざまなドライバー(NFS、SMB、iSCSIなど)によってバックアップされますが、どのドライバーが使用されているかはPodには明確ではなく、Podは(アトミックなリネームなどの)機能に依存できません。
インスタンスごとに永続的なストレージが必要な場合は、StatefulSetを使用できます。これは単一のリソースで、ワークロード(Pod)、レプリカ数、マウント設定、およびボリュームクレームテンプレートを定義します。
StatefulSetに関する興味深い点は以下の通りです:
- Podには安定したホスト名(
nginx-0、nginx-1など)があります。 - デプロイメントは順序通りに実行されます:
- Podの作成(昇順:0、1、...)
- Podの削除(降順:3、2、...)
- Podの更新(降順:3、2、...)
これは奇妙に思え、オンラインで見つけた説明によると、番号が小さいインスタンスは「コア」または「より安定している」とのことです。クラッシュしやすいPodは番号が再採番されないので、私には理解できません。おそらく、降順の「アップタイム」順に更新する方が良いのでしょうか?もしかしたら、この「シニアリティ/安定性」はPodのために後付けされたもので、Kubernetesは単に順序が必要だっただけなのかもしれません。
運用面では、この時点で、Persistent Volumeの内容を手動で調査する方法が、実行中のコンテナにアタッチしてコマンドを実行する以外に不明です。コンテナにシェルがない場合はどうなるのでしょうか?
スコアカード
Kubernetesの主要な部分であるコントロールループ、サービス、ワークロード、ストレージについて説明しました。これらの概念を学ぶことで、私は求めていた(ただし、まだクラスターを運用したり使用したりしたわけではありませんが)より根拠のある意見を形成することができました。
全体として、概念は非常に理にかなっていると思います。特に、サービス(ネットワークモデルとkube-proxyを含む)は素晴らしいですし、リソース管理のエンジンとしてコントローラーパターンを維持することは正しい運用方法だと思います。
いくつかの未解決の質問/不満が残っています。これらは皆さんの楽しみのためにここに残しておきます。
コントローラーのドキュメントには次のように書かれています:
クラスターは、作業が行われるにつれていつでも変化する可能性があり、コントロールループは自動的に障害を修正します。これは、潜在的にクラスターが安定した状態に決して到達しないことを意味します。
クラスターのコントローラーが実行されており、有用な変更を加えることができる限り、全体の状態が安定しているかどうかは関係ありません。
なぜ状態が不安定であることが問題にならないのでしょうか?もし私が安定しないクラスターを運用しているなら、すぐに知りたいです!
- コントローラーパターンを考えると、なぜ「クラウドネイティブ」アーキテクチャのサポートがないのでしょうか?
- キューの深さの単純な計算(例:キューの深さ / 16 = レプリカ数)に基づいてレプリカ数をスケーリングするReplicaSetが欲しいです。
- これらのイベント(キューの深さ、オープン接続、応答レイテンシ)のインターフェースを定義するのは素晴らしいでしょう。
- 基本的に、CPU以外のセンサーを持つHorizontal Pod Autoscalerです。
- なぜストレージとネットワークの実装が「アウトオブツリー」(CNI / CSI)なのですか?
- 「モジュール性」と「関心の分離」のためだと仮定します。
- 上記の質問を考えると、なぜクラウドプロバイダーの明示的なサポートがあるのですか?
- 例:LoadBalancerはAWS/GCP/Azure/...をサポートしています。
そして、最大の未解決トピックについて、これはKubernetesのせいではありませんが、言いたいことがあります。
文字列ベースの型
Kubernetesの「コミュニティ」は、何よりもまずテキスト補間に基づいてツールを構築することで、自分たちの生活をより困難にすることに躍起になっているようです。まるでAnsibleを見て、「うわー、これはひどく痛そうだ。これをやろう!」と思ったかのようです。
例として、このRabbitMQスケール機能(KEDA、Microsoftがメンテナンス)を見てみましょう:
value: トリガーするメッセージバックログまたはPublish/sec.レート。(mode: MessageRate の場合、この値は浮動小数点数になることがあります)
valueの型がmodeの値に依存するとはどういう意味ですか?そして、modeは列挙型であるべきなのに文字列であるとはどういう意味ですか?
Helmが人気を博したのはどうしてでしょうか?このようなツールが、ありとあらゆるものの中からテキストテンプレート処理を行うことがどうして可能なのですか?
この任意のNGINXチャートを見て、ほとんどの値が手動の文字列インデントでどのように処理されているか確認してください:
{{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} 14 {{- end }}
または
{{- toYaml { "rollingUpdate": {}, "type": "RollingUpdate" } .Values.updateStrategy | nindent 4 }}
なぜ私たちは、コンピューターを使って、構文を有効にするために手動でスペースを追加することで、構造化言語(YAML)を生成しているのでしょうか?このような中間的なテキストテンプレート表現は存在するべきではありません。