ラズパイクラスタでkubernetes the hard wayをやってみた

に公開

はじめに

k8sとお友達になりたいと今日この頃思い始めたので、よくお見掛けするラズパイクラスタでのk8s構築をしてみました。(ミニpcでproxmoxなどでVM組んでやった方が良い!などの意見も見ますが、私は物理的に組み立てをやりたかったのもあったのでラズパイを選びました。)
また、折角なので基本的なコンポーネントについても学びたく、kubernetes the hard wayを参考に構築してみました。

HW準備

用意したもの

下記のような機器を用意しました。

機器 数量 用途
Raspberry Pi4 Model B (8GB) 3 主役のラズパイくん。 マスターx1, ワーカーx2想定。
GeeekPi Raspberry Pi4 PoE HAT 3 PoE給電を採用。配線が少ない方が嬉しい。
SanDisk microSDカード 32GB 2 ラズパイに挿す子。128GBを1枚持ってたので2枚だけ購入。
SanDisk microSDカード 128GB 1 うちに眠ってた子を引っ張り出した。
GeeekPi Raspberry Pi4クラスターケース 1 クラスタというからには見た目もそれっぽく。
TP-Link スイッチングハブ PoEハブ 1 LAN構築兼ラズパイくんへのPoE給電。
エレコム LANケーブル CAT6 0.15m 3 ラズパイとスイッチを繋ぐ用。短いのが欲しかった。
KIOXIA 内蔵 SSD 480GB 1 うちに眠ってた子を引っ張り出した。

組み立て

  1. ラズパイにPoE HATを装着

    クラスタケースに付属していたヒートシンクをペタペタと貼って、いざPoE HATを装着!

    しかし、RAM部分のヒートシンクと物理干渉してしまうため、気をつけながら剥がしました。

  2. クラスタケースに積み上げる・2段・3段

    HATを装着したラズパイたちをクラスタケースに載せていきます。

    ・1段

  3. スイッチと接続して、PoE動作確認

    最後にLANケーブルぶっ刺してスイッチと繋ぎます。

    スイッチに電源を入れると、無事にラズパイも電源が入りました。

    やはり線の少なさは魅力的です。(ちゃんと動作することが一番ですが。)

構成

NW

LAN1: Wifi(192.168.0.0/24)

Name IP Address
master01 192.168.0.101
worker01 192.168.0.151
worker02 192.168.0.152

k8s Network

Name CIDR
Pod用サブネット 10.10.0.0/16
├ worker01 10.10.1.0/24
└ worker02 10.10.2.0/24
Cluster IP用サブネット 10.32.0.0/24

k8s

以下の公式の図が今回構成するk8sのアーキテクチャとしてシンプルでわかりやすい。(cloud-controller-managerは今回の対象外)

https://kubernetes.io/images/docs/kubernetes-cluster-architecture.svg

各コンポーネントの説明は以下の資料が個人的にはちょうどいい粒度でした。

https://speakerdeck.com/bells17/akitekutiyakaraxue-hukubernetesnoquan-ti-xiang

OS設定

  • OSはUbuntu Server 24.04.2 LTSを使用。
  • Raspberry Pi MangerでOSイメージ書き込むときに以下を追加で設定しておく(楽なので)。
    • ホスト名
    • ユーザー作成
    • Wi-Fi(必要な場合)
    • ssh接続有効化
  • IPアドレスの固定

/etc/netplan/ 配下の設定yamlファイルを作成もしくは編集して以下を設定する。今回は有線LANのみ利用するが、Wifiを利用する場合はその分の設定も追加する

めも
/etc/netplan配下に複数のyamlがある場合は辞書順に読み込まれて設定がマージされていく。設定項目レベルの重複がある場合は後から読み込まれた設定で上書きされる。

network:
    version: 2
    ethernets:
        eth0:
            dhcp4: false
            addresses:
                - 192.168.0.x/24

作成したらnetplan applyで設定を反映させる。

新規にyamlファイルを作成するとPermissions for /etc/netplan/<作成したyamlファイル> are too open. Netplan configuration should NOT be accessible by others. のようなパーミッション広すぎ警告が出ることがあります。

  • 各種ラズパイの/etc/hostsに互いのホスト名を登録しておくと接続が楽になります。

バージョン(2025/06/01時点)

各種バイナリファイルをダウンロード

ラズパイに配布するために、作業用pcにダウンロードしていく。

