はじめに
Kubernetesは、コンテナの配置や運用を自動化するソフトウェアですが、中でどんな部品が動いていて、どうやって自動化しているのかを説明できますか?
この記事は、Kubernetesを構成する部品を、kubectl apply というコマンドを実行してからコンテナが動き出すまでの流れに沿って整理したものです。
前回の記事で扱ったDockerの用語を前提に、OCI(Oracle Cloud Infrastructure。Oracleのクラウドサービス)上でKubernetesを動かすサービスであるOKEを使って、流れの記録を実際に確認します。
前半でKubernetesを構成する要素を整理し、後半でOKEの実際のクラスタでその動きを確認します。
先に手を動かすところから始めたい場合はOKEで流れを確かめるへ飛んでください!
この記事は、学習の過程で調べた内容を自分なりの解釈も含めて整理したもので、所属する組織の公式な見解ではありません。
正確な定義が必要な場合は、参考に載せている公式ドキュメントをご確認ください。
Dockerだけで運用するときの課題
前回の記事では、1台のVMの上で docker run を実行し、nginx(Webサーバー)のコンテナを起動しました。
この方法のまま、Webサービスを運用し続ける場面を考えます。
例えば、アクセスの増加に備えてVMを3台に増やし、nginxのコンテナを合計6つ動かすとします。
-
置き場所の判断 — どのVMに何個のコンテナを置くかを人が決め、そのVMにSSHでログインして
docker runを実行する。VMごとのCPUやメモリの空きも人が確認する - 止まったときの対応 — コンテナやVMが止まったことに気づき、別の場所でコンテナを起動し直す
- 数の変更 — 6つを10個に増やすときも減らすときも、VMごとにコマンドを実行する
(Dockerにも、止まったコンテナを同じVMの上で自動的に起動し直す設定(再起動ポリシー)はあります。ただし、VMそのものが止まったときに、別のVMでコンテナを起動し直す機能はDocker Engine単体にはありません。)
Kubernetesは、こうした作業を人の代わりに行うソフトウェアです。
使う側は、コンテナを1つずつ起動する命令を出す代わりに、「nginxのコンテナを6つ動かしておく」というあるべき状態をファイルに書いて渡します。
Kubernetesは、実際の状態をそのファイルの内容と比べ続け、違いがあれば置き場所を決めてコンテナを起動したり、足りない分を作り直したりします。
このように、手順ではなく状態を書いて渡す方式は、宣言的(declarative) と呼ばれます。(反対の「手順を1つずつ命令する」やり方は imperative(命令的)。)
ファイルを受け取ってからコンテナを起動するまでの作業は、Kubernetesの中の複数の部品が分担しています。
まず、その部品の構成を見ていきます。
Kubernetesクラスタの構成
Kubernetesは、複数のマシンを束ねて1つのシステムとして扱います。
このまとまりをクラスタ、クラスタに参加している1台1台のマシンをノードと呼びます。
ノードの実体は、前回の記事で作ったようなVM(または物理サーバー)です。
クラスタの中では、Kubernetesはコンテナを直接扱わず、Podという単位で扱います。
Podは1つ以上のコンテナをまとめた入れ物で、Kubernetesがノードに配置したり起動したりするときの最小の単位です。
この記事の例では、1つのPodにnginxのコンテナが1つ入ります。
クラスタは、役割の異なる2つの部分でできています。
- コントロールプレーン — クラスタ全体の状態を管理し、Podの置き場所などを決める部品の集まり
- ワーカーノード — 実際にPodを動かすノード
構成を図にすると、次のようになります。(図1)
kubectlは、このクラスタの外から操作するコマンドです。
図に登場する部品の役割は次のとおりです。
| 部品 | 動く場所 | 役割 |
|---|---|---|
| kube-apiserver | コントロールプレーン | クラスタへの指示を受け付ける窓口。部品どうしのやり取りもここを通る |
| etcd | コントロールプレーン | クラスタの状態を保存するデータベース |
| kube-controller-manager | コントロールプレーン | あるべき状態と実際の状態を比べ、Podの数などを合わせる |
| kube-scheduler | コントロールプレーン | Podを置くノードを決める |
| kubelet | 各ワーカーノード | 自分のノードに割り当てられたPodのコンテナを起動し、状態を報告する |
| コンテナランタイム | 各ワーカーノード | kubeletの指示でイメージを取得し、コンテナを実際に動かす |
(ほかにも、コントロールプレーンにはクラウドサービスと連携するcloud-controller-manager、ワーカーノードにはPod宛ての通信を振り分けるkube-proxyが動いています。Podが起動するまでの流れには直接関わらないため、この記事では扱いません。)
これらの部品が、1つの指示を受けてどう動くのかを順に追っていきます。
kubectl applyからPodが動くまで
nginxのPodを3つ動かす指示を例に、指示が各部品にどう渡っていくのかを見ていきます。
マニフェストとkubectl
あるべき状態を書いたファイルをマニフェストと呼びます。
マニフェストは、YAML(字下げで階層を表すテキストの書式)で書きます。
nginxのPodを3つ動かし続けるマニフェストは、次のようになります。
apiVersion: apps/v1
kind: Deployment # リソースの種類。ここではDeployment
metadata:
name: nginx # このDeploymentの名前
spec:
replicas: 3 # 動かしておくPodの数
selector:
matchLabels:
app: nginx # このラベル(目印)が付いたPodを管理対象にする
template: # ここから下が、作るPodのひな形
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: docker.io/library/nginx:1.27 # 使うイメージ
resources:
requests:
cpu: 100m # CPUを0.1個分確保する
memory: 128Mi # メモリを128MiB確保する
kind: Deployment のDeploymentは、「このひな形のPodを、この数だけ動かしておく」という指定をまとめたリソース(Kubernetesが管理する設定や状態の単位)です。
replicas: 3 がPodの数、template の下が作るPodのひな形です。
resources.requests は、Podが動くために確保しておきたいCPUとメモリの量で、置き場所を決めるときの判断材料になります。
(イメージ名を nginx ではなく docker.io/library/nginx:1.27 とレジストリの場所から書いているのは、OKEのKubernetes 1.34.1以降では、イメージ名を省略せずに書くことが求められているためです。前回の記事でDocker Hubから取得していたのと同じ、nginx公式イメージです。)
このマニフェストをクラスタに渡すのがkubectlです。
kubectlはKubernetesのクラスタを操作するためのコマンドで、Dockerにおける docker コマンドにあたります。
kubectl apply -f ファイル名 を実行すると、ファイルに書かれた内容がクラスタに送られます。
ここから先、Podが動き出すまでの流れを図にすると、次のようになります。
図2:
図の①〜⑥の番号に沿って、部品ごとに説明します。
kube-apiserverとetcd
kubectlが送った内容を受け取るのは、コントロールプレーンのkube-apiserverです(図2 ①)。
kube-apiserverは、クラスタに対する操作を受け付けるAPI(プログラムから機能を呼び出すための窓口)を提供する部品です。
受け取った内容は、送り主にその操作が許可されているか、書き方に誤りがないかを確認したうえで、etcdに保存されます(図2 ②)。
etcdは、キーと値の組でデータを保存するデータベースで、クラスタにどんなリソースがあり、それぞれがどんな状態かを記録しています。
この時点で起きたのは、「nginxのPodを3つ動かす」というDeploymentの記録がetcdに書き込まれたことだけです。
Podはまだ1つも作られていません。
kube-apiserverは、kubectlからの指示だけでなく、クラスタの部品どうしのやり取りも受け持っています。
この後に出てくるkube-controller-manager、kube-scheduler、kubeletは、etcdを直接読み書きしません。
kube-apiserverを通して記録を読み、記録に変化があれば通知を受け取ります。
各部品は、この通知で自分の担当する変化に気づいて動き出します。
kube-controller-manager
Deploymentが記録されたことに気づいて動くのが、kube-controller-managerです(図2 ③)。
kube-controller-managerの中では、コントローラーと呼ばれるプログラムが、リソースの種類ごとに動いています。
コントローラーは、etcdに記録された「あるべき状態」と「実際の状態」を比べ、違いがあれば実際の状態を近づける処理を繰り返します。
今回の例では、あるべきPodの数が3、実際の数が0です。
そこでコントローラーは、マニフェストのひな形をもとにPodを3つ作成します。
ただし、ここで作られるのはPodの記録で、どのノードで動かすかはまだ決まっていません。
(正確には、DeploymentとPodの間に、Podの数を保つ役割のReplicaSetというリソースが挟まります。Deploymentを担当するコントローラーがReplicaSetを作り、ReplicaSetを担当するコントローラーがPodを作る、という2段階です。Deploymentが更新のときに新しいReplicaSetを作って古い方と入れ替えるため、数を保つ役割が分かれています。)
実際の数をあるべき数に合わせるこの動きは、Podを初めて作るときだけのものではありません。
Podが消えて実際の数が2に減れば、コントローラーはその差に気づいて、Podを1つ作り直します。
replicas を5に書き換えたマニフェストを渡し直せば、差の2つ分のPodを作ります。
kube-scheduler
置き場所が決まっていないPodに気づいて動くのが、kube-schedulerです(図2 ④)。
kube-schedulerは、Podごとに次の2段階でノードを選びます。
-
絞り込み — Podの
resources.requestsに書かれたCPUとメモリを確保できるか、といった条件で、置けるノードだけを残す - 順位付け — 残ったノードに点数を付け、点数の高いノードを選ぶ
選んだノードは、kube-apiserverを通してPodの記録に書き込まれます。
この時点でも、まだコンテナは起動していません。
kube-schedulerが行うのは置き場所を決めて記録するところまでで、コンテナを起動するのは次に説明するkubeletです。
条件を満たすノードが1つもない場合、Podは置き場所が決まらないまま、ステータスがPending(保留中)の状態で待ち続けます。
ノードの空きが増えるなどして条件を満たすノードが現れれば、その時点でkube-schedulerが置き場所を決めます。
ノードを自動で増やす仕組み(Cluster AutoscalerやKarpenterなど)は、この置き場所が決まらないPodをきっかけに動きます。
(Pendingは、コンテナの起動が終わっていない状態全般を指すステータスです。置き場所が決まった後、イメージを取得している間もPendingと表示されます。)
kubeletとコンテナランタイム
各ワーカーノードでは、kubeletという部品が動いています。
kubeletは、kube-apiserverを通して、自分のノードに割り当てられたPodがないかを見ています。
kube-schedulerによって自分のノードが書き込まれたPodを見つけると、kubeletはそのPodのコンテナを起動します(図2 ⑤)。
コンテナを実際に起動するのは、ノードに入っているコンテナランタイムです。
前回の記事では、Docker Engineの内部でcontainerdというコンテナランタイムが動いていることを紹介しました。
Kubernetesでは、kubeletがDocker Engineを介さずに、コンテナランタイムへ直接指示を出します。
使われるランタイムは環境によって異なり、OKEのワーカーノードではCRI-Oが使われています。
ランタイムがDocker Engineと違っても、Dockerでビルドしたイメージはそのまま使えます。
Dockerのイメージは、OCI(Open Container Initiative)という団体が定めたコンテナの共通仕様に沿っていて、CRI-Oもこの仕様に沿ったイメージを扱うためです。
(ここでのOCIは、Oracle Cloud Infrastructureとは別のものです。)
指示を受けたランタイムは、イメージがノードになければレジストリから取得し、コンテナを起動します。
コンテナが動き出すと、kubeletはPodの状態をkube-apiserverに報告します(図2 ⑥)。
報告された状態はetcdに記録され、kubectl get pods で確認したときに Running(実行中)と表示されます。
kubeletは、起動した後もコンテナを見続けています。
コンテナのプロセスが終了した場合は、同じノードの上でコンテナを起動し直します。
Dockerだけで運用するときの課題との対応
最初に挙げた課題が、どの部品によって自動化されているかをまとめます。
| 課題 | 担当する部品 | 仕組み |
|---|---|---|
| 置き場所の判断 | kube-scheduler | Podが確保したいCPUとメモリをもとに、置けるノードを選ぶ |
| 止まったときの対応 | kubelet、kube-controller-manager | コンテナが止まれば、kubeletが同じノードで起動し直す。ノードが止まってPodが失われれば、コントローラーが代わりのPodを作り、kube-schedulerが別のノードに置く |
| 数の変更 | kube-controller-manager |
replicas を書き換えたマニフェストを渡せば、コントローラーが差の分だけPodを増やす、または減らす |
OKEで流れを確かめる
ここまでの流れが、実際のクラスタでどう見えるのかを確かめます。
Podがどのノードに置かれたか、kube-schedulerとkubeletが動いた記録が残っているか、Podを消すと作り直されるかを確認します。
あわせて、OKEではコントロールプレーンの部品がどう見えるのかも確認します。
前提:動かした環境
- OKE(Oracle Kubernetes Engine。OCI上でKubernetesのクラスタを作成、運用できるサービス)のクラスタ。Basicクラスタ、Kubernetesバージョンv1.36、VM.Standard.A1.Flex(1 OCPU / 6 GB)
- 操作は、OCIコンソール(ブラウザで開くOCIの管理画面)のCloud Shell(ブラウザ上で使えるターミナル)から行う。Cloud Shellには、kubectlが最初から入っている
- kubectlの接続設定は、OKEのクラスタ詳細画面にある「クラスタへのアクセス」に表示されるコマンドを、Cloud Shellで実行して済ませている
上記のデプロイ手順は、こちらの記事にまとめていますので、環境構築から行う場合は以下の記事から実行してください。
ワーカーノードを確認する
では早速、クラスタに参加しているノードを表示します。
# get はリソースの一覧を表示するサブコマンド。-o wide を付けると表示する列が増える
kubectl get nodes -o wide
1行が1台のワーカーノードで、OCIコンソールのコンピュート・インスタンスの一覧にも、同じ台数のVMが並んでいます。
STATUS 列の Ready は、そのノードがPodを受け入れられる状態であることを示しています。
CONTAINER-RUNTIME 列には、コンテナランタイムとして cri-o が表示されています。
一方で、この一覧にはコントロールプレーンのノードが表示されていません。
コントロールプレーンの部品を探す
Kubernetesには、クラスタの中のリソースを用途ごとに分けて扱うための区画であるNamespaceがあります。
Kubernetes自身の動作に関わるPodは、kube-system というNamespaceに置かれます。
# -n で表示するNamespaceを指定する
kubectl get pods -n kube-system
kubeadm(Kubernetesのクラスタを自分で構築するためのツール)で作ったクラスタでは、kube-apiserver、etcd、kube-scheduler、kube-controller-managerもPodとして動いていて、このNamespaceに並びます。
OKEの出力には、これらのPodは見当たりません。
並んでいるのは、kube-proxy、corednsといった、ワーカーノードの上で動くPodです。
OKEでは、コントロールプレーンをOracleが管理しています。
コントロールプレーンの部品は、利用者のテナンシ(OCIの契約単位)ではなく、OKEサービス側のテナンシにあるノードで動いています。
利用者のテナンシにあるのはワーカーノードで、kubectlは、利用者のVCN(OCIの中に作る仮想ネットワーク)に用意されたエンドポイント(接続先の窓口)を通して、kube-apiserverに接続しています。
図1でいえば、コントロールプレーンの枠がOracle側にある構成です。
このように、コントロールプレーンの運用をクラウドサービス側が引き受けるKubernetesは、マネージドKubernetesと呼ばれます。
Deploymentを適用する
先ほどのマニフェストをファイルに書き込み、クラスタに適用します。
# 次の行から EOF と打つまでの内容を、nginx-deployment.yaml に書き込む
cat > nginx-deployment.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: docker.io/library/nginx:1.27
resources:
requests:
cpu: 100m
memory: 128Mi
EOF
# マニフェストをクラスタに送る。deployment.apps/nginx created と表示される
kubectl apply -f nginx-deployment.yaml
作られたリソースを、種類をまとめて表示します。
kubectl get deployments,replicasets,pods -o wide
Deploymentが1つ、ReplicaSetが1つ、Podが3つ作られています。
Podの名前は、Deployment名、ReplicaSetごとの文字列、Podごとの文字列をつなげた形になっています。
Podの行の NODE 列を見ると、3つのPodがそれぞれどのワーカーノードに置かれたかが分かります。
kube-schedulerとkubeletの記録を見る
Podに対して各部品が行った処理は、イベントとして記録されています。
describe は、リソースの詳細を表示するサブコマンドです。
# Pod名は、kubectl get pods で表示された名前に置き換える
kubectl describe pod nginx-xxxxxxxxxx-xxxxx
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 6m57s default-scheduler Successfully assigned default/nginx-59b7874d9c-2q4bq to 10.0.10.222
Normal Pulling 6m55s kubelet spec.containers{nginx}: Pulling image "docker.io/library/nginx:1.27"
Normal Pulled 6m45s kubelet spec.containers{nginx}: Successfully pulled image "docker.io/library/nginx:1.27" in 10.94s (10.94s including waiting). Image size: 201906571 bytes.
Normal Created 6m44s kubelet spec.containers{nginx}: Container created
Normal Started 6m44s kubelet spec.containers{nginx}: Container started
出力の最後にある Events 欄が、このPodに起きた出来事の記録です。
Scheduled の行は、From 列が default-scheduler になっていて、kube-schedulerがこのPodをどのノードに割り当てたかが書かれています(図2 ④)。
続く Pulling、Pulled、Created、Started の行は From 列が kubelet で、イメージを取得し、コンテナを作成して起動した記録です(図2 ⑤)。
(同じノードでイメージをすでに取得していた場合は、Pulling の行が無く、Pulled の行に「already present on machine」と表示されます。)
Podを消して作り直されるかを見る
最後に、Podを1つ消してみます。
# Pod名は、kubectl get pods で表示された名前に置き換える
kubectl delete pod nginx-xxxxxxxxxx-xxxxx
kubectl get pods -o wide
消したPodの代わりに、名前の違う新しいPodが1つ作られていて、AGE(作られてからの経過時間)が数秒になっています。
Podの数が3から2に減ったことにコントローラーが気づいて、1つ作り直した結果です(図2 ③)。
新しいPodの置き場所は、kube-schedulerが改めて決めています。
片付け
確認が終わったら、Deploymentを削除します。
Deploymentを削除すると、ReplicaSetとPodも一緒に削除されます。
kubectl delete -f nginx-deployment.yaml
(クラスタ自体を使い終わった場合は、ワーカーノードのシェイプによってはVMの料金がかかり続けるため、OCIコンソールからクラスタを削除します。)
まとめ
- Kubernetesは、あるべき状態を書いたマニフェストを受け取り、実際の状態をそれに合わせ続ける
- コントロールプレーンでは、kube-apiserverが窓口となって状態をetcdに保存し、kube-controller-managerがPodを作り、kube-schedulerがPodを置くノードを決める
- ワーカーノードでは、kubeletがコンテナランタイム(OKEではCRI-O)を使ってコンテナを起動し、状態を報告する
- OKEではコントロールプレーンをOracleが管理しているため、kube-systemにその部品のPodは表示されない
今回の例では、3つのPodすべてに置けるノードが見つかりました。
ワーカーノードのCPUやメモリが足りずに置き場所が見つからない場合、PodはPendingのまま待ち続けます。
次は、resources.requests を大きくしてPodをPendingにし、その理由を kubectl describe pod で確認してみます。
少しでも参考になれば嬉しいです。








