なんちゃっておうちKubernetes:kubeadm編
こんにちは、AI ShiftでAI Workerのバックエンド開発をしています、中野(@247Subaru)です。
この記事はAI Shift Advent Calendar 2025の4日目の記事です。
前回は「なんちゃっておうちKubernetes」として物理インフラを整える話をしました。今回はその続きで、kubeadmを使ってKubernetes環境を構築していきます。kubeadmでクラスタを立ち上げ、APIサーバーとDBサーバーをデプロイした実践記録をまとめます。
本家と違い、1ノードなので「なんちゃって」とつけています。
今回やりたいこと
本記事では、以下を実現します:
- kubeadm initでControl Planeを初期化
- Cilium CNIでPod間ネットワークを構築
- PostgreSQL StatefulSetとHono API Deploymentのデプロイ
- Todo APIの動作確認(CRUD操作)
- kubeadmを通してネットワーク構築の一端を理解
モチベーション
なぜkubeadmを選んだのか
Kubernetes公式ドキュメントにはこう書かれています:
Using kubeadm, you can create a minimum viable Kubernetes cluster that conforms to best practices. In fact, you can use kubeadm to set up a cluster that will pass the Kubernetes Conformance tests. kubeadm also supports other cluster lifecycle functions, such as bootstrap tokens and cluster upgrades.[1]
つまり、kubeadmは本番環境に準拠したベストプラクティスのクラスタを構築できるツールです。1ノード構成であっても、本格的なKubernetesの仕組みを体験できる点が魅力だったので採用しました。
学習目的
- Kubernetesのネットワーク構築の一端を理解したい
- Control Planeコンポーネント(etcd、API Server、Schedulerなど)がどう連携するかを知りたい
- Kubernetesのエコシステムを体感したい
シングルノード構成で進めます
一旦ノードを増やす予定はないので、Control PlaneとWorkerを兼用したシングルノード構成でセットアップを進めます。
実行環境
リモートサーバー
- OS: Ubuntu 22.04.5 LTS (jammy)
- カーネル: Linux 5.15.0-157-generic
- CPU: Intel(R) Core(TM) i5-8400T @ 1.70GHz (6コア)
- メモリ: 16GB (15Gi)
- ディスク: 98GB (使用: 19GB、空き: 75GB)
- 構成: シングルノード(Control Plane + Worker兼用)
事前準備: HonoサーバーとDockerイメージ
本セットアップを始める前に、以下を準備済みです:
- Hono製APIサーバー: TypeScriptでTodo APIを実装
- Dockerfile: マルチステージビルドで最適化
- マルチアーキテクチャイメージビルド: linux/amd64とlinux/arm64の両対応
- Docker Hubプライベートリポジトリ: イメージをプッシュ済み
詳しいコードは以下リポジトリに載せてありますので、興味のある方は覗いてみてください。
Docker ImageはPrivate Repositoryにpushしているので、Kubernetes公式ドキュメント「Using a Private Registry」[2]に従って、ImagePullSecretを設定する予定です:
kubectl create secret docker-registry dockerhub-secret \
--docker-server=docker.io \
--docker-username=<your-username> \
--docker-password=<your-password> \
-n app
全体図
Kubernetesクラスタのコンポーネント