kubectlに関しては作業用PCへインストールするので、作業用PCのアーキテクチャにあったものをダウンロードする点に注意。

  • ダウンロードしたもの(https://github.com/kelseyhightower/kubernetes-the-hard-way/blob/master/docs/02-jumpbox.mdの手順に従って取得)

    kubectl 1.32.3 k8sのCLIクライアントツール
    kube-apiserver 1.32.3 k8s APIを提供
    kube-controller-manager 1.32.3 各種コントローラの管理
    kube-scheduler 1.32.3 PodをNodeに割り当て
    etcd 3.6.0 k8sのクラスター情報を保存するKVS
    kubelet 1.32.3 Podの管理
    kube-proxy 1.32.3 Serviceへのルーティング処理
    crictl 1.32.0 CRI互換のコンテナランタイム用のコマンドラインインターフェース
    runc 1.3.0 コンテナの低レベルランタイム
    containerd 2.1.0 コンテナの高レベルランタイム
    cni 1.6.2 Container Network Interface

kubectl は実行権限を付与して/usr/local/bin に配置し、コマンド実行できることを確認する。

kubectl version --client
> Client Version: v1.32.3
> Kustomize Version: v5.5.0

証明書

k8sではコンポーネント間の通信が基本的にTLS通信で行われるらしい。

https://kubernetes.io/ja/docs/setup/best-practices/certificates/

本家ではopensslを用いているが、ここではcfsslを用いることにする。

必要な証明書

Kubernetesは以下のコンポーネント・用途にて証明書を利用するとのこと。

kubeletがAPIサーバーの認証をするためのクライアント証明書
APIサーバーがkubeletと通信するためのkubeletのサーバー証明書
APIサーバーのエンドポイント用サーバー証明書
クラスターの管理者がAPIサーバーの認証を行うためのクライアント証明書
APIサーバーがkubeletと通信するためのクライアント証明書
APIサーバーがetcdと通信するためのクライアント証明書
controller managerがAPIサーバーと通信するためのクライアント証明書およびkubeconfig
スケジューラーがAPIサーバーと通信するためのクライアント証明書およびkubeconfig
front-proxy用のクライアント証明書およびサーバー証明書

証明書の作成

  • CA
    configとcsrの情報を記載したjsonを作成する。cfsslではこの辺りをjsonで管理できるのが良き。

    • ca-config.json

      {
          "signing": {
              "default": {
                  "expiry": "87600h"
              },
              "profiles": {
                  "kubernetes": {
                      "expiry": "87600h",
                      "usages": [
                          "signing",
                          "key encipherment",
                          "server auth",
                          "client auth"
                      ]
                  }
              }
          }
      }
      
    • ca-csr.json

      {
          "CN": "Kubernetes",
          "key": {
              "algo": "rsa",
              "size": 2048
          },
          "names": [
              {
      	          "C": "JP",
      	          "O": "Kubernetes",
      	          "ST": "Tokyo"
              }
          ]
      }
      

    以下のコマンドで認証局の秘密鍵と自己署名証明書を作成する。

    cfssl gencert -initca ca-csr.json | cfssljson -bare ca -
    
  • admin用

    クライアント証明書の作成

    • admin-csr.json

      {
          "CN": "admin",
          "key": {
              "algo": "rsa",
              "size": 2048
          },
          "names": [
              {
                  "C": "JP",
                  "O": "system:masters",
                  "ST": "Tokyo"
              }
          ]
      }
      

    以下のコマンドで秘密鍵と証明書を作成する。

    cfssl gencert \
      -ca=ca.pem \
      -ca-key=ca-key.pem \
      -config=ca-config.json \
      -profile=kubernetes \
      admin-csr.json | cfssljson -bare admin
    
  • kubelet用
    クライアント証明書の作成

    • <hostname>-csr.json

      {
          "CN": "system:node:<hostname>",
          "key": {
              "algo": "rsa",
              "size": 2048
          },
          "names": [
              {
                  "C": "JP",
                  "O": "system:nodes",
                  "ST": "Tokyo"
              }
          ]
      }
      

    以下のコマンドで秘密鍵と証明書を作成する。

    cfssl gencert \
      -ca=ca.pem \
      -ca-key=ca-key.pem \
      -config=ca-config.json \
      -hostname=<hostname>,<node_ipaddr> \
      -profile=kubernetes \
      <hostname>-csr.json | cfssljson -bare <hostname>
    

    なお、<node_ipaddr>は各ノードのIPアドレスとなるため、以下のように設定しました。

    <hostname> <node_ipaddr>
    master01 192.168.0.101
    worker01 192.168.0.151
    worker02 192.168.0.152
  • kube-controller-manager用
    クライアント証明書の作成

    • kube-controller-manager-csr.json

      {
          "CN": "system:kube-controller-manager",
          "key": {
              "algo": "rsa",
              "size": 2048
          },
          "names": [
              {
                  "C": "JP",
                  "O": "system:kube-controller-manager",
                  "ST": "Tokyo"
              }
          ]
      }
      

    以下のコマンドで秘密鍵と証明書を作成する。

    cfssl gencert \
      -ca=ca.pem \
      -ca-key=ca-key.pem \
      -config=ca-config.json \
      -profile=kubernetes \
      kube-controller-manager-csr.json | cfssljson -bare kube-controller-manager
    
  • kube-proxy用
    クライアント証明書の作成

    • kube-proxy-csr.json

      {
          "CN": "system:kube-proxy"
          "key": {
              "algo": "rsa",
              "size": 2048
          },
          "names": [
              {
                  "C": "JP",
                  "O": "system:node-proxier",
                  "ST": "Tokyo"
              }
          ]
      }
      

    以下のコマンドで秘密鍵と証明書を作成する。

    cfssl gencert \
      -ca=ca.pem \
      -ca-key=ca-key.pem \
      -config=ca-config.json \
      -profile=kubernetes \
      kube-proxy-csr.json | cfssljson -bare kube-proxy
    
  • kube-scheduler用
    クライアント証明書の作成

    • kube-scheduler-csr.json

      {
          "CN": "system:kube-scheduler",
          "key": {
              "algo": "rsa",
              "size": 2048
          },
          "names": [
              {
                  "C": "JP",
                  "O": "system:kube-scheduler",
                  "ST": "Tokyo"
              }
          ]
      }
      

    以下のコマンドで秘密鍵と証明書を作成する。

    cfssl gencert \
      -ca=ca.pem \
      -ca-key=ca-key.pem \
      -config=ca-config.json \
      -profile=kubernetes \
      kube-scheduler-csr.json | cfssljson -bare kube-scheduler
    
  • kube-apiserver用
    サーバ証明書の作成

    • kubernetes-csr.json

      {
          "CN": "kubernetes",
          "key": {
              "algo": "rsa",
              "size": 2048
          },
          "names": [
              {
                  "C": "JP",
                  "O": "Kubernetes",
                  "ST": "Tokyo"
              }
          ]
      }
      

    以下のコマンドで秘密鍵と証明書を作成する。

    ※workerノードのセットアップにて後述しますが、以下のコマンドの-hostname に不足があります。

    cfssl gencert \
      -ca=ca.pem \
      -ca-key=ca-key.pem \
      -config=ca-config.json \
      -hostname=10.32.0.1,master01,127.0.0.1,kubernetes,kubernetes.default,kubernetes.default.svc,kubernetes.default.svc.cluster,kubernetes.svc.cluster.local,api-server.kubernetes.local \
      -profile=kubernetes \
      kubernetes-csr.json | cfssljson -bare kubernetes
    
  • service-account用
    クライアント証明書の作成

    • service-account-csr.json

      {
          "CN": "service-account",
          "key": {
              "algo": "rsa",
              "size": 2048
          },
          "names": [
              {
                  "C": "JP",
                  "O": "Kubernetes",
                  "ST": "Tokyo"
              }
          ]
      }
      

    以下のコマンドで秘密鍵と証明書を作成する。

    cfssl gencert \
      -ca=ca.pem \
      -ca-key=ca-key.pem \
      -config=ca-config.json \
      -profile=kubernetes \
      service-account-csr.json | cfssljson -bare service-account
    

作成できたら証明書を各サーバに配置します。配置する証明書は以下の通りです。

  • master01
    • ca.pem
    • ca-key.pem
    • kebernetes.pem
    • kuberbetes-key.pem
    • service-sccount.pem
    • service-sccount-key.pem
  • worker01, worker02
    • ca.pem
    • <hostname>.pem
    • <hostname>-key.pem

Kubernetes Configファイルの作成

各コンポーネントとkube-apiserverが通信するための設定ファイルを作成します。

  • kubelet用

    for host in worker01 worker02; do
      kubectl config set-cluster kubernetes-the-hard-way \
        --certificate-authority=ca.pem \
        --embed-certs=true \
        --server=https://master01.kubernetes.local:6443 \
        --kubeconfig=${host}.kubeconfig
    
      kubectl config set-credentials system:node:${host} \
        --client-certificate=${host}.pem \
        --client-key=${host}-key.pem \
        --embed-certs=true \
        --kubeconfig=${host}.kubeconfig
    
      kubectl config set-context default \
        --cluster=kubernetes-the-hard-way \
        --user=system:node:${host} \
        --kubeconfig=${host}.kubeconfig
    
      kubectl config use-context default \
        --kubeconfig=${host}.kubeconfig
    done
    
  • kube-proxy用

    {
      kubectl config set-cluster kubernetes-the-hard-way \
        --certificate-authority=ca.pem \
        --embed-certs=true \
        --server=https://master01.kubernetes.local:6443 \
        --kubeconfig=kube-proxy.kubeconfig
    
      kubectl config set-credentials system:kube-proxy \
        --client-certificate=kube-proxy.pem \
        --client-key=kube-proxy-key.pem \
        --embed-certs=true \
        --kubeconfig=kube-proxy.kubeconfig
    
      kubectl config set-context default \
        --cluster=kubernetes-the-hard-way \
        --user=system:kube-proxy \
        --kubeconfig=kube-proxy.kubeconfig
    
      kubectl config use-context default \
        --kubeconfig=kube-proxy.kubeconfig
    }
    
  • kube-controller-manager用

    {
      kubectl config set-cluster kubernetes-the-hard-way \
        --certificate-authority=ca.pem \
        --embed-certs=true \
        --server=https://master01.kubernetes.local:6443 \
        --kubeconfig=kube-controller-manager.kubeconfig
    
      kubectl config set-credentials system:kube-controller-manager \
        --client-certificate=kube-controller-manager.pem \
        --client-key=kube-controller-manager-key.pem \
        --embed-certs=true \
        --kubeconfig=kube-controller-manager.kubeconfig
    
      kubectl config set-context default \
        --cluster=kubernetes-the-hard-way \
        --user=system:kube-controller-manager \
        --kubeconfig=kube-controller-manager.kubeconfig
    
      kubectl config use-context default \
        --kubeconfig=kube-controller-manager.kubeconfig
    }
    
  • kube-scheduler用

    {
      kubectl config set-cluster kubernetes-the-hard-way \
        --certificate-authority=ca.pem \
        --embed-certs=true \
        --server=https://master01.kubernetes.local:6443 \
        --kubeconfig=kube-scheduler.kubeconfig
    
      kubectl config set-credentials system:kube-scheduler \
        --client-certificate=kube-scheduler.pem \
        --client-key=kube-scheduler-key.pem \
        --embed-certs=true \
        --kubeconfig=kube-scheduler.kubeconfig
    
      kubectl config set-context default \
        --cluster=kubernetes-the-hard-way \
        --user=system:kube-scheduler \
        --kubeconfig=kube-scheduler.kubeconfig
    
      kubectl config use-context default \
        --kubeconfig=kube-scheduler.kubeconfig
    }
    
  • admin用

    {
      kubectl config set-cluster kubernetes-the-hard-way \
        --certificate-authority=ca.pem \
        --embed-certs=true \
        --server=https://127.0.0.1:6443 \
        --kubeconfig=admin.kubeconfig
    
      kubectl config set-credentials admin \
        --client-certificate=admin.pem \
        --client-key=admin-key.pem \
        --embed-certs=true \
        --kubeconfig=admin.kubeconfig
    
      kubectl config set-context default \
        --cluster=kubernetes-the-hard-way \
        --user=admin \
        --kubeconfig=admin.kubeconfig
    
      kubectl config use-context default \
        --kubeconfig=admin.kubeconfig
    }
    

作成できたら各サーバに配置します。配置するkubeconfigファイルは以下の通りです。

  • master01
    • admin.kubeconfig
    • kube-controller-manager.kubeconfig
    • kube-scheduler.kubeconfig
  • worker01, worker02
    • kube-proxy.kubeconfig
    • <hostname>.kubeconfig

データ暗号化用の設定、および暗号化鍵の生成

/dev/urandom を用いて暗号化鍵を簡易的に作成し、暗号化用の設定ファイルを作成し、masterノードへ配置する。

  • encryption-config.yaml

    kind: EncryptionConfiguration
    apiVersion: apiserver.config.k8s.io/v1
    resources:
      - resources:
          - secrets
        providers:
          - aescbc:
              keys:
                - name: key1
                  secret: <encryption_key>
          - identity: {}
    

etcdのセットアップ

etcdはキーバリューストアで、k8sクラスタ内の状態を保存してくれている。
kube-apiserverが各コンポーネントからのAPIリクエストを受けるなどしてetcdの状態の閲覧・更新する。

各種ファイルの配置

masterノードへetcdのバイナリを転送する。 ⇒ /usr/local/binへ配置

  • etcd
  • etcdctl

etcdの設定

  • ディレクトリを作成し、証明書を配置する。
mkdir -p /etc/etcd /var/lib/etcd
chmod 700 /var/lib/etcd
cp ca.pem kubernetes.pem kubernetes-key.pem /etc/etcd/
  • etcd.service を作成してetcdの名前を設定する。
    • etcdクラスタ内で固有の名称を設定する必要あり
[Unit]
Description=etcd
Documentation=https://github.com/etcd-io/etcd

[Service]
Type=notify
ExecStart=/usr/local/bin/etcd \
  --name <クラスター固有名称> \
  --cert-file=/etc/etcd/kubernetes.pem \
  --key-file=/etc/etcd/kubernetes-key.pem \
  --peer-cert-file=/etc/etcd/kubernetes.pem \
  --peer-key-file=/etc/etcd/kubernetes-key.pem \
  --trusted-ca-file=/etc/etcd/ca.pem \
  --peer-trusted-ca-file=/etc/etcd/ca.pem \
  --peer-client-cert-auth \
  --client-cert-auth \
  --initial-advertise-peer-urls http://127.0.0.1:2380 \
  --listen-peer-urls http://127.0.0.1:2380 \
  --listen-client-urls http://127.0.0.1:2379 \
  --advertise-client-urls http://127.0.0.1:2379 \
  --initial-cluster-token etcd-cluster-0 \
  --initial-cluster controller=http://127.0.0.1:2380 \
  --initial-cluster-state new \
  --data-dir=/var/lib/etcd
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
  • etcd.service を/etc/systemd/system/配下に配置して、systemctlで起動する。
systemctl daemon-reload
systemctl enable etcd
systemctl start etcd

etcdctl member list
>6702b0a34e2cfd39, started, master01, http://127.0.0.1:2380, http://127.0.0.1:2379, false

コントロールプレーン(masterノード)の設定

各種ファイルの配置

masterノードへ各種バイナリを転送する。 ⇒ /usr/local/binへ配置

  • kube-apiserver
  • kube-controller-manager
  • kube-scheduler
  • kubectl(作業用pcとmasterノードでcpuアーキテクチャが異なる場合は注意)

kube-apiserverの設定

  • ディレクトリを作成し、証明書を配置する。
mkdir -p /var/lib/kubernetes/
mv ca.pem ca-key.pem kubermetes.pem kubernetes-key.pem service-account.pem service-account-key.pem encryption-config.yaml /var/lib/kubernetes/
  • kube-apiserver.service を作成し、/etc/systemd/system/配下に配置する。
[Unit]
Description=Kubernetes API Server
Documentation=https://github.com/kubernetes/kubernetes

[Service]
ExecStart=/usr/local/bin/kube-apiserver \
  --allow-privileged=true \
  --audit-log-maxage=30 \
  --audit-log-maxbackup=3 \
  --audit-log-maxsize=100 \
  --audit-log-path=/var/log/audit.log \
  --authorization-mode=Node,RBAC \
  --bind-address=0.0.0.0 \
  --client-ca-file=/var/lib/kubernetes/ca.pem\
  --enable-admission-plugins=NamespaceLifecycle,NodeRestriction,LimitRanger,ServiceAccount,DefaultStorageClass,ResourceQuota \
  --etcd-servers=http://127.0.0.1:2379 \
  --event-ttl=1h \
  --encryption-provider-config=/var/lib/kubernetes/encryption-config.yaml \
  --kubelet-certificate-authority=/var/lib/kubernetes/ca.pem \
  --kubelet-client-certificate=/var/lib/kubernetes/kubernetes.pem \
  --kubelet-client-key=/var/lib/kubernetes/kubernets-key.pem \
  --runtime-config='api/all=true' \
  --service-account-key-file=/var/lib/kubernetes/service-accounts.pem \
  --service-account-signing-key-file=/var/lib/kubernetes/service-accounts-key.pem \
  --service-account-issuer=https://server.kubernetes.local:6443 \
  --service-node-port-range=30000-32767 \
  --tls-cert-file=/var/lib/kubernetes/kubernetes.pem \
  --tls-private-key-file=/var/lib/kubernetes/kubernetes-key.pem \
  --v=2
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

kube-controller-managerの設定

  • kubeconfigファイルを/var/lib/kubernets/配下に配置する。
mv kube-controller-manager.kubeconfig /var/lib/kubernetes
  • kube-controller-manager.service を作成し、/etc/systemd/system/配下に配置する。
[Unit]
Description=Kubernetes Controller Manager
Documentation=https://github.com/kubernetes/kubernetes

[Service]
ExecStart=/usr/local/bin/kube-controller-manager \
  --bind-address=0.0.0.0 \
  --cluster-cidr=10.200.0.0/16 \
  --cluster-name=kubernetes \
  --cluster-signing-cert-file=/var/lib/kubernetes/ca.pem\
  --cluster-signing-key-file=/var/lib/kubernetes/ca-key.pem \
  --kubeconfig=/var/lib/kubernetes/kube-controller-manager.kubeconfig \
  --root-ca-file=/var/lib/kubernetes/ca.pem \
  --service-account-private-key-file=/var/lib/kubernetes/service-accounts-key.pem \
  --service-cluster-ip-range=10.32.0.0/24 \
  --use-service-account-credentials=true \
  --v=2
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

kube-schedulerの設定

  • kubeconfigファイルを/var/lib/kubernets/配下に配置する。
mv kube-scheduler.kubeconfig /var/lib/kubernetes
  • 設定ファイルkube-scheduler.yamlを作成し、/etc/kubernetes/config/配下に配置する。
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
clientConnection:
  kubeconfig: "/var/lib/kubernetes/kube-scheduler.kubeconfig"
leaderElection:
  leaderElect: true
mkdir -p /etc/kubernetes/config
mv kube-scheduler.yaml /etc/kubernetes/config/
  • kube-scheduler.service を作成し、/etc/systemd/system/配下に配置する。
[Unit]
Description=Kubernetes Scheduler
Documentation=https://github.com/kubernetes/kubernetes

[Service]
ExecStart=/usr/local/bin/kube-scheduler \
  --config=/etc/kubernetes/config/kube-scheduler.yaml \
  --v=2
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

サービスの起動

systemctl daemon-reload
systemctl enable kube-apiserver kube-controller-manager kube-scheduler
systemctl start kube-apiserver kube-controller-manager kube-scheduler

ここで、問題が発生しました。

kube-apiserverが起動しない…

statusを確認すると以下のようなエラーログが確認できました。

run.go:72] "command failed" err="service IP family \"10.32.0.0/24\" must match public address family \"240b:10:9f60:8600:da3a:ddff:fe06:a179\""

