はじめに
3章で完成したmanifestをローカル環境で動作する軽量なk8sクラスタであるkind(Kubernetes in Docker)にデプロイしてみます。
kindを使えばお金もかからず、簡易的にローカルマシンでk8sの動作を確認できるため、まずはEKSにデプロイする前にお手軽ローカルで試してみましょうと言う章です。
ハンズオンのゴールからは若干それますが、構成図のEKS Clusterの部分をkindに置き換えて試してみると言う感じです。
雑ですが、構成図を改変して以下のようにあらわしてました(強引)。
Kindとは
Kindは、DockerコンテナをNodeとして転用し、クラスターとして構成してくれる開発用ツールです。 Kubernetes In DockerでKindのようです。
Kindに関しては深入りせず、早速デプロイしてk8sを動かしてみましょう。
Kindクラスターにデプロイする
以下の手順でデプロイします。
- Kindクラスター用のconfigファイルを用意
- Kindクラスターを起動
- manifestを適用
以下に従ってKindをインストールしておいてください。
Kind用のconfigファイルを用意
Worker Nodeをマルチノードとしたり、Kindクラスターの設定をconfigファイルで管理できます。
以下のようにファイルを記述します。
1つのControle Planeノード、1つのWorkerノードをクラスタに用意する構成です。
# k8s/cluster/kind/config.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
extraPortMappings:
- containerPort: 30080 # NodePort
hostPort: 80
protocol: TCP
また、前章にてNodePort Serviceで公開するようにmanifestを記述しており、Worker Nodeの:30080番ポートへの通信がPodの:80番ポートにService経由で転送されるようにしていました。
Kindでは、Worker NodeはDockerコンテナとして起動されるので、以下のようにホストポートとWorker Nodeコンテナポートのマッピングをしています。
- role: worker
extraPortMappings:
- containerPort: 30080 # NodePort
hostPort: 80
protocol: TCP
これで、http://localhost:80の通信がWorker Nodeの:30080に転送されるようになります。
クラスタを起動する
それではKindクラスターを起動させていきましょう。ルートディレクトリで、以下のようにコマンドを実行します。
kind create cluster --name sandbox --config ./k8s/cluster/kind/config.yaml --image kindest/node:v1.29.0

