サーバー移行の経験から学んだKubernetesの教訓
CentOS 8がEOLを迎えた2021年、私は1週間で5台のサーバーをRocky Linuxに移行しなければならなかった。その作業はmigrationのノウハウを教えてくれただけでなく、infrastructure管理の全体的なやり方を見直すきっかけにもなった。各サーバーで直接コンテナを動かし、サーバーごとに設定が微妙に異なり、deployには大量の手作業が必要だった…。そのとき、はじめてKubernetesを真剣に考えるようになった。
CentOS Stream 9はproductionに選んだ基盤だ。Fedoraより安定していて、RHELのupstreamに位置し、Red Hatの公式サポートがある。EKSやGKEのほうが便利だが、クラスター料金だけで月$70〜150ほど余計にかかる。kubeadm + Containerd + Calicoなら、安価なVPS上でも十分production-readyなクラスターを自己管理できる。
ContainerdとCalico:このコンボを選んだ理由
Kubernetes 1.24以降、DockerはコンテナruntimeとしてKubernetesに直接サポートされなくなった。いまだに勘違いしている人も多い。ContainerdはDockerから分離されたruntimeで、Dockerデーモンを必要とせず軽量だ。EKS、GKE、AKSのデフォルトruntimeでもある。
Calicoを選んだのはnetwork policyのためだ。Flannelはセットアップが簡単だが、policyがなく、デフォルトでは全podが互いに通信できてしまう。Calicoなら「サービスAはサービスBのみ呼び出し可能、データベースへの直接アクセスは禁止」といったルールを記述できる。複数チームが同じクラスターにdeployする場合には必要不可欠だ。ベアメタル上でのパフォーマンスも、FlannelのオーバーレイVXLANより優れている。
環境の準備
最小要件
- Control plane:2 CPU、2GB RAM(4GB推奨)
- Worker node:1 CPU、1GB RAM(2GB推奨)
- CentOS Stream 9、各ノードに静的IPを割り当て
- イメージをpullするためのインターネット接続
この記事では3ノード構成を使用する:control plane 1台+worker 2台。以下の手順は、特記がない限りすべてのノードで実行する。
swapの無効化とカーネル設定
Kubernetesはswapを完全に無効化する必要がある。最もよく忘れがちなステップで、これをスキップするとkubeletが不可解なエラーを出して何度も再起動することになり、デバッグが非常に困難になる:
# 即座にswapを無効化
swapoff -a
# 永続的に無効化 — /etc/fstabのswap行をコメントアウト
sed -i '/ swap / s/^\(.*\)$/#\1/g' /etc/fstab
# 必要なカーネルモジュールを有効化
cat <<EOF | tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
modprobe overlay
modprobe br_netfilter
# sysctlを設定
cat <<EOF | 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
sysctl --system
ファイアウォールの適切な設定
多くのガイドではfirewalldを無効化することを勧めているが、productionではそうしないこと。必要なポートだけを開けることで、セキュリティが高まり、後々のトラブルも回避できる:
# control planeで実行
firewall-cmd --permanent --add-port=6443/tcp # Kubernetes API
firewall-cmd --permanent --add-port=2379-2380/tcp # etcd
firewall-cmd --permanent --add-port=10250/tcp # Kubelet API
firewall-cmd --permanent --add-port=10259/tcp # kube-scheduler
firewall-cmd --permanent --add-port=10257/tcp # kube-controller-manager
# worker nodeで実行
firewall-cmd --permanent --add-port=10250/tcp
firewall-cmd --permanent --add-port=30000-32767/tcp # NodePort Services
# CalicoはBGPとIP-in-IPが必要 — 全ノードで適用
firewall-cmd --permanent --add-port=179/tcp
firewall-cmd --permanent --add-protocol=ipip
firewall-cmd --reload
Containerdのインストール
# Dockerリポジトリを追加(ContainerdはこのリポジトリにあるContainer runtime)
dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
# Containerdをインストール
dnf install -y containerd.io
# デフォルト設定を生成
mkdir -p /etc/containerd
containerd config default | tee /etc/containerd/config.toml
# SystemdCgroupを有効化 — Kubernetesでは必須
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
# 起動
systemctl enable --now containerd
SystemdCgroup = trueの設定はよく見落とされがちだ。falseのままにすると、kubeletとcontainerdが異なるcgroup driverを使うことになる。クラスターは起動し、podも動くが、resource limitsが正しく機能しない。この問題は高負荷時にのみ顕在化し、OOM killerが予測不能な形で発動する。
kubeadm、kubelet、kubectlのインストール
# Kubernetesリポジトリを追加
cat <<EOF | tee /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://pkgs.k8s.io/core:/stable:/v1.30/rpm/
enabled=1
gpgcheck=1
gpgkey=https://pkgs.k8s.io/core:/stable:/v1.30/rpm/repodata/repomd.xml.key
exclude=kubelet kubeadm kubectl cri-tools kubernetes-cni
EOF
# SELinuxをpermissiveに設定
setenforce 0
sed -i 's/^SELINUX=enforcing$/SELINUX=permissive/' /etc/selinux/config
# インストール
dnf install -y kubelet kubeadm kubectl --disableexcludes=kubernetes
# kubeletを有効化
systemctl enable --now kubelet
SELinuxについて:disabledではなくpermissiveにすることで、violationのログを残すことができる。将来的にどのようなpolicyを書けばよいかがわかるためだ。disabledのほうが手っ取り早いが、長期的なproductionでは適切なpolicyを記述する価値がある。
Control Planeの初期化
このステップはcontrol planeノードでのみ実行する:
kubeadm init \
--pod-network-cidr=192.168.0.0/16 \
--control-plane-endpoint="10.0.0.10:6443" \
--cri-socket=unix:///run/containerd/containerd.sock
各フラグの説明:
--pod-network-cidr=192.168.0.0/16:Calicoはデフォルトでこのレンジを使用する。正確に一致させる必要がある--control-plane-endpoint:control planeのIPアドレス。将来的にHAのために複数のcontrol planeを使いたい場合は、load balancerのIPを指定する--cri-socket:Containerd socketを明示的に指定する。複数のruntimeが存在する場合の混乱を避けるため
実行が完了すると、outputの末尾にkubeadm joinコマンドが表示される。すぐにコピーしておくこと。worker nodeをjoinする際に必要になる。その後、kubectlを設定する:
mkdir -p $HOME/.kube
cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
chown $(id -u):$(id -g) $HOME/.kube/config
Calico CNIのインストール
# Calico operatorをインストール
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/tigera-operator.yaml
# Calicoカスタムリソースをインストール
kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/custom-resources.yaml
# 全CalicoのpodがRunningになるまで監視
watch kubectl get pods -n calico-system
2〜3分待つ。全podがRunningになると、control planeノードはNotReadyからReadyに変わる:
kubectl get nodes
# NAME STATUS ROLES AGE VERSION
# cp-node Ready control-plane 5m v1.30.x
Worker NodeをClusterに参加させる
各worker nodeで以下のjoinコマンドを実行する。このコマンドはkubeadm initのoutputから取得したものだ:
kubeadm join 10.0.0.10:6443 \
--token <token> \
--discovery-token-ca-cert-hash sha256:<hash>
デフォルトのtokenは24時間で失効する。joinが遅れた場合は、control planeで新しいtokenを生成する:
kubeadm token create --print-join-command
クラスター稼働後の実践的なTips
基本的なクラスター確認
# 全ノードを確認
kubectl get nodes -o wide
# 全システムpodを確認
kubectl get pods -A
# テスト用deployを素早く実行
kubectl create deployment nginx-test --image=nginx --replicas=3
kubectl get pods -o wide # podが各nodeに均等に分散されていることを確認
etcdのバックアップ — 絶対に省略しないこと
etcdはclusterの全状態を保存している:deployment、secrets、configmaps、certificates。このsnapshotを失ったら復旧不可能だ。「一部が失われる」のではなく、「すべてが失われる」。私は毎日バックアップするcronjobを設定している:
ETCDCTL_API=3 etcdctl \
--endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key \
snapshot save /backup/etcd-$(date +%Y%m%d).db
メンテナンス前のノードのドレイン
OSの再起動やアップデートの前には必ずノードをdrainすること。Kubernetesはシャットダウン前に他のノードへpodをrescheduleする。podはdropされず、ユーザーには何も見えない:
# メンテナンス前
kubectl drain worker-1 --ignore-daemonsets --delete-emptydir-data
# メンテナンス完了後
kubectl uncordon worker-1
必ずresource limitsを設定する
requests/limitsを設定しないと、schedulerがどのノードにpodを配置すべきかを判断できない。最もよくある問題:あるpodがノードのメモリを使い果たし、OOM killerが発動して他のpodも道連れになる。全Deploymentに必要な最低限のテンプレート:
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
まとめ
初回のクラスターセットアップには1〜2時間ほどかかる。ほとんどはイメージのpullとCalicoの起動待ちだ。繰り返しのステップをスクリプト化すれば、次回以降はずっと速くなる。
セットアップの速さよりも、各ステップの意味を理解することのほうが重要だ。swapをなぜ無効化するのか、SystemdCgroupが何に影響するのか、CalicoがなぜpodのCIDRを独自に必要とするのか。これらを理解していなければ、クラスターで問題が発生したとき(必ず発生する)、destroyして再構築するしかなくなる。
クラスターを実際に使えるようにするために、すぐに追加すべき3つのものがある:metrics-server(kubectl topを使えるようにするため)、Nginx Ingress Controller(ドメイン経由でserviceを公開するため)、そしてworkloadがpersistent storageを必要とする場合はStorageClass。