Kubernetesクラスタは、以下のコンポーネントで構成されます:
Control Planeコンポーネント:
- API Server: クラスタのエントリーポイント(いわばAPIゲートウェイ)
- etcd: クラスタの状態を保存(RedisやDBのような設定ストア)
- Scheduler: PodをどのNodeで実行するか決定
- Controller Manager: クラスタの状態を管理
Nodeコンポーネント:
- kubelet: Podの起動・管理を担当
- kube-proxy: Serviceへのトラフィックをルーティング(内部ロードバランサー)
今回構築するアーキテクチャ
シングルノード構成なので、Control PlaneとWorkerが同じマシン上で動作します。
実装してみる
ここからは、実際の構築手順を実行ログ付きで紹介します。
セッション1: 事前準備
# swap無効化(試行錯誤あり)
$ sudo swapoff -a
# 永続化設定(試行1,2 - 失敗)
$ sudo sed -i '/ swap /s^/#/' /etc/fstab
sed: -e expression #1, char 13: unterminated `s' command
# 永続化設定(試行3 - 成功)
$ sudo sed -i '/ swap / s/^/#/' /etc/fstab
# 確認
$ free -h | grep -i swap
Swap: 0B 0B 0B
# カーネルモジュール設定
$ cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
$ sudo modprobe overlay
$ sudo modprobe br_netfilter
# 確認
$ lsmod | grep -E 'overlay|br_netfilter'
overlay 151552 0
br_netfilter 32768 0
bridge 311296 1 br_netfilter
# sysctlパラメータ設定
$ cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
EOF
$ sudo sysctl --system
* Applying /etc/sysctl.d/k8s.conf ...
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
気づき: sedコマンドのセパレーター記号を間違えると構文エラーになります。正しくはs/^/#/です。
セッション2: containerdをインストール
# Docker公式リポジトリの追加
$ sudo mkdir -p /etc/apt/keyrings
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
$ echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# containerdのインストール
$ sudo apt-get update
$ sudo apt-get install -y containerd.io
# バージョン確認
$ containerd --version
containerd containerd.io v2.2.0 1c4457e00facac03ce1d75f7b6777a7a851e5c41
# デフォルト設定を生成
$ sudo mkdir -p /etc/containerd
$ containerd config default | sudo tee /etc/containerd/config.toml
# SystemdCgroupをtrueに変更(重要!)
$ sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
# containerdを再起動
$ sudo systemctl restart containerd
$ sudo systemctl enable containerd
# 動作確認
$ sudo systemctl status containerd
● containerd.service - containerd container runtime
Active: active (running)
重要: SystemdCgroup = true の設定を忘れると、kubeadm initで問題が発生する可能性があります。
セッション3: kubeadmをインストール
# Kubernetes公式リポジトリの追加
$ curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.31/deb/Release.key | \
sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
$ echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.31/deb/ /' | \
sudo tee /etc/apt/sources.list.d/kubernetes.list
# kubelet、kubeadm、kubectlのインストール
$ sudo apt-get update
$ sudo apt-get install -y kubelet kubeadm kubectl
# パッケージのバージョン固定
$ sudo apt-mark hold kubelet kubeadm kubectl
kubelet set on hold.
kubeadm set on hold.
kubectl set on hold.
# バージョン確認
$ kubeadm version
kubeadm version: v1.31.14
$ kubelet --version
Kubernetes v1.31.14
$ kubectl version --client
Client Version: v1.31.14
インストールされたパッケージ:
- kubelet: Kubernetesノードエージェント
- kubeadm: クラスタセットアップツール
- kubectl: クライアントコマンド
- cri-tools: コンテナランタイムインターフェースツール
- kubernetes-cni: CNIプラグイン
セッション4: Control Plane初期化とTLS証明書エラー対応
# Control Planeの初期化
$ sudo kubeadm init --pod-network-cidr=10.244.0.0/16
[init] Using Kubernetes version: v1.31.14
[certs] Generating "ca" certificate and key
[certs] Generating "apiserver" certificate and key
[certs] apiserver serving cert is signed for DNS names [kubernetes kubernetes.default kubernetes.default.svc kubernetes.default.svc.cluster.local octom-server] and IPs [10.96.0.1 192.168.0.50]
...
Your Kubernetes control-plane has initialized successfully!
# kubeconfigの設定
$ mkdir -p $HOME/.kube
$ sudo cp /etc/kubernetes/admin.conf $HOME/.kube/config
$ sudo chown $(id -u):$(id -g) $HOME/.kube/config
# 確認(エラー発生!)
$ kubectl get nodes
Unable to connect to the server: tls: failed to verify certificate: x509: certificate is valid for 10.96.0.1, 192.168.0.50, not 127.0.0.1
# 原因: kubeconfigのserverが127.0.0.1だが、証明書には含まれていない
$ grep server: $HOME/.kube/config
server: https://127.0.0.1:6443
# 解決: serverをノードIPに変更
$ rm -f $HOME/.kube/config
$ sudo cp /etc/kubernetes/admin.conf $HOME/.kube/config
$ sudo chown $(id -u):$(id -g) $HOME/.kube/config
$ sudo chmod 600 $HOME/.kube/config
$ sed -i 's|https://127.0.0.1:6443|https://192.168.0.50:6443|g' $HOME/.kube/config
# 成功!
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
octom-server Ready control-plane 7m34s v1.31.14
# taintの削除(シングルノード構成のため)
$ kubectl taint nodes --all node-role.kubernetes.io/control-plane-
node/octom-server untainted
トラブルシューティング: TLS証明書エラーは、kubeconfigを再コピーしてserverのみを修正することで解決しました。
セッション5: CNIを導入
# Cilium CLIのダウンロード
$ CILIUM_CLI_VERSION=$(curl -s https://raw.githubusercontent.com/cilium/cilium-cli/main/stable.txt)
$ CLI_ARCH=amd64
$ curl -L --fail --remote-name-all https://github.com/cilium/cilium-cli/releases/download/${CILIUM_CLI_VERSION}/cilium-linux-${CLI_ARCH}.tar.gz{,.sha256sum}
$ sha256sum --check cilium-linux-${CLI_ARCH}.tar.gz.sha256sum
cilium-linux-amd64.tar.gz: OK
$ sudo tar xzvfC cilium-linux-${CLI_ARCH}.tar.gz /usr/local/bin
$ rm cilium-linux-${CLI_ARCH}.tar.gz{,.sha256sum}
# バージョン確認
$ cilium version --client
cilium-cli: v0.18.8
# Ciliumのインストール
$ cilium install --version 1.16.5
Using Cilium version 1.16.5
Auto-detected cluster name: kubernetes
Auto-detected kube-proxy has been installed
# ステータス確認
$ cilium status --wait
/¯¯\
/¯¯\__/¯¯\ Cilium: OK
\__/¯¯\__/ Operator: OK
/¯¯\__/¯¯\ Envoy DaemonSet: OK
\__/¯¯\__/ Hubble Relay: disabled
\__/ ClusterMesh: disabled
DaemonSet cilium Desired: 1, Ready: 1/1, Available: 1/1
DaemonSet cilium-envoy Desired: 1, Ready: 1/1, Available: 1/1
Deployment cilium-operator Desired: 1, Ready: 1/1, Available: 1/1
セッション6: Local Path Provisionerの導入
# Local Path Provisionerのデプロイ
$ kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.30/deploy/local-path-storage.yaml
namespace/local-path-storage created
deployment.apps/local-path-provisioner created
storageclass.storage.k8s.io/local-path created
# Podの起動確認
$ kubectl get pods -n local-path-storage
NAME READY STATUS RESTARTS AGE
local-path-provisioner-dbff48958-sr4nx 1/1 Running 0 14s
# StorageClassの確認
$ kubectl get storageclass
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
local-path rancher.io/local-path Delete WaitForFirstConsumer false 20s
セッション7: PostgreSQLとHono APIのデプロイ
# appネームスペースの作成
$ kubectl create namespace app
namespace/app created
# Secretの作成
$ kubectl create secret generic postgres-secret \
--from-env-file=deployment/environments/prod/.env.secret \
-n app
secret/postgres-secret created
# PostgreSQLのデプロイ
$ helm upgrade --install postgres ./deployment/charts/postgres \
-n app \
-f ./deployment/environments/prod/postgres-values.yaml
NAME: postgres
NAMESPACE: app
STATUS: deployed
# Hono APIのデプロイ(最初はImagePullBackOffエラーが発生した)
$ kubectl get pods -n app
NAME READY STATUS RESTARTS AGE
api-xxx-abc 0/1 ImagePullBackOff 0 2m
postgres-0 1/1 Running 0 5m
# エラー詳細
$ kubectl describe pod -n app api-xxx-abc
Events:
Warning Failed Failed to pull image: no match for platform in manifest
# 原因: ローカル(arm64)とリモート(x86_64)のアーキテクチャ不一致
# 解決: マルチアーキテクチャイメージに修正後、Podを削除
$ kubectl delete pods -n app -l app=api
$ kubectl get pods -n app -w
api-xxx-def 0/1 ContainerCreating 0 6s
postgres-0 1/1 Running 0 15m
api-xxx-def 1/1 Running 0 25s
セッション8: アプリケーション動作確認
# curl-testコンテナの起動
$ kubectl run curl-test --image=curlimages/curl:latest --rm -it --restart=Never -- sh
# ヘルスチェック
~ $ curl -s http://api.app.svc.cluster.local:3000/healthz
{"status":"healthy"}
# Todo一覧の取得(空)
~ $ curl -s http://api.app.svc.cluster.local:3000/api/todos
[]
# Todoの作成
~ $ curl -s -X POST http://api.app.svc.cluster.local:3000/api/todos \
-H "Content-Type: application/json" \
-d '{"title":"Test from kubeadm","description":"Testing PostgreSQL connection","completed":false}'
{"title":"Test from kubeadm","description":"Testing PostgreSQL connection","completed":false,"id":1,"createdAt":"2025-11-29T22:51:24.349Z","updatedAt":"2025-11-29T22:51:24.349Z"}
# Todo一覧の取得(作成したTodoが表示される)
~ $ curl -s http://api.app.svc.cluster.local:3000/api/todos
[{"id":1,"title":"Test from kubeadm","description":"Testing PostgreSQL connection","completed":false,"createdAt":"2025-11-29T22:51:24.349Z","updatedAt":"2025-11-29T22:51:24.349Z"}]
出来上がったもの
すべてのセットアップが完了し、以下の状態になりました:
# ノードの確認
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
octom-server Ready control-plane 45m v1.31.14
# アプリケーションの確認
$ kubectl get all -n app
NAME READY STATUS RESTARTS AGE
pod/api-xxx-abc 1/1 Running 0 10m
pod/api-xxx-def 1/1 Running 0 10m
pod/postgres-0 1/1 Running 0 25m
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/api ClusterIP 10.96.100.50 <none> 3000/TCP 10m
service/postgres ClusterIP 10.96.100.51 <none> 5432/TCP 25m
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/api 2/2 2 2 10m
NAME DESIRED CURRENT AGE
statefulset.apps/postgres 1 1 25m
完成したアーキテクチャ(再掲):
kubeadmで個人でセットアップする内容
注: 実際の実行手順は前述の「実装してみる」セクションを参照してください。ここでは、各手順の理論的背景と「なぜ必要なのか」を解説します。
一旦ノードを増やす予定はないので、シングルノード構成(Control Plane + Worker兼用)でセットアップを進めています。
1. システム事前準備
なぜswap無効化が必要?
Kubernetes公式ドキュメント[3]によると、Kubernetesがメモリを適切に管理するため、swapを無効化する必要があります。swapが有効だと、Podのメモリ制限が正しく機能しません。
なぜカーネルモジュールが必要?
overlayとbr_netfilterモジュールは、コンテナネットワーキングに必須です。overlayはコンテナのファイルシステム、br_netfilterはブリッジネットワークのパケットフィルタリングを提供します。
設定内容:
- swapの無効化(
swapoff -a、/etc/fstabの編集) - カーネルモジュールのロード(overlay、br_netfilter)
- sysctlパラメータ設定(iptablesとIP forwarding)
2. containerdのインストール
なぜcontainerdが必要?
Kubernetesはコンテナランタイム[4]を必要とします。Docker Engine、containerd、CRI-Oなどが選択肢ですが、containerdが最も軽量で推奨されています。
なぜSystemdCgroup = true?
containerdとKubernetes(kubelet)のCgroupドライバーを統一する必要があります。不一致があると、Podの起動やリソース管理で問題が発生します。
3. kubeadmのインストール
kubeadmとは?
kubeadm[5]は、Kubernetesクラスタを構築するための公式ツールです[1:1]。
なぜapt-mark hold?
apt-mark holdでパッケージをバージョン固定します。Kubernetesのアップグレードは計画的に行う必要があり、意図しない自動更新を防ぎます。
4. Control Planeの初期化
kubeadm initで何が起こる?
- 証明書の生成(CA、API Server、etcdなど)
- Control Planeコンポーネントの起動(Static Podとして)
- CoreDNS(8.8.8.8のようなクラスタ内DNS)とkube-proxyの自動デプロイ
なぜtaint削除?
シングルノード構成では、Control PlaneでもアプリケーションPodをスケジュールする必要があります。デフォルトではControl PlaneにNoSchedule taintが設定されているため、削除します。
5. Cilium CNIの導入
なぜCNIが必要?
CNI[6](Container Network Interface)がないと、Pod間通信ができません。CNIがインストールされるまで、ノードはNotReady状態のままです。
Ciliumとは?
Cilium[7]は、eBPFベースの高性能なCNIプラグインです。いわば「仮想ネットワークスイッチとファイアウォール」のような役割を果たします。
6. Local Path Provisionerの導入
なぜ必要?
StatefulSetなどで永続ボリュームが必要な場合、PersistentVolumeを動的に作成するプロビジョナーが必要です。Local Path Provisionerは、ローカルディスク上にボリュームを作成します。
7. PostgreSQLのデプロイ
なぜStatefulSet?
データベースのような状態を持つアプリケーションは、StatefulSetを使用します。Podに永続的な識別子(postgres-0)が付き、再起動してもデータが保持されます。
8. Hono APIのデプロイ
なぜDeployment?
APIサーバーはステートレスなので、Deploymentを使用します。複数レプリカを起動でき、ローリングアップデートも可能です。
なぜService?
Podは再起動時にIPアドレスが変わるため、ServiceでClusterIP(仮想IP)を提供します。これは「内部ロードバランサー」のような役割です。
kubeadm init時に起きていること
kubeadm initを実行すると、以下のプロセスが自動的に実行されます:
Static Podとは?
/etc/kubernetes/manifestsに配置されたマニフェストファイルを、kubeletが自動的にPodとして起動します。API Serverなどのコアコンポーネントは、この仕組みで起動されます。
立てたらできること
kubeadmでKubernetesクラスタを構築すると、以下ができるようになります:
- Kubernetesオペレーションの学習: 本番環境と同じ構成で実験できる
- ストレージの利用: PVC/PVを使った動的プロビジョニング
- ネットワーキングの理解: Service、DNS、CNIの仕組みを体感
- 学習と実験環境: 失敗しても何度でもやり直せる
リクエストフローの詳細解説
Todo作成リクエストがどのように処理されるか、2つのネットワークフローで説明します。
kubeadm内部ネットワークフロー(クラスタ内部)
Pod → Service → kube-proxy → CNI → Pod
クラスタ内部のPod-to-Pod通信は、すべて自動で行われます:
-
Client PodがServiceにアクセス - Service名(例:
api.app.svc.cluster.local)をCoreDNS(8.8.8.8のようなDNS)で解決 - kube-proxyがルーティング - iptablesルールでServiceのClusterIP(仮想IP)を実際のPod IPに変換(内部ロードバランサー)
- CNIがPod間通信を提供 - CiliumがPodネットワークを構築(仮想ネットワークスイッチ)
- 宛先Podに到達 - リクエストが処理される
全体ネットワークフロー(外部 → Kubernetes → Pod)
curl → Node NIC → Ingress → Service → kube-proxy → Pod → PostgreSQL
外部からのリクエストがKubernetesクラスタを通ってPostgreSQLまで到達する流れ:
- 外部リクエスト - curl や Webブラウザからリクエスト
- Node NIC - Ubuntuサーバーのネットワークインターフェースで受信
- Ingress - HTTPパスで振り分け(L7ロードバランサー、Ingressが分岐の司令塔)
- Service - ClusterIPで内部ルーティング
- kube-proxy - iptablesでPod IPに変換
- API Pod - Honoアプリケーションが処理
- PostgreSQL Pod - データベース操作を実行
ユースケース: Todo作成リクエスト
curl -X POST http://api.app.svc.cluster.local:3000/api/todos \
-H "Content-Type: application/json" \
-d '{"title":"Test from kubeadm","completed":false}'
まとめ
達成したこと
- kubeadmでControl Plane初期化完了
- Cilium CNIでPod間ネットワーク構築
- PostgreSQL StatefulSetとHono API Deploymentのデプロイ
- Todo APIの動作確認(CRUD操作成功)
- kubeadmを通してネットワーク構築の一端を理解
- Kubernetesエコシステムの深い理解
学んだこと
- kubeadmの初期化プロセス: 証明書生成、Static Pod、Control Planeコンポーネントの起動フロー
- TLS証明書管理: kubeconfigのserver設定と証明書の関係
- マルチアーキテクチャイメージビルド: docker buildxの重要性
- Service Discoveryの仕組み: CoreDNS、kube-proxy、CNIの連携
- StatefulSetとDeploymentの使い分け: ステートフルとステートレスの違い
次のステップ
今回はクラスタ内部でAPIが動作するところまで確認しました。次は以下に挑戦したいです:
- Cloudflare Tunnelの導入: クラスタIPを登録してAPIを外部公開
- フロントエンドアプリの接続: Reactアプリなどから公開されたAPIにリクエストを投げられるようにする
- オブザーバビリティの向上: WiZやOpenTelemetry(OTel)を導入して、ログ・メトリクス・トレースを可視化
Discussion