新卒インフラエンジニア向け「Kubernetes 実践教科書 ② CKA クラスタ構築・運用編」(全16回)の第2回です。第1回では第2巻の地図を広げ、kubeadm という道具の位置づけと 9 VM の舞台を確認しました。今回からいよいよ手を動かします。まず 1 台(k8s-cp-01)に kubeadm でシングルノードのクラスタを立ち上げ、CNI(Pod 間通信を担うネットワークプラグイン)として Calico を適用し、ノードが Ready になるところまでを通します。これが本番クラスタ構築の第一歩であり、第3回の HA 設計、第4回の 5 ノード HA 化へと積み上げていく土台になります。
本回の操作の中心は k8s-cp-01 ですが、alma-proxy と作業端末 k8s-ops でも一部作業します。本回で使う 3 台は、いずれも第1回で紹介した 9 VM 構成の一部です。
- k8s-cp-01(
192.168.1.125)― 1 台目の Control Plane Node。これから kubeadm でクラスタを立てる本体で、下ごしらえやkubeadm initはここで行います。 - alma-proxy(
192.168.1.121)― DNS・NTP・Squid プロキシ。whitelist と名前解決(dnsmasq)の設定を行います。 - k8s-ops(
192.168.1.122)― 第1巻から使っている作業端末。kubectl はここから実行します。
操作の約束事は次のとおりです。
- プロンプトの
#は root、$は一般ユーザーdeveloperを表します。 - 各 VM へは、あなたの手元のマシン(SSH クライアント)から developer で SSH ログインし、root が必要な作業は
sudo -iで root シェルに切り替えます。 - 各コマンドの前に実行先のホスト名を明記します。どの VM で実行するかを取り違えないよう進めてください。
目次
今ここマップ(全 16 回中の現在地)
本シリーズは 6 部構成です。現在地は第1部「クラスタ構築」の第2回です。
- 第1部 クラスタ構築(第1〜5回)← 今ここ
- 第2部 ワークロード管理(第6〜8回)
- 第3部 ネットワーク(第9〜11回)
- 第4部 ストレージ(第12回)
- 第5部 監視・運用(第13〜14回)
- 第6部 トラブルシュート(第15〜16回)
- AlmaLinux 10.2(kernel 6.12.0-211.22.1.el10_2)
- kubeadm・kubelet・kubectl v1.35.6(pkgs.k8s.io・v1.35 系にピン留め)
- containerd.io v2.2.6
- Calico v3.32.0
- Pod CIDR 10.244.0.0/16
- 確認日 2026-07-25
この回のゴール
本回を終えると、次のことができるようになります。到達できたかは記事末の「やってみよう」と「理解度チェック」で確認します。
- alma-proxy の whitelist に、Kubernetes 関連ドメインとそのリダイレクト先 CDN を揃えられる
- kubeadm の前提(swap 無効・カーネルモジュール・sysctl)を整え、containerd と kubeadm・kubelet・kubectl を v1.35 で導入できる
kubeadm initで単一ノードのコントロールプレーンを起動し、admin.conf を作業端末 k8s-ops に配って kubectl を通せる- Calico v3.32.0 を適用してノードを Ready にできる
今回作るもの(全体像)
作業は一本道です。ノードの下ごしらえ(swap・カーネルモジュール・sysctl・firewalld)を整え、コンテナランタイム containerd を入れ、kubeadm・kubelet・kubectl を導入します。そのうえで kubeadm init でコントロールプレーンを起動し、管理用 kubeconfig(admin.conf)を k8s-ops に配り、最後に Calico を適用してノードを Ready にします。この順序を頭に入れておくと、各手順が全体のどこに位置するのかが見えてきます。

もう 1 点、先に触れておきます。今回作るのは 1 台構成ですが、これは第4回で HA クラスタへ育てる最初の種です。kubeadm init のコントロールプレーンエンドポイントには k8s-lb:6443 という名前を最初から指定します。今の段階では k8s-lb という名前を暫定的に k8s-cp-01 自身(192.168.1.125)へ向けておき、第3回で HAProxy を実際の k8s-lb VM(192.168.1.124)に構築したら、この名前の指す先だけを差し替えます。エンドポイントの名前が変わらないため、クラスタを作り直す必要はありません。この「名前で間接的に指す」設計のおかげで、後の HA 化でクラスタを作り直さずに済みます。
alma-proxy の whitelist を確認する
本シリーズの検証環境は、企業ネットワークを再現するために alma-proxy(192.168.1.121)の Squid を whitelist 方式で経由します。許可されたドメインだけに外部通信できる構成です。第2巻では kubeadm のパッケージ配布元(pkgs.k8s.io)や Calico のマニフェスト配布元などが必要になるため、これらが whitelist に含まれているかをまず確認します。
実行コマンド(alma-proxy・root):
# grep -E 'pkgs.k8s.io|raw.githubusercontent|registry.k8s.io|dl.k8s.io' /etc/squid/whitelist.txt
実行結果:
raw.githubusercontent.com
dl.k8s.io
registry.k8s.io
第1巻までで必要だった 3 つは登録済みですが、pkgs.k8s.io がありません。第1巻では kubectl のバイナリを dl.k8s.io から直接取得していたため不要でしたが、本巻では kubeadm・kubelet・kubectl を RPM パッケージとして入れるため、公式リポジトリのドメインが必要になります。追記して反映します。
実行コマンド(alma-proxy・root):
# echo 'pkgs.k8s.io' >> /etc/squid/whitelist.txt
# systemctl reload squid
次に、k8s-cp-01 から実際にプロキシ経由で pkgs.k8s.io へ到達できるかを確認します。
実行コマンド(k8s-cp-01・root):
# curl -x http://192.168.1.121:3128 -s -o /dev/null -w 'HTTP %{http_code}\n' https://pkgs.k8s.io/core:/stable:/v1.35/rpm/repodata/repomd.xml
実行結果:
HTTP 302
302 はリダイレクト応答で、pkgs.k8s.io がパッケージの実体を別のホスト(CDN)へ転送していることを示します。プロキシ経由でここまで到達できていますが、注意すべき点があります。whitelist 方式のプロキシでは、リダイレクト先の CDN ホストも個別に許可しないと、実際のダウンロード段階で 403 で弾かれます。302 が返っても安心せず、転送先まで許可する必要があります。
リダイレクト先の CDN を許可する
第2巻の構築で pkgs.k8s.io と registry.k8s.io が転送する先は、次の CDN 群です。これらを whitelist に加えないと、パッケージ導入(dnf install)やコンテナイメージの取得(kubeadm init のイメージ pull)が途中で失敗します。
.packages.k8s.io:pkgs.k8s.io の RPM 配布 CDN(prod-cdn.packages.k8s.io).pkg.dev:registry.k8s.io のイメージ manifest 配信(Google Artifact Registry・<region>-docker.pkg.dev).amazonaws.com:イメージ本体(レイヤ)の配信(AWS S3・prod-registry-k8s-io-<region>.s3.dualstack...)storage.googleapis.com:一部イメージ本体の配信(Google Cloud Storage)
実行コマンド(alma-proxy・root):
# for d in .packages.k8s.io .pkg.dev storage.googleapis.com .amazonaws.com; do echo "$d" >> /etc/squid/whitelist.txt; done
# systemctl reload squid
先頭にドットを付けたエントリ(.pkg.dev など)は、Squid の dstdomain でそのドメインのすべてのサブドメインに一致します。CDN はリージョンごとにホスト名が変わるため、サブドメインをまとめて許可しておくと取りこぼしを防げます。
k8s-cp-01 を kubeadm 向けに下ごしらえする
kubeadm でクラスタを起動する前に、ノード側の前提条件を整えます。ここは本番運用でも必ず通る準備で、いずれも省くと後段で kubelet が起動しなかったり Pod が不安定になったりします。k8s-cp-01 に root で順に実施します。
swap を無効化する
kubelet は既定で swap が有効なノードでは起動を拒否します。今すぐ無効化するだけでなく、/etc/fstab の swap 行もコメントアウトして、再起動後も無効のままにします。片方だけだと再起動で swap が復活し、kubelet が起動しなくなります。
実行コマンド(k8s-cp-01・root):
# swapoff -a
# sed -i.bak '/swap/s/^/#/' /etc/fstab
# free -h | grep -i swap
実行結果:
Swap: 0B 0B 0B
Swap の各値が 0B になっていれば無効化できています。
SELinux は Enforcing のまま進める
kubeadm の公式手順には、SELinux を permissive に切り替える記述があります。コンテナランタイムやネットワークプラグインが必要とするアクセスを、SELinux が拒否する場合があったためです。しかし現在は container-selinux パッケージがコンテナ向けのポリシーを提供しており、AlmaLinux 10 では Enforcing のままでもクラスタは問題なく動きます。本書では緩めずに進めます。
SELinux は、プロセスやファイルに付けたラベルに基づいてアクセスを強制的に制御する仕組みです。Permissive は違反を記録するだけで遮断しません。つまり保護が働いていない状態です。CIS Kubernetes Benchmark をはじめとするセキュリティ基準は Enforcing での運用を求めるため、最初から Enforcing で構築しておけば、後から締め直す手間もかかりません。
現在の状態を確認します。
実行コマンド(k8s-cp-01・root):
# getenforce
実行結果:
Enforcing
Enforcing のままで進めます。ここでは何も変更しません。なお、根拠として挙げた container-selinux はこの時点ではまだ入っていません。次節で containerd.io を導入すると依存パッケージとして自動的に入るため、導入後に確認します。
もし SELinux が原因で何かが動かない場合は、切ってしまう前に何が拒否されたかを確認します。ausearch -m AVC -ts recent で拒否の記録(AVC)を読み、必要なら setsebool で個別に許可するのが本来の対処です。実際、後の第3回では HAProxy が 6443 番ポートへ接続する際にこの対処が必要になります。
公式手順と本書が違う点
Kubernetes 公式ドキュメントの kubeadm インストール手順には、いまも setenforce 0 で permissive にする記述が残っています。これは幅広い環境で確実に動かすための保守的な案内です。一方、本書が使う AlmaLinux 10 + containerd の組み合わせでは Enforcing で動作することを確認済みのため、セキュリティを優先して Enforcing を維持します。試験対策として公式手順の内容は把握しておき、本番では「なぜ緩めるのか、緩めずに済むか」を判断できるようにしてください。
カーネルモジュールを読み込む
containerd と Pod 間通信のために、overlay(オーバーレイファイルシステム)と br_netfilter(ブリッジ経由パケットを iptables で扱う)の 2 つのカーネルモジュールを読み込みます。/etc/modules-load.d/ に設定を置くことで、再起動後も自動で読み込まれます。
実行コマンド(k8s-cp-01・root):
# cat <<'EOF' > /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
# modprobe overlay
# modprobe br_netfilter
# lsmod | grep -E 'overlay|br_netfilter'
実行結果:
br_netfilter 36864 0
bridge 417792 1 br_netfilter
overlay 245760 0
sysctl を設定する
Pod 間通信のパケット転送を有効にするため、3 つのカーネルパラメータを設定します。net.ipv4.ip_forward はノードをまたぐパケット転送、bridge-nf-call-iptables 系はブリッジ経由のパケットを iptables のルール対象にするための設定です。/etc/sysctl.d/ に置いて永続化します。
実行コマンド(k8s-cp-01・root):
# cat <<'EOF' > /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
# sysctl --system
# sysctl net.ipv4.ip_forward
実行結果:
net.ipv4.ip_forward = 1
firewalld で必要なポートを開ける
本シリーズでは、本番運用に近い形を学ぶため firewalld を有効のまま、必要な通信だけを許可します。開放するのは API Server の 6443/tcp と kubelet の 10250/tcp です。あわせて、Pod ネットワーク(10.244.0.0/16)と Service ネットワーク(10.96.0.0/12)を信頼済み(trusted)として扱う設定も加えます。これは、CoreDNS などクラスタ内部の Pod が API Server と通信する経路を firewalld が遮断しないようにするためで、これを忘れるとノードが Ready になりません。etcd や Calico のノード間通信ポートは、複数ノードになる第4回で改めて開放します。
実行コマンド(k8s-cp-01・root):
# firewall-cmd --permanent --add-port=6443/tcp
# firewall-cmd --permanent --add-port=10250/tcp
# firewall-cmd --permanent --zone=trusted --add-source=10.244.0.0/16
# firewall-cmd --permanent --zone=trusted --add-source=10.96.0.0/12
# firewall-cmd --reload
# firewall-cmd --list-ports
# firewall-cmd --zone=trusted --list-all | grep sources
実行結果:
6443/tcp 10250/tcp
sources: 10.244.0.0/16 10.96.0.0/12
開放ポートに加えて、trusted ゾーンの sources に Pod・Service の CIDR が入っていることを確認してください。この 2 つがノードを Ready にするための要です。もし空なら --add-source が反映されておらず、後段でノードが Ready になりません。
containerd を導入する
kubelet が実際にコンテナを動かすために、コンテナランタイムとして containerd を入れます。ここで押さえておきたいのが拡張インターフェースという考え方です。Kubernetes はランタイムを CRI(Container Runtime Interface)という規格経由で扱うため、Docker に直結せず containerd などを差し替えて使えます。このあと入れる Calico は CNI(Container Network Interface)、第12回で扱う Longhorn は CSI(Container Storage Interface)で、CRI・CNI・CSI の 3 つが Kubernetes の主要な拡張点です。本回はその 1 つ目、CRI を担う containerd を導入します。
containerd は docker-ce リポジトリの containerd.io パッケージから導入します。リポジトリ定義ファイルを /etc/yum.repos.d/ に配置してインストールします。
実行コマンド(k8s-cp-01・root):
# curl -x http://192.168.1.121:3128 -sSL https://download.docker.com/linux/centos/docker-ce.repo -o /etc/yum.repos.d/docker-ce.repo
# dnf install -y containerd.io
# containerd --version
実行結果:
containerd containerd.io v2.2.6 11ce9d5f3c68c941867e82890e93e815c1304f1b
ここで、先ほど SELinux の節で触れた container-selinux を確認します。containerd.io の依存パッケージとして一緒に入っているはずです。これがコンテナ向けの SELinux ポリシーを提供するため、Enforcing のままでもコンテナが動きます。
実行コマンド(k8s-cp-01・root):
# rpm -q container-selinux
実行結果:
container-selinux-2.246.0-1.el10.noarch
入っていれば、Enforcing のまま先へ進めます。もし入っていない場合は dnf install -y container-selinux で導入してください。
次に containerd の設定を生成し、cgroup ドライバを systemd に合わせます。AlmaLinux 10 は cgroup v2 環境のため、SystemdCgroup = true に設定しないと kubelet と containerd の cgroup ドライバが食い違い、kubelet が正しく起動しません。既定設定を書き出してから該当行を書き換え、containerd を再起動して有効化します。
実行コマンド(k8s-cp-01・root):
# containerd config default > /etc/containerd/config.toml
# sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
# systemctl enable --now containerd
# grep SystemdCgroup /etc/containerd/config.toml
実行結果:
SystemdCgroup = true
containerd にプロキシを教える
もう 1 つ、忘れてはならない設定があります。kubeadm init は API Server などのコンテナイメージを registry.k8s.io から取得しますが、その取得を実際に行うのは containerd です。containerd はデーモンとして動くため、シェルのプロキシ環境変数を引き継ぎません。プロキシを教えないと、イメージ取得が外部へ直接出ようとして失敗します。systemd の drop-in で containerd にプロキシを設定します。
NO_PROXY には、クラスタ内部通信がプロキシへ回らないよう、Pod・Service の CIDR とノードのホスト名を含めます。これを怠ると、内部通信まで Squid 経由になり whitelist 外として拒否されます。
実行コマンド(k8s-cp-01・root):
# mkdir -p /etc/systemd/system/containerd.service.d
# cat <<'EOF' > /etc/systemd/system/containerd.service.d/http-proxy.conf
[Service]
Environment="HTTP_PROXY=http://192.168.1.121:3128"
Environment="HTTPS_PROXY=http://192.168.1.121:3128"
Environment="NO_PROXY=127.0.0.1,localhost,192.168.1.0/24,10.0.10.0/24,10.96.0.0/12,10.244.0.0/16,.svc,.cluster.local,k8s-lb,k8s-cp-01,k8s-cp-02,k8s-cp-03,k8s-wl-01,k8s-wl-02"
EOF
# systemctl daemon-reload
# systemctl restart containerd
# systemctl is-active containerd
実行結果:
active
kubeadm・kubelet・kubectl を導入する
Kubernetes 公式リポジトリ pkgs.k8s.io から、3 つのツールを導入します。第1回で触れたとおり、本巻ではバージョンを固定します。最新へ自動追従する運用は再現性を損なう本番アンチパターンだからです。まずリポジトリ定義を配置します。
ここで固定の粒度に注意してください。リポジトリの URL に含まれる v1.35 はマイナーバージョンまでの指定です。これだけでは v1.35.6 と v1.35.7 のようなパッチバージョンの違いには追従してしまい、実行した時期によって入るものが変わります。本書はパッチバージョンまで明示して固定します。
実行コマンド(k8s-cp-01・root):
# cat <<'EOF' > /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://pkgs.k8s.io/core:/stable:/v1.35/rpm/
enabled=1
gpgcheck=1
gpgkey=https://pkgs.k8s.io/core:/stable:/v1.35/rpm/repodata/repomd.xml.key
EOF
# dnf install -y kubelet-1.35.6 kubeadm-1.35.6 kubectl-1.35.6
# systemctl enable kubelet
# kubeadm version -o short
実行結果:
v1.35.6
この時点では kubelet は起動待ちの状態で構いません。kubeadm init が設定を書き込むと正常に起動します。最後に、意図しない自動アップグレードを防ぐためバージョンを固定します。アップグレードは計画的に行うもので、第6回で手順を扱います。
実行コマンド(k8s-cp-01・root):
# dnf install -y python3-dnf-plugin-versionlock
# dnf versionlock add kubelet kubeadm kubectl
実行結果:
Adding versionlock on: kubelet-0:1.35.6-150500.1.1.*
Adding versionlock on: kubeadm-0:1.35.6-150500.1.1.*
Adding versionlock on: kubectl-0:1.35.6-150500.1.1.*
k8s-lb の名前を用意する
これから kubeadm init に --control-plane-endpoint k8s-lb:6443 を指定します。この名前は init の実行時に解決できる必要があります。HA 構成では、ロードバランサの名前を最初から固定しておくと、あとでノードを増やしても作り直さずに済みます。ただし本物の HAProxy を載せる k8s-lb VM の構築は第3回のため、今は k8s-lb という名前を暫定的に k8s-cp-01 自身(192.168.1.125)へ向けます。
全ノードは名前解決を alma-proxy の dnsmasq に委ねています。dnsmasq は /etc/dnsmasq.hosts に全 VM のホスト名と IP を持っており、k8s-lb はすでに本来の IP 192.168.1.124(k8s-lb VM)として登録されています。ただしその VM に HAProxy はまだ無いため、この k8s-lb の行を一時的に k8s-cp-01(192.168.1.125)へ付け替えます。alma-proxy で設定します。
実行コマンド(alma-proxy・root):
# sed -i 's/^192.168.1.124 k8s-lb/192.168.1.125 k8s-lb/' /etc/dnsmasq.hosts
# systemctl restart dnsmasq
dnsmasq は設定の再読み込み(reload)に対応していないため、反映には restart を使います。
k8s-cp-01 側で名前が引けることを確認します。
実行コマンド(k8s-cp-01・root):
# getent hosts k8s-lb
実行結果:
192.168.1.125 k8s-lb
第3回では HAProxy を k8s-lb VM(192.168.1.124)に構築し、この dnsmasq のレコードを 192.168.1.124 へ変更します。そのとき HAProxy が k8s-cp-01 の API Server へ転送するようになりますが、k8s-lb:6443 という名前自体は変わりません。だからクラスタを作り直す必要がないわけです。
kubeadm init で単一ノードを起動する
下ごしらえが済んだので、コントロールプレーンを初期化します。イメージの取得は先ほどプロキシを教えた containerd が担うため、この段階でプロキシを追加で意識する必要はありません。kubeadm 自身の通信は API Server や etcd といったローカル宛て(127.0.0.1・localhost)で、これらは no_proxy に含まれるため、root シェルで kubeadm init をそのまま実行できます。
まず、必要なイメージを事前に取得しておきます。ここで失敗すれば whitelist の CDN 不足がすぐ分かるため、init の前に確認する意味があります。
実行コマンド(k8s-cp-01・root):
# kubeadm config images pull --kubernetes-version v1.35.6
実行結果:
[config/images] Pulled registry.k8s.io/kube-apiserver:v1.35.6
[config/images] Pulled registry.k8s.io/kube-controller-manager:v1.35.6
[config/images] Pulled registry.k8s.io/kube-scheduler:v1.35.6
[config/images] Pulled registry.k8s.io/kube-proxy:v1.35.6
[config/images] Pulled registry.k8s.io/coredns/coredns:v1.13.1
[config/images] Pulled registry.k8s.io/pause:3.10.1
[config/images] Pulled registry.k8s.io/etcd:3.6.6-0
7 つのイメージがすべて取得できました。もしここで Forbidden が出る場合は、リダイレクト先の CDN(.pkg.dev や .amazonaws.com)が whitelist に足りていません。続いて初期化します。--control-plane-endpoint に先ほど用意した名前、--pod-network-cidr に Calico と揃える 10.244.0.0/16、--apiserver-advertise-address に k8s-cp-01 の実 IP を指定します。実 IP を明示するのは、このホストのホスト名が名前解決の仕組み上 IPv6 のループバック(::1)に解決されるため、指定しないと advertise 先を取り違える可能性があるからです。--kubernetes-version にはインストールした kubeadm と同じ v1.35.6 を明示します。これを省略すると kubeadm は公式サイトへ問い合わせてそのマイナーの最新パッチ版を採用するため、コントロールプレーンだけが新しいパッチ版になり、kubelet とバージョンがずれます(実際に省略すると、kubelet が v1.35.6 なのに API Server が v1.35.7 で起動する、といった食い違いが起きます)。最後の --upload-certs は、第4回で追加のコントロールプレーンを参加させる際に証明書を引き継ぐための指定です。
実行コマンド(k8s-cp-01・root):
# kubeadm init \
--control-plane-endpoint k8s-lb:6443 \
--apiserver-advertise-address 192.168.1.125 \
--pod-network-cidr 10.244.0.0/16 \
--kubernetes-version v1.35.6 \
--upload-certs
実行結果(抜粋):
Your Kubernetes control-plane has initialized successfully!
To start using your cluster, you need to run the following as a regular user:
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
Alternatively, if you are the root user, you can run:
export KUBECONFIG=/etc/kubernetes/admin.conf
You should now deploy a pod network to the cluster.
Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:
https://kubernetes.io/docs/concepts/cluster-administration/addons/
You can now join any number of control-plane nodes running the following command on each as root:
kubeadm join k8s-lb:6443 --token <トークン> \
--discovery-token-ca-cert-hash sha256:<ハッシュ> \
--control-plane --certificate-key <証明書キー>
Then you can join any number of worker nodes by running the following on each as root:
kubeadm join k8s-lb:6443 --token <トークン> \
--discovery-token-ca-cert-hash sha256:<ハッシュ>
出力の末尾に、コントロールプレーン参加用と Workload Node 参加用の 2 種類の kubeadm join コマンドが表示されます。これらは第4回で使いますが、トークンは 24 時間、証明書キーは 2 時間で失効します。第4回では改めて発行し直すため、ここで無理に控えておく必要はありません。仕組みだけ頭に入れておいてください。
admin.conf を k8s-ops に配って kubectl を通す
クラスタの操作は、第1巻から作業端末として使ってきた k8s-ops から行います。管理用 kubeconfig である /etc/kubernetes/admin.conf(k8s-cp-01 上に root 所有・パーミッション 600 で置かれる特権ファイル)を k8s-ops へコピーします。まず k8s-cp-01 側で developer が読める場所へ複製します。
実行コマンド(k8s-cp-01・root):
# cp /etc/kubernetes/admin.conf /home/developer/admin.conf
# chown developer:developer /home/developer/admin.conf
次に、この admin.conf を k8s-ops へ運びます。ここで実環境の事情を 1 つ。各 VM への SSH 鍵は、あなたが各 VM にログインするのに使っている手元のマシン側にあり、k8s-ops から k8s-cp-01 へ直接 SSH できるとは限りません。そこで、手元のマシンを中継して admin.conf を運びます。まず k8s-ops 側に配置先ディレクトリを用意し(第1巻の kind 用にすでにあるはずですが、念のため)、手元のマシンで 2 段階の scp を実行します。
実行コマンド(手元のマシン):
$ ssh developer@192.168.1.122 'mkdir -p ~/.kube'
$ scp developer@192.168.1.125:/home/developer/admin.conf /tmp/admin.conf
$ scp /tmp/admin.conf developer@192.168.1.122:~/.kube/kubeadm.conf
$ rm /tmp/admin.conf
admin.conf はクラスタ全体を管理者権限で操作できる資格情報です。中継に使った手元のマシンの一時コピーは上記の rm で消しました。同様に、cp-01 に置いた中継用コピーも運び終えたら削除しておきます(cp-01 で sudo rm /home/developer/admin.conf)。正規の admin.conf は cp-01 の /etc/kubernetes/admin.conf(root 所有・600)に残るため、中継コピーを消しても問題ありません。
k8s-ops には第1巻の kind クラスタを指す kubeconfig(~/.kube/config)がまだ生きています。上書きを避けるため別ファイル(~/.kube/kubeadm.conf)として置き、KUBECONFIG で切り替えて使います。kind クラスタは第4回で役目を終えて破棄するまで残します。
もう 1 つ、忘れやすい点があります。kubectl は API Server へ k8s-lb:6443 というホスト名で接続します。k8s-ops のプロキシ設定(/etc/profile.d/proxy.sh の no_proxy)にこのホスト名が含まれていないと、接続がプロキシ経由になり Forbidden で拒否されます(no_proxy に IP レンジだけがあってもホスト名では一致しない点に注意)。含まれているか確認し、無ければクラスタ内のノード名もまとめて追加します。
実行コマンド(k8s-ops・root):
# grep -q ',k8s-lb' /etc/profile.d/proxy.sh || sed -i 's/\.cluster\.local"/.cluster.local,k8s-lb,k8s-cp-01,k8s-cp-02,k8s-cp-03,k8s-wl-01,k8s-wl-02,alma-proxy"/' /etc/profile.d/proxy.sh
この no_proxy はログインシェルで読み込まれるため、変更を反映するには k8s-ops にログインし直します(または developer で source /etc/profile.d/proxy.sh を実行します)。続いて developer で kubeconfig の権限を整え、kubectl を通します。
実行コマンド(k8s-ops・developer):
$ chmod 600 ~/.kube/kubeadm.conf
$ export KUBECONFIG=~/.kube/kubeadm.conf
$ alias k=kubectl
$ kubectl get nodes
実行結果:
NAME STATUS ROLES AGE VERSION
k8s-cp-01 NotReady control-plane 2m23s v1.35.6
k8s-cp-01 が表示されましたが、STATUS は NotReady です。これは異常ではありません。まだ CNI(ネットワークプラグイン)を入れていないため、Pod 間通信の土台が整っておらず、ノードは Ready になれないのです。次でこれを解消します。KUBECONFIG を設定していない元の状態に戻せば、これまでどおり kind クラスタを操作できます。
Calico で CNI を入れてノードを Ready にする
CNI として Calico v3.32.0 を適用します。第1巻の kind でも Calico を使いましたが、本番でも同じ CNI・同じ Pod CIDR(10.244.0.0/16)を継続することで、環境差による戸惑いを減らします。1 点だけ設定変更が必要です。Calico の既定の Pod CIDR は 192.168.0.0/16 で、これは検証環境の LAN(192.168.1.0/24)と重複します。そのため、マニフェストを取得してから CALICO_IPV4POOL_CIDR を 10.244.0.0/16 に書き換えて適用します。
まず、ホームディレクトリにマニフェストを取得します(この後の編集と適用も同じディレクトリで行います)。
実行コマンド(k8s-ops・developer):
$ cd ~
$ curl -x http://192.168.1.121:3128 -sSLO https://raw.githubusercontent.com/projectcalico/calico/v3.32.0/manifests/calico.yaml
取得した calico.yaml の中に、Pod CIDR を指定する環境変数の定義があります。既定ではコメントアウトされているので、エディタ(vi など)で開き、該当箇所の行頭の # を外して値を 192.168.0.0/16 から 10.244.0.0/16 に変更します。該当箇所は calico-node の DaemonSet 定義の中にあり、編集後は次のようになります。
- name: CALICO_IPV4POOL_CIDR
value: "10.244.0.0/16"
編集を保存したら適用します。その前に、KUBECONFIG が kubeadm クラスタ(~/.kube/kubeadm.conf)を指していることを必ず確認してください。設定を忘れて kind を指したままだと、Calico が第1巻のクラスタに適用されてしまいます。心配なら kubectl config current-context で接続先(kubernetes-admin@kubernetes になっているか)を確かめてから適用します。
実行コマンド(k8s-ops・developer):
$ kubectl apply -f calico.yaml
適用後、Calico の Pod が起動してくるのを待ちます。calico-node と calico-kube-controllers が Running になれば、ノードが Ready に変わります。
実行コマンド(k8s-ops・developer):
$ kubectl get pods -n kube-system | grep calico
$ kubectl get nodes
実行結果:
calico-kube-controllers-6b4b6457d5-lmpx8 1/1 Running 0 72s
calico-node-5p75j 1/1 Running 0 72s
NAME STATUS ROLES AGE VERSION
k8s-cp-01 Ready control-plane 4m56s v1.35.6
STATUS が Ready になりました。単一ノードのコントロールプレーンが動き出したことになります。なお、コントロールプレーンには既定で「通常の Pod を載せない」ための taint(汚れ)が付いています。本回ではこれを外しません。第4回で Workload Node を追加し、そちらへワークロードを配置する設計にするためです。
やってみよう
構築した単一ノードクラスタを、いくつかの角度から確認します。特に断りがなければ k8s-ops で KUBECONFIG を kubeadm クラスタに向けた状態で実行します(演習2 の一部だけ、Control Plane Node 上のファイルを見るため k8s-cp-01 で実行します)。
ノードの素性を確認する
実行コマンド:
$ kubectl get nodes -o wide
実行結果:
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
k8s-cp-01 Ready control-plane 5m47s v1.35.6 192.168.1.125 <none> AlmaLinux 10.2 (Lavender Lion) 6.12.0-211.22.1.el10_2.x86_64 containerd://2.2.6
OS-IMAGE が AlmaLinux 10.2 になっている点に注目してください。第1回で見た kind の kind-control-plane は Debian でした。あれは「コンテナの中の K8s」だったからです。今回は VM そのものがノードなので、AlmaLinux が表示されます。これが「マシンの上の K8s」です。INTERNAL-IP も k8s-cp-01 の実 IP になっています。
コントロールプレーンの中身を覗く
まず k8s-ops から、kube-system の Pod を確認します。
実行コマンド(k8s-ops・developer):
$ kubectl get pods -n kube-system
実行結果:
NAME READY STATUS RESTARTS AGE
calico-kube-controllers-6b4b6457d5-lmpx8 1/1 Running 0 2m3s
calico-node-5p75j 1/1 Running 0 2m3s
coredns-7d764666f9-j269l 1/1 Running 0 5m37s
coredns-7d764666f9-lj65s 1/1 Running 0 5m37s
etcd-k8s-cp-01 1/1 Running 0 5m44s
kube-apiserver-k8s-cp-01 1/1 Running 0 5m44s
kube-controller-manager-k8s-cp-01 1/1 Running 0 5m44s
kube-proxy-5nhsr 1/1 Running 0 5m37s
kube-scheduler-k8s-cp-01 1/1 Running 0 5m44s
次に、これらを定義している Static Pod の manifest ファイルそのものを見ます。/etc/kubernetes/manifests/ は Control Plane Node(k8s-cp-01)上のディレクトリで、k8s-ops には存在しません。この確認だけは k8s-cp-01 にログインして行います(ディレクトリの一覧表示はファイル本体の読み取りを伴わないため developer のままで確認できます)。
実行コマンド(k8s-cp-01・developer):
$ ls /etc/kubernetes/manifests/
実行結果:
etcd.yaml kube-apiserver.yaml kube-controller-manager.yaml kube-scheduler.yaml
k8s-ops で見た kube-system の Pod のうち、API Server・etcd・スケジューラ・コントローラマネージャという主要コンポーネントは、この /etc/kubernetes/manifests/ に置かれたファイルとして定義されています。第1回で触れた Static Pod(ファイルとして定義される Pod)の実物です。これらの標準パスは CKA 試験にそのまま登場します。
エンドポイントを確認する
実行コマンド:
$ getent hosts k8s-lb
$ kubectl cluster-info
実行結果:
192.168.1.125 k8s-lb
Kubernetes control plane is running at https://k8s-lb:6443
CoreDNS is running at https://k8s-lb:6443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
クラスタのエンドポイントが k8s-lb:6443 になっていること、そしてその名前が今は 192.168.1.125 を指していることを確認できます。第3回でこの指す先を HAProxy へ差し替えますが、名前は変わりません。
まとめ
本回では、1 台のノードに本番の Kubernetes を素から立ち上げました。下ごしらえ(swap・カーネルモジュール・sysctl・firewalld)から始まり、CRI を担う containerd、kubeadm・kubelet・kubectl の導入、kubeadm init による初期化、admin.conf の配布、Calico による CNI 適用という一本道を通り、ノードを Ready にしました。コントロールプレーンエンドポイントに k8s-lb という名前を先に据えたことが、次回以降の伏線になります。
理解度チェック(○×形式・全 9 問)
次の各文が正しいか(○)誤りか(×)を判断してください。下の「解答と解説」を開くと答え合わせができます。
- swap を無効化しないと kubelet は起動を拒否する。
- AlmaLinux 10 では SELinux を Enforcing のままにして kubeadm クラスタを構築できる。
--pod-network-cidrは CNI(Calico)の Pod CIDR と一致させる必要がある。- kubeadm は stable channel で常に最新版へ追従させるのが本巻の方針である。
- admin.conf は誰でも読めるように配置してよい。
- CNI を入れる前は、ノードは NotReady のままである。
--control-plane-endpointの名前は、後から HAProxy へ差し替えてもクラスタを作り直す必要がない。- whitelist プロキシ環境では、pkgs.k8s.io が 302 を返せばパッケージのダウンロードは必ず成功する。
- containerd はシェルのプロキシ環境変数を自動で引き継ぐため、イメージ取得のために追加設定は要らない。
解答と解説
1=○/2=○(container-selinux がコンテナ向けポリシーを提供するため Enforcing で動く。公式手順には permissive にする記述があるが、本書は緩めない)/3=○/4=×(v1.35 にピン留めする)/5=×(root 所有・パーミッション 600 の特権ファイル)/6=○(CNI 未導入では Ready にならない)/7=○(名前が不変なら再構築不要)/8=×(リダイレクト先の CDN も whitelist に許可が必要)/9=×(デーモンは環境変数を継がないため systemd drop-in でプロキシを明示設定する)
次回予告
次回・第3回では、この単一ノードを HA(高可用)構成へ育てる設計に踏み込みます。k8s-lb VM に HAProxy を構築して API Server のロードバランサを用意し、k8s-lb の名前をその HAProxy へ差し替えます。あわせて、なぜコントロールプレーンを 3 台にするのか、その根拠となる etcd の quorum(過半数合意)の考え方を学びます。