上記を分からないなりに解釈すると

  • 起動コマンド内で、k8sのService IP family(10.32.0.0/24)とpublic address family(240b:10:9f60:8600:da3a:ddff:fe06:a179)が一致しない
    • Service IP familyはkube-apiserver.serviceで設定したServiceで利用するCIDRなので、こちらが正のものに見える

    • 後者は明らかにIPv6アドレスで設定した覚えはない

      • hostのネットワーク設定を確認すると、eht0に対してIPv6で上記のアドレスが設定されている

        2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
            link/ether d8:3a:dd:06:a1:79 brd ff:ff:ff:ff:ff:ff
            inet 192.168.0.101/24 brd 192.168.0.255 scope global eth0
               valid_lft forever preferred_lft forever
            inet6 **240b:10:9f60:8600:da3a:ddff:fe06:a179/64** scope global dynamic mngtmpaddr noprefixroute
               valid_lft 7836sec preferred_lft 6036sec
            inet6 fe80::da3a:ddff:fe06:a179/64 scope link
               valid_lft forever preferred_lft forever
        
    • IPv4とIPv6の設定が混在している

上記から、本来IPv4で設定しなければならない場所に設定がされておらず、IPv6が優先されて表立ったことで設定が衝突していそう。

関連の設定をした箇所はkube-apiserver.service くらいなので見直したところ、—dvertise-sddressオプションにてノードIPを指定していないことを発見。

設定を行うことで無事にエラーは解消し、サービス起動を確認できました。

コントロールプレーンの起動確認

kubectl cluster-info --kubeconfig admin.kubeconfig
>Kubernetes control plane is running at https://127.0.0.1:6443

kubelet認可のためのRBACの設定

  • ClusterRoleの作成

    • Pod管理に関連する権限を付与したRoleの作成
    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRole
    metadata:
      annotations:
        rbac.authorization.kubernetes.io/autoupdate: "true"
      labels:
        kubernetes.io/bootstrapping: rbac-defaults
      name: system:kube-apiserver-to-kubelet
    rules:
      - apiGroups:
          - ""
        resources:
          - nodes/proxy
          - nodes/stats
          - nodes/log
          - nodes/spec
          - nodes/metrics
        verbs:
          - "*"
    
  • 作成したRoleをkubernetesユーザに付与

    apiVersion: rbac.authorization.k8s.io/v1
    kind: ClusterRoleBinding
    metadata:
      name: system:kube-apiserver
      namespace: ""
    roleRef:
      apiGroup: rbac.authorization.k8s.io
      kind: ClusterRole
      name: system:kube-apiserver-to-kubelet
    subjects:
      - apiGroup: rbac.authorization.k8s.io
        kind: User
        name: kubernetes
    
  • 作業用PCから接続確認

curl --cacert ca.pem https://master01:6443/version
>{
>  "major": "1",
>  "minor": "32",
>  "gitVersion": "v1.32.3",
>  "gitCommit": "32cc146f75aad04beaaa245a7157eb35063a9f99",
>  "gitTreeState": "clean",
>  "buildDate": "2025-03-11T19:52:21Z",
>  "goVersion": "go1.23.6",
>  "compiler": "gc",
>  "platform": "linux/arm64"
>}

workerノードの設定

各種ファイルの配置

workerノードへ各種バイナリを転送する。

  • kubectl
  • kubelet
  • kube-proxy
  • containerd
  • cni-plugins

ライブラリのインストール

各workerノードで以下のライブラリをインストールする。

  • k8s上でのライブラリの役割
  • socat
    • 双方向のデータ転送をしてくれるコマンドラインツール
    • kubectl port-forward に必要らしい
  • conntrack
    • コネクションの追跡情報を取得できるコマンド
  • ipset
    • IPアドレスやポート番号を管理してくれる。
  • kmod
    • Linuxモジュールを管理するためのツールセット

swapの無効化

k8sではpodのメモリ使用率の保証が難しくなるため、swapの無効化が推奨されているため無効化する。

swapon --show # swapが無効化されているか確認, 出力がemptyであれば無効化されている
swapoff -a # swapの無効化