正常に完了したら、docker psで起動されているコンテナを確かめてみてみましょう。
Control PlaneとWorker Nodeの二つのコンテナが起動しています。
% docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
67cc2193deea kindest/node:v1.29.0 "/usr/local/bin/entr…" About an hour ago Up About an hour 127.0.0.1:63801->6443/tcp sandbox-control-plane
d28bf8f3d7c4 kindest/node:v1.29.0 "/usr/local/bin/entr…" About an hour ago Up About an hour 0.0.0.0:80->30080/tcp sandbox-worker
kubectlコマンドで起動しているPodを確認します。
kubectl get pods -A
前章で扱ったetcdやapi-serverのpodがありますね。
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system coredns-76f75df574-2cz96 1/1 Running 0 14s
kube-system coredns-76f75df574-4zpbk 1/1 Running 0 14s
kube-system etcd-sandbox-control-plane 1/1 Running 0 28s
kube-system kindnet-4lrxz 1/1 Running 0 15s
kube-system kindnet-vnbwp 1/1 Running 0 10s
kube-system kube-apiserver-sandbox-control-plane 1/1 Running 0 28s
kube-system kube-controller-manager-sandbox-control-plane 1/1 Running 0 29s
kube-system kube-proxy-fhbw8 1/1 Running 0 15s
kube-system kube-proxy-rskt5 1/1 Running 0 10s
kube-system kube-scheduler-sandbox-control-plane 1/1 Running 0 28s
local-path-storage local-path-provisioner-6f8956fb48-hz4p7 1/1 Running 0 14s
どのpodがどのNodeに乗っているかも見るために、.spec.nodeNameも表示させて確認してみましょう。
kubectl get pods -A -o custom-columns=NAMESPACE:.metadata.namespace,NAME:.metadata.name,NODE:.spec.nodeName --sort-by='.spec.nodeName'
以下のような出力になりました。
NAMESPACE NAME NODE
kube-system coredns-76f75df574-2cz96 sandbox-control-plane
kube-system coredns-76f75df574-4zpbk sandbox-control-plane
kube-system etcd-sandbox-control-plane sandbox-control-plane
kube-system kindnet-4lrxz sandbox-control-plane
kube-system kube-apiserver-sandbox-control-plane sandbox-control-plane
kube-system kube-controller-manager-sandbox-control-plane sandbox-control-plane
kube-system kube-proxy-fhbw8 sandbox-control-plane
kube-system kube-scheduler-sandbox-control-plane sandbox-control-plane
local-path-storage local-path-provisioner-6f8956fb48-hz4p7 sandbox-control-plane
kube-system kindnet-vnbwp sandbox-worker
kube-system kube-proxy-rskt5 sandbox-worker
kube-systemは、k8sに必要なシステムコンポーネントが配置されるnamespaceと捉えて良いかと思います。
Podを以下に整理します。
- Controle Plane:
- etcd
- kube-controller-manager
- kube-api-server
- kube-scheduler
- cordns (×2)
- kube-proxy
- kindnet
- local-path-provisioner
- Worker Node:
- kube-proxy
- kindnet
ここで、(筆者的には)気になるポイントがいくつかあります。
- kube-proxyがWorker NodeだけでなくControl Planeにもある
- corednsって何
- kindnetって何
- kubeletはPodではない?
デプロイが完了した段階で、この点に関して掘っていくとしましょう。
クラスタにコンテナイメージをロードする
Kindでは、デプロイするコンテナイメージを起動したクラスタにダウンロードしておく必要があります。
k8s一般の話ではないかと思うのでさくっといきます。
この作業をせずにデプロイしようとすると、以下のようなエラーが出ます。
unpack image "docker.io/library/app:20251123173531": failed to resolve reference "docker.io/library/app:20251123173531": pull access denied, repository does not exist or may require authorization: server message: insufficient_scope: authorization failed
docker.io/library、つまりDocker Hubのリポジトリにイメージを探しにいってくれています。しかし、ビルドしたイメージはローカルマシン上にしかなかったため存在せず、デプロイに失敗した感じです。
まずはイメージビルドから行います。 2章あたりでも行なったことです。
今回は、appイメージにタイムスタンプのイメージタグをつけてdocker buildしましょう。
TIMESTAMP=$(date +"%Y%m%d%H%M%S")
docker build -t app:$TIMESTAMP .
echo $TIMESTAMPとかでタイムスタンプに何が適用されたかは確認して控えておいてください。
ビルドされたイメージを以下のコマンドでkindクラスターにロードしましょう。
kind load docker-image app:{TIMESTAMP} --name sandbox
これで準備が整いました。
開発環境用のmanifestを適用させる
helmfileを使用しているので、helmfile applyでKindクラスタにデプロイが可能です。
今回ビルドしたアプリケーションコンテナのイメージを参照させるため、values.yamlのtagを書き換えましょう。
# k8s/values/development/values.yaml
deployment:
name: app
# ...
image:
repository: app
tag: "20260404024812" # <= ここをTIMESTAMPの値に変更
最後に、helmfile applyでdevelopment環境のmanifestファイルをクラスタに適用させ、デプロイを完了させましょう。
cd ./k8s
helmfile -e development apply
成功したら、アプリケーションPodが存在しているはずです。
% kubectl get pods -n app-development
NAME READY STATUS RESTARTS AGE
app-http-server-5449665dbb-68g4m 2/2 Running 0 56m
app-http-server-5449665dbb-p6rzt 2/2 Running 0 39h
app-http-server-5449665dbb-t9wvm 2/2 Running 0 39h
http://localhost/todosでアクセス可能なことも確認しましょう。
少しいじってみて、podのログを確認してみましょう。-l app=app-http-serverで、ラベルにマッチするPod(つまりReplica3つ分)すべてのログを出すようにします。
% kubectl logs -l app=app-http-server -n app-development
Defaulted container "http-server" out of: http-server, nginx
Defaulted container "http-server" out of: http-server, nginx
Defaulted container "http-server" out of: http-server, nginx
2026/04/05 09:04:02 [::1]:60614 POST /todos 303
2026/04/05 09:04:02 127.0.0.1:56306 GET /todos 200
2026/04/03 18:15:37 [::1]:50866 GET /health 404
2026/04/05 09:04:19 127.0.0.1:49632 GET /todos 200
2026/04/05 09:04:21 [::1]:37054 POST /todos 303
2026/04/05 09:04:21 127.0.0.1:49686 GET /todos 200
2026/04/05 09:04:22 [::1]:37060 POST /todos 303
2026/04/05 09:04:22 127.0.0.1:49702 GET /todos 200
また、いくつかTODOを作成し、リロードしたり、別ウィンドウで開着直してみたりを繰り返してみてください。おそらく、確率で表示が変わるはずです。
これは、TODOのリストをメモリで保持していることによるものです。リクエストをどのPodが捌くかはランダムだということがわかりますね。
気になりポイント
以下の点について、疑問点を解消していきましょう。
筆者が勝手に疑問に感じただけですが....笑。
また、このノリで行けばlocal-path-provisionerのPodに関しても取り上げるべきですが、今回は省略します。
- kube-proxyがWorker NodeだけでなくControl Planeにもある
- corednsって何
- kindnetって何
- kubeletはPodではない?
kube-proxyがControl Planeにもある
kubectl get podsで叩くとkube-proxyが出力されたことからわかるように、kube-proxyはpodとして動作しているようです。
NAMESPACE NAME NODE
// ...
kube-system kube-proxy-fhbw8 sandbox-control-plane
// ...
kube-system kube-proxy-rskt5 sandbox-worker
そして、Worker Nodeのコンポーネントとしてkube-proxyについては取り上げていました。しかし見てみると、Control PlaneとWorker Nodeの両方に配置されています。
公式documentをよく見てみると、「どのNode上でもkube-proxyは実行されている」とのことが書いてあります。
Every node in a Kubernetes cluster runs a kube-proxy (unless you have deployed your own alternative component in place of kube-proxy).
つまり、Control PlaneもNodeであるということでしょう。以下の一文のmachineは、nodeと置き換えて捉えても問題ないと思います。
Control plane components can be run on any machine in the cluster.
また、kube-proxyはDaemonSetとして展開されているので、Controle Plane Nodeにあって当然、と考えることもできますね。
それに、Control Planeには、前章で説明したようなコンポーネント(etcd, kube-apiserver, kube-scheduler)がPodとして存在していることが出力からわかります。kube-proxy は、Podへの通信(ネットワーク)に関わる制御を行なうものですから、やはりControl Planeにも必要な存在であることが言えそうですね。
corednsって何
名前の通りDNSサーバーになります。前章でも少し触れていました。
Servivceには、<service-name>.<namespace>.svc.cluster.localの規則でDNS名が割り当てられています。
ServiceリソースにはClusterIPが割り当てられています。
% kubectl describe svc app-http-server-service -n app-development
Name: app-http-server-service
Namespace: app-development
Labels: app=app-http-server
app.kubernetes.io/managed-by=Helm
Annotations: meta.helm.sh/release-name: app
meta.helm.sh/release-namespace: app-development
Selector: app=app-http-server
Type: NodePort
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.96.86.191
IPs: 10.96.86.191
Port: http 80/TCP
TargetPort: 80/TCP
NodePort: http 30080/TCP
Endpoints: 10.244.1.6:80,10.244.1.7:80,10.244.1.8:80
Session Affinity: None
External Traffic Policy: Cluster
Internal Traffic Policy: Cluster
Events: <none>
coredns podによって、<service-name>.<namespace>.svc.cluster.localのような形での名前解決を実現しています。
試しに、適当な一時Podを立てて、そこからapp-http-serverpodにcurlでリクエストしてみましょう。
kubectl run -it --rm curlpod --image=radial/busyboxplus:curl --restart=Never -- sh
shellに入れたら、ヘルスチェック用エンドポイントを叩いてみましょう。
[ root@curlpod:/ ]$ curl -v app-http-server-service.app-development.svc.cluster.local/healthz
以下のように200が返ります。
> GET /healthz HTTP/1.1
> User-Agent: curl/7.35.0
> Host: app-http-server-service.app-development.svc.cluster.local
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: nginx/1.25.5
< Date: Fri, 03 Apr 2026 18:15:46 GMT
< Content-Length: 0
< Connection: keep-alive
ここで、一時的にたてたPodのresolv.confを覗いてみましょう。
resolv.confは、Linuxサーバーにおける名前解決に関する設定が記載されているファイルになります。
OSには「リゾルバ」というプログラムが入っています。このリゾルバはDNSクライアントに相当するものであり、Linux系OSの場合は/etc/resolv.confを参照してDNSサーバーにipアドレスを問い合わせます。
nameserverに注目してみましょう。これは、まさにcoredns serviceのipアドレスを指します。
[ root@curlpod:/ ]$ cat /etc/resolv.conf
# ドメイン名の補完
search default.svc.cluster.local svc.cluster.local cluster.local
# DNSサーバのIPアドレス => corednsのserviceのClusterIP
nameserver 10.96.0.10
# 名前解決の細かい挙動設定 => ドットが5個未満の名前はsearchドメインを補完して解決
options ndots:5
kube-dnsというServiceリソースを覗いてみましょう。10.96.0.10のClusterIPが割り当てられていますね。また、labelにはkubernetes.io/name=CoreDNSとあります。
% kubectl describe svc kube-dns -n kube-system
Name: kube-dns
Namespace: kube-system
Labels: k8s-app=kube-dns
kubernetes.io/cluster-service=true
kubernetes.io/name=CoreDNS
Annotations: prometheus.io/port: 9153
prometheus.io/scrape: true
Selector: k8s-app=kube-dns
Type: ClusterIP
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.96.0.10
IPs: 10.96.0.10
Port: dns 53/UDP
TargetPort: 53/UDP
Endpoints: 10.244.0.4:53,10.244.0.2:53
Port: dns-tcp 53/TCP
TargetPort: 53/TCP
Endpoints: 10.244.0.4:53,10.244.0.2:53
Port: metrics 9153/TCP
TargetPort: 9153/TCP
Endpoints: 10.244.0.4:9153,10.244.0.2:9153
Session Affinity: None
Internal Traffic Policy: Cluster
Events: <none>
coredns podのIPアドレスと、EndpointsリソースのIPアドレスの1つと一致します。
% kubectl get pod coredns-76f75df574-2cz96 -n kube-system -o custom-columns=NAME:.metadata.name,IP:.status.podIP,NODE:.spec.nodeName --no-headers
10.244.0.4
つまり、curl用Pod内でcurl -v app-http-server-service.app-development.svc.cluster.local/healthzを叩いた時、「一度corednsに問い合わせてipアドレスを取得する」という工程が挟まっていたわけですね。

kindnetって何
主要なコンポーネントとして取り上げなかったものについて説明します。と言っても、kindnet pod自体は、名前から察せられるようにKind Cluster特有のものになります。
kindnetはCNI(Container Network Interface)プラグインの一種です。
CNIとは、Podやコンテナのネットワークを管理・接続するための標準仕様(インターフェース)です。
k8sのネットワークといえば、前章でkube-proxyについて取り上げました。
- kube-proxyがService/EndpointSliceリソースをもとにiptablesを使い、Node上にネットワークルールを構築している
- Linuxカーネルがこれらのルールを適用することで、Service宛のリクエストがPodのIPアドレスに変換される
対して、CNIプラグインはPodネットワークの構築の役割を果たしているといえます。
「Podネットワークの構築」の意味するところについて、少しだけ解像度を上げていきましょう。
CNIの文脈において、container = network namespace(netns) になります。
netnsはLinuxのネットワーク機能です。 1台のマシン上に仮想ホストを作成でき、その仮想ホストにはIPアドレスやNICを持たせることもできます。
インターフェースとしては以下の二つが主に定められています。
- ADD :
- netnsにinterfaceを作成(→別netnsと通信する口を作る)
- netnsにipアドレスを割り当て
- ルーティング設定
- DEL :
- interfaceを削除(→通信の口を閉じる)
- IPアドレスを解放
- ルーティングを削除
ここで理解しておきたいのは、Pod単位でnetnsが作られており、コンテナはそのnetnsを共有していることです。 http-server podのnetnsをコンテナが共有していることを以下で確認します。
# Podのnetns idを確認
% kubectl exec app-http-server-5449665dbb-7mkjc -n app-development -- readlink /proc/1/ns/net
Defaulted container "http-server" out of: http-server, nginx
net:[4026534098]
# http-serverコンテナのnetns idを確認
% kubectl exec app-http-server-5449665dbb-7mkjc -c http-server -n app-development -- readlink /proc/1/ns/net
net:[4026534098]
# nginx(サイドカー)コンテナのnetns idを確認
% kubectl exec app-http-server-5449665dbb-7mkjc -c nginx -n app-development -- readlink /proc/1/ns/net
net:[4026534098]
CNIプラグインはnetns(≒ Pod)にIPアドレスを付与し、netns外との通信ができるように仮想NIC(Network Interface Card)であるveth(Virtual Ethernet Device)を配置します。
これではまだPod同士は独立したネットワークです。では、どうやってPod同士、ひいてはWorker Node(ホストマシン)外と通信できているのでしょうか。
ここからはLinuxのお話にもなってきますが、vethはペアで成り立ちます。
以下のように、vethがペアで接続されることで、Podの独立したnetnsがホスト(root netns)と通信可能になります。これにより、PodネットワークがWorker Nodeのルーティングに組み込まれます。
CNIはインターフェースであり、実装方法は様々です。
例えば、今回のKindの場合、Worker NodeはDockerコンテナです。 Worker Nodeコンテナのeth0も実際はvethであり、ホストマシンのvethと接続されます。そのvethがブリッジ(L2スイッチ)に接続され、ネットワークを実現しているようです。参考:https://note.com/minato_kame/n/n81b1a4396807
まとめると、
- CNIプラグインがPodという1つのnetnsにIPアドレスを割り当て、vethでWorker Nodeのroot netnsと接続し、ルーティングに組み込むことで、「Pod間ネットワークの構築」が実現されている
といえそうです。
さらに、kube-proxy, Serviceを含めてk8s ネットワークを改めて整理すると、
- kube-proxyがService/EndpointSliceリソースをもとにiptablesを使い、Node上にネットワークルールを構築している
- Linuxカーネルがこれらのルールを適用することで、Service宛のリクエストがPodのIPアドレスに変換される
- CNIプラグインがPodのIPアドレスを管理し、Pod間およびPodと外部のネットワークを接続することで、通信が実現される
k8sのネットワーク実現方法の解像度が上がってきましたね。
kubeletはPodではない?
Worker Nodeで主要なコンポーネントとしてkubeletの説明をしました。 kubeletは、Controle Planeのkube-api-serverから指示を受け取ってPodのライフサイクルを制御するのでした。
しかし、クラスター内のPod一覧を覗いても、kubelet podは特に見当たりませんでしたね。つまり、kubeletはPodではなく、Node上で動くデーモンプロセスであるということです。 Nodeの要件は以下のように定められているようです。
ノード適合性テストを実行するには、ノードは標準的なKubernetesノードと同じ前提条件を満たす必要があります。最低限、ノードには以下のデーモンがインストールされている必要があります。 ・Docker、containerd、CRI-OなどのCRI互換コンテナランタイム ・kubelet
試しに、起動中のWorker Node(KindなのでDockerコンテナですが)に入って見てみましょう。docker exec -it [ps id] bashとかで入ってください。
root@sandbox-worker:/# which kubelet
/usr/bin/kubelet
確かにkubeletがインストールされていますね。
また、kubeletプロセスが起動していることも確認できました。
root@sandbox-worker:/# ps -eo pid,comm,cmd,%cpu,%mem | grep kubelet
176 kubelet /usr/bin/kubelet --bootstra 1.3 1.2
kubeletがちゃんと動いていることをさらに確かめてみましょう。以下でkubeletのlogがリアルタイムで出力されます。
root@sandbox-worker:/# journalctl -u kubelet -f
試しに、別のターミナルでkubectlでPodを削除してみてください。
kubectl delete pod app-http-server-5449665dbb-7mkjc -n app-development
すると、ログに以下のようなPod削除関連のログが出ていることが確認できるはずです。以下で抜粋&見やすい形で示します。
# コンテナの状態削除
RemoveStaleState: removing container nginx
RemoveStaleState: removing container http-server
RemoveStaleState removing state
# ボリュームのアンマウント
UnmountVolume started for volume "kube-api-access..."
UnmountVolume started for volume "nginx-conf"
UnmountVolume.TearDown succeeded
Volume detached
# コンテナ削除
RemoveContainer containerID=...
kubectlはkube-api-serverへリクエストするコマンドです。kubectl delete podコマンドを叩いた時、kubeletはapi-serverから削除イベントを受け取っていて、実際にコンテナの削除を行なっていたということでしょう。
k8sを実際に動かして知る、というだけならKindクラスターでも十分ということがわかりますね。
次章では、EKSクラスター環境を用意すべく、terraformを記述していきます。