はじめに

3章で完成したmanifestをローカル環境で動作する軽量なk8sクラスタであるkind(Kubernetes in Docker)にデプロイしてみます。

kindを使えばお金もかからず、簡易的にローカルマシンでk8sの動作を確認できるため、まずはEKSにデプロイする前にお手軽ローカルで試してみましょうと言う章です。

ハンズオンのゴールからは若干それますが、構成図のEKS Clusterの部分をkindに置き換えて試してみると言う感じです。

雑ですが、構成図を改変して以下のようにあらわしてました(強引)。

Kindとは

Kindは、DockerコンテナをNodeとして転用し、クラスターとして構成してくれる開発用ツールです。 Kubernetes In DockerでKindのようです。

https://kind.sigs.k8s.io/

Kindに関しては深入りせず、早速デプロイしてk8sを動かしてみましょう。

Kindクラスターにデプロイする

以下の手順でデプロイします。

  • Kindクラスター用のconfigファイルを用意
  • Kindクラスターを起動
  • manifestを適用

以下に従ってKindをインストールしておいてください。

https://kind.sigs.k8s.io/docs/user/quick-start/#installation

Kind用のconfigファイルを用意

Worker Nodeをマルチノードとしたり、Kindクラスターの設定をconfigファイルで管理できます。

https://kind.sigs.k8s.io/docs/user/quick-start/#configuring-your-kind-cluster

以下のようにファイルを記述します。

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
https://kind.sigs.k8s.io/docs/user/quick-start/#mapping-ports-to-the-host-machine

これで、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と捉えて良いかと思います。

https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/#:~:text=kube%2D-,system,-The%20namespace%20for

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では、デプロイするコンテナイメージを起動したクラスタにダウンロードしておく必要があります。

https://kind.sigs.k8s.io/docs/user/quick-start/#loading-an-image-into-your-cluster

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).

https://kubernetes.io/docs/reference/networking/virtual-ips/#kube-proxy

つまり、Control PlaneもNodeであるということでしょう。以下の一文のmachineは、nodeと置き換えて捉えても問題ないと思います。

Control plane components can be run on any machine in the cluster.

https://v1-33.docs.kubernetes.io/docs/concepts/architecture/?utm_source=chatgpt.com#control-plane-components

また、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
https://hub.docker.com/r/radial/busyboxplus

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)プラグインの一種です。

https://kindnet.es/docs/design/cni/

CNIとは、Podやコンテナのネットワークを管理・接続するための標準仕様(インターフェース)です。

https://kubernetes.io/ja/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/

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はペアで成り立ちます。

https://note.com/minato_kame/n/n7de03017804d

以下のように、vethがペアで接続されることで、Podの独立したnetnsがホスト(root netns)と通信可能になります。これにより、PodネットワークがWorker Nodeのルーティングに組み込まれます。

CNIはインターフェースであり、実装方法は様々です。

https://www.tkng.io/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

https://v1-33.docs.kubernetes.io/docs/setup/best-practices/node-conformance/?utm_source=chatgpt.com#node-prerequisite

試しに、起動中の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を記述していきます。