CentOS Stream 9でkubeadmを使ったKubernetesデプロイガイド:ContainerdとCalico CNIでproduction-readyクラスターを構築する

CentOS tutorial - IT technology blog
CentOS tutorial - IT technology blog

サーバー移行の経験から学んだ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。

Share: