k3sとは、SUSE(旧Rancher Labs)が開発するCNCF認定の軽量Kubernetesディストリビューションで、containerd・flannel・CoreDNS・Traefik・ServiceLB・local-pathストレージが1つのバイナリに同梱され、組み込みetcdを使えば3ノードの高可用(HA)クラスタを追加ソフトなしで組めます。Kubernetes(k8s)の標準APIをそのまま持つので、ここで覚えた操作は会社のEKSやAKSでもそのまま通用します。
ミニPC3台(Proxmox上のVM3台でも同じ)にUbuntu Server 24.04を入れ、3台すべてをserver(コントロールプレーン兼ワーカー)にする構成で、インストールからkube-vipのVIP、MetalLB、Ingress、永続ストレージ、etcdスナップショットまでを通します。所要時間は40分ほどです。
この記事でわかること
- k3sと本家Kubernetesの違いと、自宅で3台すべてをserverにする理由(etcdのクォーラム)
/etc/rancher/k3s/config.yamlと--cluster-init、K3S_TOKEN、--tls-sanの役割- kube-vipでのVIP、MetalLBのL2モード、TraefikのIngressの組み合わせ方
- local-path / Longhorn / NFS の選び方と、etcdスナップショットからの復旧手順
- x509エラー、node password rejected、EXTERNAL-IPがpendingのままなど実際に出るエラーの直し方
Kubernetesとは何か、なぜk3sを選ぶのか
Kubernetesは、複数のマシンにまたがってコンテナを配置・監視・復旧するオーケストレーターです。「nginxを2つ動かす」という望ましい状態(Deployment)を宣言すると、コントローラーが差分を埋め続け、ノードが落ちれば残りのノードでPodを作り直します。Docker Composeが「1台の上での並べ方」なら、Kubernetesは「複数台での置き場所と直し方」です。
| 観点 | 本家Kubernetes(kubeadm) | k3s |
|---|---|---|
| 配布形態 | kubelet、kube-apiserver等を個別に導入 | 単一バイナリ(100MB未満)に全部入り |
| データストア | etcdを別途構築 | SQLite(単体)/ 組み込みetcd(HA)/ 外部DB |
| 同梱物 | CNI・Ingress・LBは自分で選ぶ | flannel、CoreDNS、Traefik、ServiceLB、local-path、metrics-server |
| メモリ(server 1台) | 目安2GB以上 | 目安512MB〜1GB |
自宅ではk3sを選びます。etcd・証明書・CNIという「動かす前の作業」が省け、壊しても k3s-uninstall.sh 一発で消せるからです。kubeadmで組む価値があるのは、CKAのようなkubeadm前提の試験対策と、本番と同一構成の検証に限られます。
構成:3ノード・組み込みetcd・VIP
| ホスト名 | IP | 役割 |
|---|---|---|
| k3s-1 | 192.168.10.11 | server(cluster-init)+ ワークロード |
| k3s-2 | 192.168.10.12 | server + ワークロード |
| k3s-3 | 192.168.10.13 | server + ワークロード |
| VIP | 192.168.10.100 | kube-vipがAPIサーバー用に保持 |
| MetalLBプール | 192.168.10.200〜220 | LoadBalancer Serviceに配る |
3台すべてをserverにするのは、組み込みetcdが過半数(クォーラム)で動くからです。3台なら1台落ちても書き込みが続き、2台だと1台落ちた時点で過半数を失って書き込み不能になり、1台構成より弱くなります。etcdは奇数台が原則です。k3sのserverは既定でワークロードも受け付けるので、家庭では「server 3台」が「server 1台 + agent 2台」より単純で強い構成になります。機材が1台しかないなら、SQLiteのserver単体で始めて後からetcdに移行できます。
ノード間で開けておくポートは、6443/tcp(APIサーバー)、2379-2380/tcp(etcd、server間のみ)、8472/udp(flannelのVXLAN)、10250/tcp(kubelet)です。ufwを有効にするなら、これに加えてPodネットワーク 10.42.0.0/16 とServiceネットワーク 10.43.0.0/16 からの通信を許可します。
下準備:固定IP・ホスト名・swap無効・時刻同期
3台それぞれで固定IPとホスト名を決めます。ホスト名はノード名としてetcdに登録されるので、重複させると後述の「Node password rejected」の原因になります。
network:
version: 2
ethernets:
eno1:
dhcp4: false
addresses: [192.168.10.11/24]
routes:
- to: default
via: 192.168.10.1
nameservers: {addresses: [192.168.10.1]}sudo netplan apply
sudo hostnamectl set-hostname k3s-1 # k3s-2, k3s-3 も同様
sudo swapoff -a
sudo sed -i '/\sswap\s/s/^/#/' /etc/fstab # 再起動後もswapを無効に
timedatectl # System clock synchronized: yes を確認
sudo ufw disable # 有効にするなら上記ポートを許可k3sはswapが有効でも起動しますが、メモリ不足時にPodがswapへ追い出されて「遅いのに落ちない」状態になるので無効にします。時刻がずれると証明書検証とetcdのリース更新が失敗するため、同期状態は必ず見ておきます。
1台目のインストールとトークン
設定はインストール時のフラグではなく /etc/rancher/k3s/config.yaml に書きます。フラグと同じ名前で -- を外すだけで、再インストールや更新のときに同じ設定が読まれます。トークンは自分で長い乱数を作って3台で共有します。
cluster-init: true
token: "ここに openssl rand -hex 32 で作った値"
tls-san:
- "192.168.10.100"
- "k3s.home.lan"
disable:
- servicelb
write-kubeconfig-mode: "0644"cluster-init: true がSQLiteではなく組み込みetcdで初期化する指示、tls-san はAPIサーバー証明書に含める追加の名前で、VIPやDNS名でアクセスするなら最初から入れておかないと x509 エラーになります。servicelb を無効にするのは、後でMetalLBを入れるためです。
sudo mkdir -p /etc/rancher/k3s
# config.yaml を上の内容で作成してから
curl -sfL https://get.k3s.io -o install-k3s.sh
less install-k3s.sh # 中身を確認してから実行する
sudo sh install-k3s.sh server
sudo cat /var/lib/rancher/k3s/server/node-token
sudo kubectl get nodesnode-token は「K10(CAのハッシュ)::server:(トークン)」という形で、ここに含まれるCAハッシュで参加ノードがサーバーの真正性を確認します。config.yamlに書いたトークンは /var/lib/rancher/k3s/server/token にも保存されます。
2台目・3台目の追加(serverとagent)
2台目以降のserverは、cluster-init の代わりに server: で1台目を指します。それ以外(token、tls-san、disable)は1台目と同じ内容にします。
server: https://192.168.10.11:6443
token: "1台目と同じ値"
tls-san:
- "192.168.10.100"
- "k3s.home.lan"
disable:
- servicelb
write-kubeconfig-mode: "0644"sudo sh install-k3s.sh server
# agentとして足すなら config.yaml なしで:
# K3S_URL=https://192.168.10.11:6443 K3S_TOKEN=トークン sh install-k3s.shagentはetcdもAPIサーバーも持たないワーカー専用ノードで、4台目以降やRaspberry Piのような非力な機械を混ぜるときに使います。
kubectlの設定:手元のPCから操作する
kubeconfigを手元のPCにコピーし、接続先をVIP(設定前なら1台目のIP)に書き換えます。
mkdir -p ~/.kube
scp [email protected]:/etc/rancher/k3s/k3s.yaml ~/.kube/config
sed -i 's/127.0.0.1/192.168.10.100/' ~/.kube/config
chmod 600 ~/.kube/config
kubectl get nodes -o widekube-vipでAPIサーバーにVIPを付ける
kubeconfigが1台目のIPを指したままだと、その1台が落ちた瞬間に kubectl が使えなくなります。kube-vipはARPモードで動くDaemonSetで、リーダー選出された1台がVIP(192.168.10.100)を自分のNICに付け、落ちれば別のノードが引き継ぎます。/var/lib/rancher/k3s/server/manifests/ に置いたマニフェストはk3sが自動で適用するので、そこにRBACとDaemonSetを置きます。
sudo curl -fsSL https://kube-vip.io/manifests/rbac.yaml \
-o /var/lib/rancher/k3s/server/manifests/kube-vip-rbac.yaml
export VIP=192.168.10.100 INTERFACE=eno1 KVVERSION=v0.8.9 # 執筆時点の例。最新版は公式で確認
sudo k3s ctr image pull ghcr.io/kube-vip/kube-vip:$KVVERSION
sudo k3s ctr run --rm --net-host ghcr.io/kube-vip/kube-vip:$KVVERSION vip /kube-vip \
manifest daemonset --interface $INTERFACE --address $VIP --inCluster \
--controlplane --arp --leaderElection \
| sudo tee /var/lib/rancher/k3s/server/manifests/kube-vip.yaml
ping -c 2 192.168.10.100--services を付けていないのは、LoadBalancer ServiceのIP配布はMetalLBに任せるためです。両方に持たせると同じServiceに2つのIPが付いて混乱します。VIPが応答したら、kubeconfigの接続先をVIPに変えます。
LoadBalancerとIngress:MetalLBとTraefik
クラウドなら type: LoadBalancer のServiceを作るとロードバランサーが自動で用意されますが、自宅にはそれがありません。k3s同梱のServiceLBは各ノードのIPとポートをそのまま使う簡易実装で、80番を使うServiceを2つ作れません。MetalLBのL2モードは、指定したプールからIPを1つ選び、ARPで「このIPは私です」と答えることで、Serviceごとに専用IPを与えます。
sudo snap install helm --classic
helm repo add metallb https://metallb.github.io/metallb && helm repo update
helm install metallb metallb/metallb -n metallb-system --create-namespace
kubectl -n metallb-system wait --for=condition=Ready pod --all --timeout=120sapiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: home-pool
namespace: metallb-system
spec:
addresses:
- 192.168.10.200-192.168.10.220
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: home-l2
namespace: metallb-system
spec:
ipAddressPools:
- home-poolkubectl apply -f metallb-pool.yaml の後、k3sが同梱するTraefikのService(kube-system/traefik)に自動でプール先頭の 192.168.10.200 が付きます。以後、HTTPのアプリはこのIP 1つにIngressのホスト名で振り分けるので、プールのIPを消費するのはHTTP以外(DNSやデータベース)だけです。プールの範囲はルーターのDHCP範囲から必ず外してください。
最初のアプリをデプロイする
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello
spec:
replicas: 2
selector:
matchLabels: {app: hello}
template:
metadata:
labels: {app: hello}
spec:
containers:
- name: nginx
image: nginx:1.27
ports: [{containerPort: 80}]
---
apiVersion: v1
kind: Service
metadata:
name: hello
spec:
selector: {app: hello}
ports: [{port: 80, targetPort: 80}]
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: hello
spec:
ingressClassName: traefik
rules:
- host: hello.home.lan
http:
paths:
- path: /
pathType: Prefix
backend:
service: {name: hello, port: {number: 80}}kubectl apply -f hello.yaml
kubectl get pods -o wide # 2つのPodが別ノードに配置される
curl -s -H "Host: hello.home.lan" http://192.168.10.200/ | grep title
# <title>Welcome to nginx!</title>自宅のDNS(ルーターやPi-hole)に *.home.lan を 192.168.10.200 に向けるワイルドカードを1つ登録しておくと、以後はIngressにホスト名を書くだけで新しいアプリが公開できます。
永続ストレージの選択肢:local-path・Longhorn・NFS
| 方式 | データの場所 | ノード停止時 | 向く用途 |
|---|---|---|---|
| local-path(既定のStorageClass) | そのノードの /var/lib/rancher/k3s/storage/ | Podは他ノードに移れず、復帰待ち | 検証、失っても困らないキャッシュ |
| Longhorn | 各ノードのディスクに複製(既定3レプリカ) | 別ノードのレプリカで継続 | DBや家計簿アプリなど止めたくない状態 |
| NFS(NASから) | NAS上 | どのノードからも同じデータ | 写真・動画・共有ファイル。複数Podの同時読み書き(RWX) |
最初はlocal-pathと、TrueNASなどNASからのNFSの2本立てで十分です。Longhornは各ノードにSSDと2.5GbE以上のNICがあり、ノード障害時にもDBを止めたくなくなった段階で入れます。前提パッケージがないとアタッチに失敗するので、先に3台へ入れます。
# 3台それぞれ
sudo apt install -y open-iscsi nfs-common && sudo systemctl enable --now iscsid
# 手元のPC
helm repo add longhorn https://charts.longhorn.io && helm repo update
helm install longhorn longhorn/longhorn -n longhorn-system --create-namespace更新とetcdスナップショット
更新はインストールスクリプトをバージョン指定で再実行するだけで、config.yamlは読み直されます。serverは1台ずつ、前の1台が Ready に戻ってから進めます。etcdスナップショットは既定で12時間ごとに /var/lib/rancher/k3s/server/db/snapshots/ へ5世代残ります。ここをProxmoxのVMバックアップやNASにも逃がしておくと、3台とも壊れても復旧できます。
# 更新(serverを1台ずつ)
curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION=v1.33.4+k3s1 sh -s - server
# etcdスナップショット
sudo k3s etcd-snapshot save --name before-upgrade
sudo k3s etcd-snapshot ls
# 復旧(全serverでk3sを停止し、1台目で実行 → 他の2台は db/ を消して再参加)
sudo systemctl stop k3s
sudo k3s server --cluster-reset \
--cluster-reset-restore-path=/var/lib/rancher/k3s/server/db/snapshots/before-upgrade-k3s-1-1758000000上のバージョン番号は執筆時点の例で、実在する最新版は公式のリリース一覧で確認してください。マイナーバージョンは1つずつ上げるのがKubernetesの原則で、1.31から1.33へ一気に上げるとAPIの互換性で失敗します。
動作確認
kubectl get nodes -o wide
# NAME STATUS ROLES AGE VERSION
# k3s-1 Ready control-plane,etcd,master 1h v1.33.x+k3s1
# k3s-2 Ready control-plane,etcd,master ...
# k3s-3 Ready control-plane,etcd,master ...
kubectl get pods -A | grep -v Running # ヘッダー以外何も出なければ正常
kubectl -n kube-system get svc traefik # EXTERNAL-IP が 192.168.10.200
sudo systemctl stop k3s # k3s-1 で実行し、手元から kubectl get nodes が通れば VIP と etcd の HA が効いている
sudo systemctl start k3s1台を止める試験は、3台すべてがReadyで、etcdスナップショットを取った後に行ってください。2台同時に止めると過半数を失い、書き込み不能になります。
つまずきポイント
x509: certificate is valid for 127.0.0.1, 192.168.10.11, ..., not 192.168.10.100:tls-sanにVIPが入っていません。config.yamlに追記してsystemctl restart k3sすると証明書が再生成されます。Node password rejected, duplicate hostname or contents of '/etc/rancher/node/password' may not match server node-passwd entry: 同じホスト名のノードを作り直したときに出ます。kubectl delete node k3s-2とkubectl -n kube-system delete secret k3s-2.node-password.k3sを実行してから再参加します。- LoadBalancer Serviceの EXTERNAL-IP が
<pending>のまま: IPAddressPoolとL2Advertisementが未適用か、servicelbを無効にし忘れて競合しています。kubectl -n metallb-system logs deploy/metallb-controllerで確認します。 - 2台目の参加が
failed to get CA certsでタイムアウトする: 1台目の6443/tcpに届いていません。ufwやProxmoxのファイアウォール、server:のIPの打ち間違いを確認します。
よくある質問
k3sと本家Kubernetesの違いは?
APIは同じで、CNCFの適合認定も受けています。違いは配布形態で、単一バイナリにcontainerd・CNI・Ingress・ストレージを同梱し、クラウド向けのin-treeプロバイダーを省いてメモリを抑えています。マニフェストやHelmチャートはそのまま使えます。
ノードは何台必要ですか?
学習だけなら1台(SQLite)で全機能が動きます。HAにするなら組み込みetcdの過半数が要るので、serverは3台が最小です。2台はetcdの過半数を維持できず、1台より弱くなるので避けてください。ワーカー(agent)は0台でも4台でも構いません。
Raspberry Piでも動きますか?
arm64版があり、Raspberry Pi 4/5で動きます。Raspberry Pi OSでは /boot/firmware/cmdline.txt に cgroup_memory=1 cgroup_enable=memory を追記し、Ubuntuでは linux-modules-extra-raspi を入れる必要があります。etcdはSDカードを早く消耗させるので、serverはSSD起動にするか、Piはagentに回します。
まとめ
config.yamlに cluster-init・token・tls-san(VIP入り)・disable: servicelb を書いて3台をserverで組み、kube-vipでAPIサーバーに、MetalLBでServiceにそれぞれIPを与え、HTTPはTraefikのIngressに集約する。この形にしておけば、1台止めてもkubectlもアプリも動き続けます。ストレージはlocal-pathとNFSから始め、etcdスナップショットはクラスタの外にも置いてください。外出先からVIPに届かせたいときはTailscaleのSubnet Routerを1台目に置くのが最短です。