各種ファイルを配置

  • 配置先ディレクトリの作成
sudo mkdir -p \
	/etc/cni/net.d \
	/opt/cni/bin \
	/etc/containerd \
	/var/lib/kubelet \
	/var/lib/kube-proxy \
	/var/lib/kubernetes \
	/var/run/kubernetes
  • バイナリファイルの配置
mv crictl kube-proxy kubelet runc /usr/local/bin/
mv containerd containerd-shim-runc-v2 containerd-stress /bin/
mv cni-plugins/* /opt/cni/bin/

CNIの設定

  • bridgeネットワークの設定ファイルを作成

    • /etc/cni/net.d/10-bridge.conf

      {
        "cniVersion": "1.0.0",
        "name": "bridge",
        "type": "bridge",
        "bridge": "cni0",
        "isGateway": true,
        "ipMasq": true,
        "ipam": {
          "type": "host-local",
          "ranges": [
            [{"subnet": <各workerノードのk8sネットワークCIDR>}]
          ],
          "routes": [{"dst": "0.0.0.0/0"}]
        }
      }
      
  • loopbackネットワークの設定ファイルを作成

    • /etc/cni/net.d/99-loopback.conf

      {
        "cniVersion": "1.1.0",
        "name": "lo",
        "type": "loopback"
      }
      
  • CNI bridgeネットワークを通過する通信がiptablesで処理されるようにするために、br-netfilterモジュールを読み込ませる

    # br-netfilter moduleの読みこみ
    modprobe br-netfilter
    echo "br-netfilter" >> /etc/modules-load.d/modules.conf
    # 設定
    echo "net.bridge.bridge-nf-call-iptables = 1" >> /etc/sysctl.d/kubernetes.conf
    echo "net.bridge.bridge-nf-call-ip6tables = 1" >> /etc/sysctl.d/kubernetes.conf
    sysctl -p /etc/sysctl.d/kubernetes.conf
    

containerdの設定

  • 設定ファイルの作成

    • /etc/containerd/config.toml

      version = 2
      
      [plugins."io.containerd.grpc.v1.cri"]
        [plugins."io.containerd.grpc.v1.cri".containerd]
          snapshotter = "overlayfs"
          default_runtime_name = "runc"
        [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
          runtime_type = "io.containerd.runc.v2"
        [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
          SystemdCgroup = true
      [plugins."io.containerd.grpc.v1.cri".cni]
        bin_dir = "/opt/cni/bin"
        conf_dir = "/etc/cni/net.d"
      
  • unitファイルの作成

    • /etc/systemd/system/containerd.service

      [Unit]
      Description=containerd container runtime
      Documentation=https://containerd.io
      After=network.target
      
      [Service]
      ExecStartPre=/sbin/modprobe overlay
      ExecStart=/bin/containerd
      Restart=always
      RestartSec=5
      Delegate=yes
      KillMode=process
      OOMScoreAdjust=-999
      LimitNOFILE=1048576
      LimitNPROC=infinity
      LimitCORE=infinity
      
      [Install]
      WantedBy=multi-user.target
      

kubeletの設定

  • 証明書とkubeconfigの移動
    事前に作成、配置していた証明書とkubeconfigファイルを所定の位置に移動する。

    mv ca.pem /var/lib/kubelet/
    mv <hostname>.pem /var/lib/kubelet/kubelet.pem
    mv <hostname>-key.pem /var/lib/kubelet/kubelet-key.pem
    mv <hostname>.kubeconfig /var/lib/kubelet/kubeconfig
    
  • 設定ファイルの作成

    • /var/lib/kubelet/kubelet-config.yaml
      unitファイルでkubeletコマンドに読み込ませるため、マニフェスト形式で作成

      kind: KubeletConfiguration
      apiVersion: kubelet.config.k8s.io/v1beta1
      address: "0.0.0.0"
      authentication:
        anonymous:
          enabled: false
        webhook:
          enabled: true
        x509:
          clientCAFile: "/var/lib/kubelet/ca.pem"
      authorization:
        mode: Webhook
      cgroupDriver: systemd
      containerRuntimeEndpoint: "unix:///var/run/containerd/containerd.sock"
      enableServer: true
      failSwapOn: false
      maxPods: 16
      memorySwap:
        swapBehavior: NoSwap
      port: 10250
      clusterDomain: "cluster.local"
      resolvConf: "/etc/resolv.conf"
      registerNode: true
      runtimeRequestTimeout: "15m"
      tlsCertFile: "/var/lib/kubelet/kubelet.pem"
      tlsPrivateKeyFile: "/var/lib/kubelet/kubelet-key.pem"
      
  • unitファイルの作成

    • /etc/systemd/system/kubelet.service

      [Unit]
      Description=Kubernetes Kubelet
      Documentation=https://github.com/kubernetes/kubernetes
      After=containerd.service
      Requires=containerd.service
      
      [Service]
      ExecStart=/usr/local/bin/kubelet \
        --config=/var/lib/kubelet/kubelet-config.yaml \
        --kubeconfig=/var/lib/kubelet/kubeconfig \
        --v=2
      Restart=on-failure
      RestartSec=5
      
      [Install]
      WantedBy=multi-user.target
      

kube-proxyの設定

  • kubeconfigの移動
    事前に作成、配置していkubeconfigファイルを所定の位置に移動する。

    mv kube-proxy.kubeconfig /var/lib/kube-proxy/kubeconfig
    
  • 設定ファイルの作成

    • /var/lib/kube-proxy/kube-proxy-config.yaml
      unitファイルでkube-proxyコマンドに読み込ませるため、マニフェスト形式で作成

      kind: KubeProxyConfiguration
      apiVersion: kubeproxy.config.k8s.io/v1alpha1
      clientConnection:
        kubeconfig: "/var/lib/kube-proxy/kubeconfig"
      mode: "iptables"
      clusterCIDR: "10.200.0.0/16"
      
  • unitファイルの作成

    • /etc/systemd/system/kube-proxy.service

      [Unit]
      Description=Kubernetes Kube Proxy
      Documentation=https://github.com/kubernetes/kubernetes
      
      [Service]
      ExecStart=/usr/local/bin/kube-proxy \
        --config=/var/lib/kube-proxy/kube-proxy-config.yaml
      Restart=on-failure
      RestartSec=5
      
      [Install]
      WantedBy=multi-user.target
      

サービスの起動

systemctl daemon-reload
systemctl enable containerd kubelet kube-proxy
systemctl start containerd kubelet kube-proxy

ノードの登録を確認

masterノードにて以下のコマンドを実行し、workerノードが登録されていることを確認する。

kubectl get nodes --kubeconfig admin.kubeconfig
>No resources found

登録されていない。

  • kubeletの起動状況確認

    systemctl status kubelet
    

    以下が記録されていたので、証明書の作成がミスっていそう。

    csi_plugin.go:887] Failed to contact API server when waiting for CSINode publishing: Get "[https://master01.kubernetes.local:6443/apis/storage.k8s.io/v1/csinodes/worker01?resourceVersion](https://master01.kubernetes.local:6443/apis/storage.k8s.io/v1/csinodes/worker01?resourceVersion=0)=0": tls: failed to verify certificate: x509: certificate is valid for master01, worker01, worker02, kubernetes, kubernetes.default, kubernetes.default.svc, kubernetes.default.svc.cluster, kubernetes.svc.cluster.local, not master01.kubernetes.local
    

    kube-apiserverの証明書作成した際、SANにmaster01.kubernetes.localは入れていない。

    これか。

    証明書を作成し直して、読み込んでいるサービス(kube-apiserver, etcd)を再起動するとworkerノードが登録されました。

    sudo kubectl get nodes --kubeconfig admin.kubeconfig
    >NAME       STATUS   ROLES    AGE    VERSION
    >worker01   Ready    <none>   115s   v1.32.3
    

引き続き、worker02の登録を試みるが、またしても登録されませんでした。

  • kubeletの起動状況確認

    systemctl status kubelet
    

    以下が記録されていたので、またしても証明書の作成がミスっていそう。今度はkubeletのクライアント証明書のCNをworker01で作ってしまった模様。

     kubelet_node_status.go:113] "Unable to register node with API server, error getting existing node" err="nodes \"worker02\" is forbidden: User \"system:node:worker01\" cannot get resource \"nodes\" in API group \"\" at the cluster scope: node 'worker01' cannot read 'worker02', only its own Node object" node="worker02"
    

    csrを確認してみる。

    • worker02-csr.json

      {
          "CN": "system:node:worker01",
          "key": {
              "algo": "rsa",
              "size": 2048
          },
          "names": [
              {
                  "C": "JP",
                  "O": "system:nodes",
                  "ST": "Tokyo"
              }
          ]
      }
      

    やってますね。修正して証明書を再作成、その後kubeletを再起動したら無事に登録されました。一安心。

    sudo kubectl get nodes --kubeconfig admin.kubeconfig
    >NAME       STATUS   ROLES    AGE   VERSION
    >worker01   Ready    <none>   32m   v1.32.3
    >worker02   Ready    <none>   26s   v1.32.3
    

クラスター外からマスターノードへのアクセスを構成

masterノード(master01.kuernetes.local)への名前解決は/etc/hostsによってできる前提。

  • 接続元端末にkubeconfigファイルを作成する。

    {
      kubectl config set-cluster kubernetes-the-hard-way \
        --certificate-authority=ca.pem\
        --embed-certs=true \
        --server=https://master01.kubernetes.local:6443
    
      kubectl config set-credentials admin \
        --client-certificate=admin.pem \
        --client-key=admin-key.pem
    
      kubectl config set-context kubernetes-the-hard-way \
        --cluster=kubernetes-the-hard-way \
        --user=admin
    
      kubectl config use-context kubernetes-the-hard-way
    }
    

    上記を実行することで、~/.kube/config が作成され、kubelet 実行時に読み込まれるようになる。

  • 接続確認
    kubectlを実行してmasterノードに接続してみる。

    kubectl version
    >Client Version: v1.32.3
    >Kustomize Version: v5.5.0
    >Server Version: v1.32.3
    
    kubectl get nodes
    >NAME       STATUS   ROLES    AGE   VERSION
    >worker01   Ready    <none>   21h   v1.32.3
    >worker02   Ready    <none>   20h   v1.32.3
    

Podのネットワークルートを構成

各workerノードに他のノードへのルーティング情報を設定する。

  • master01

    ip route add 10.10.1.0/24 via 192.168.0.151
    ip route add 10.10.2.0/24 via 192.168.0.152
    
  • worker01

    ip route add 10.10.2.0/24 via 192.168.0.152
    
  • worker02

    ip route add 10.10.1.0/24 via 192.168.0.151
    

スモークテスト

Deployment

Deploymentを作成してPodが作られることを確認してみる。

kubectl create deployment nginx --image=nginx:latest
>deployment.apps/nginx created
kubectl get pods -l app=nginx
>NAME                     READY   STATUS    RESTARTS   AGE
>nginx-54c98b4f84-84fxh   1/1     Running   0          19s

問題なさそう。

port-forwardingを設定してPodへアクセスしてみる。

ターミナルを2つ開いて、片方で、

POD_NAME=$(kubectl get pods -l app=nginx -o jsonpath="{.items[0].metadata.name}")
kubectl port-forward $POD_NAME 8080:80
>Forwarding from 127.0.0.1:8080 -> 80
>Forwarding from [::1]:8080 -> 80

もう片方で、

curl --head http://127.0.0.1:8080
>HTTP/1.1 200 OK
>Server: nginx/1.27.5
>Date: Sun, 08 Jun 2025 15:16:08 GMT
>Content-Type: text/html
>Content-Length: 615
>Last-Modified: Wed, 16 Apr 2025 12:01:11 GMT
>Connection: keep-alive
>ETag: "67ff9c07-267"
>Accept-Ranges: bytes

ちゃんとリクエスト取得できているので問題なく構築できたようです。

最後に

自分で構築することでk8sがどういった要素で構成されているか全体像を見ることができました。一方で、各要素の深堀りやk8sのエコシステムの理解はまだまだ不足しているため、今後は1つ1つを細かく見ていけたらと思います。

また、今回実施したhard-wayを各章ごとに分割して深堀りもしたいと思います。

参考

Discussion