⛴️

ラズパイで理解するkubernetes-the-hard-way

2024/01/01に公開

本記事の内容

Kubernetesの有名な学習リソースの一つにkubernetes the hard wayというものがあります。それはkubeadmやマネージドKubernetesサービスを使用せず、手作業でKubernetesクラスタを作成していき、Kubernetesを学ぶというコンテンツです。
kubernetes the hard wayではクラスタをGCPのCompute Engineで構築するのですが、本記事ではRaspberry piでクラスタ構築していきます。

概ねCyberAgentさんのリポジトリを参考にしましたが、以下の点でオリジナリティを加えています。

  • CyberAgentさんのリポジトリは2020年に作成されており少し古い情報があるため、その内容を更新している
  • kubernetes the hard wayでは、実施する各ステップについての説明が乏しいので、適宜補足や参照URLを加えて理解を促すようにしている
  • 学習目的であるため、Premature Optimizationにならないように意識している。

本記事で使用している環境

ホスト: Raspberry Pi 4 ModelB/8GB
OS: Ubuntu Server 22.04.3 LTS
Kubernetes: v1.29.0

Kubernetesの概要がわからない人向け

Kubernetesの概要を理解するために役立つ情報源をまとめた記事を以前に書きましたので、それを参考に学習をしてみてください。全くわからない状態で行うより、ある程度わかる状態で行う方が学習効果があると思います。
https://zenn.dev/num/articles/kuberletes-learning-resources

作成するKubernetesクラスタ(物理)

こちらが作成していくKubernetesクラスタです。結構気に入っていて、愛着が湧いてきました。

使用した機材

機材名 URL
Raspberry Pi 4 ModelB/8GB 3 https://raspberry-pi.ksyic.com/main/index/pdp.id/552/pdp.open/552
Raspberry Pi PoE+ HAT 3 https://raspberry-pi.ksyic.com/main/index/pdp.id/719/pdp.open/719
microSD 64GB 3 https://www.amazon.co.jp/dp/B08PTQMZY3
PoE Switch 1 https://www.amazon.co.jp/dp/B0763TGBTS
LANケーブル 3m 1 https://www.amazon.co.jp/dp/B001BY5BVW
LANケーブル 15cm 3 https://www.amazon.co.jp/dp/B00FZTNJQI/
microHDMI-HDMI変換ケーブル 1 https://www.amazon.co.jp/dp/B07QBZ667V
ケース 1 https://www.amazon.co.jp/dp/B07TJ15YL1
M2.5ネジ 10mm 12 https://www.amazon.co.jp/gp/product/B01MY8E4FF

その他モニタ, キーボードを使用しました。
PoE+ HATは給電をLANケーブルで行なうことで充電ケーブルを不要にし、配線をスッキリさせる機器なので、必須ではないです。

物理構築手順

CyberAgentさんのリポジトリを見ながら作成しました。
https://github.com/CyberAgentHack/home-kubernetes-2020/tree/master/how-to-create-cluster-physical

Kubernetesクラスタ構築手順

ここからkubernetes the hard wayを行なっていきます。クラスタの構成はマスターノード1台、ワーカーノード2台の計3台で行います。

大まかな手順

  • 各コンポーネント通信のための証明書の作成
  • マスターノード
    • etcd, kube-apiserver, kube-controller-manager, kube-schedulerの設定
  • ワーカーノード
    • kubelet, コンテナランタイム, cniプラグインの設定
  • デプロイの確認

準備

これからの説明は以下のような設定であることを前提に構築していきます。異なる点がある場合は各自の環境に合わせてください。

ip hostname 役割
192.168.0.5 master1 master node
192.168.0.7 worker1 worker node
192.168.0.8 worker2 worker node

hostnameは、hostnamectlコマンドで設定することができます。

# 例
sudo hostnamectl set-hostname master1

  • Pod用サブネット
    • 10.10.0.0/16
      • worker1: 10.10.1.0/24
      • worker2: 10.10.2.0/24
  • ClusterIP用サブネット
    • 10.32.0.0/24

各ノードのipアドレスを固定したい場合は、 /etc/netplan/に新しくyamlファイルを追加する形で実現できます。
参考: 【Ubuntu】固定IPアドレスを割り当てる

証明書の作成

Kubernetesは様々なコンポーネントで構成されており、それらの通信にTLSを使用します。そのための証明書の作成を行います
このステップはmasterノードで行います。適宜ssh接続を利用して行なってください。

まず証明書作成に使用するツール群をインストールします。

sudo apt install golang-cfssl

そして、以下のシェルスクリプト(generate-cert.sh)を実行します。

generate-cert.shを見る
#!/bin/bash

mkdir cert && cd cert
if [[ $? != 0 ]]; then
  exit
fi

echo -n "Hostname of Node1: "
read NODE1_HOSTNAME

echo -n "Hostname of Node2: "
read NODE2_HOSTNAME

echo -n "Hostname of Node3: "
read NODE3_HOSTNAME

echo -n "Addresses of Node1 (x.x.x.x[,x.x.x.x]): "
read NODE1_ADDRESS

echo -n "Addresses of Node2 (x.x.x.x[,x.x.x.x]): "
read NODE2_ADDRESS

echo -n "Addresses of Node3 (x.x.x.x[,x.x.x.x]): "
read NODE3_ADDRESS

echo -n "Address of Kubernetes ClusterIP (first address of ClusterIP subnet): "
read KUBERNETES_SVC_ADDRESS

cat > ca-config.json <<EOF
{
  "signing": {
    "default": {
      "expiry": "8760h"  # デフォルトの証明書の有効期間(1年)
    },
    "profiles": {
      "kubernetes": {
        "usages": ["signing", "key encipherment", "server auth", "client auth"],
        "expiry": "8760h" # Kubernetesプロファイルの証明書有効期間
      }
    }
  }
}
EOF

cat > ca-csr.json <<EOF
{
  "CN": "Kubernetes",  # 一般名(Common Name)
  "key": {
    "algo": "rsa",  # 鍵のアルゴリズム(RSA)
    "size": 2048  # 鍵のサイズ(2048ビット)
  },
  "names": [
    {
      "C": "JP",  # 国(Country)
      "L": "Setagaya",  # 地域(Locality)
      "O": "Kubernetes",  # 組織名(Organization)
      "OU": "CA",  # 組織単位名(Organizational Unit)
      "ST": "Chiba"  # 州・県(State)
    }
  ]
}
EOF

echo "---> Generate CA certificate"
cfssl gencert -initca ca-csr.json | cfssljson -bare ca


cat > admin-csr.json <<EOF
{
  "CN": "admin",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "JP",
      "L": "Setagaya",
      "O": "system:masters",
      "OU": "Kubernetes The Hard Way",
      "ST": "Chiba"
    }
  ]
}
EOF

echo "---> Generate certificate for admin user"
cfssl gencert \
  -ca=ca.pem \                        # CAの証明書ファイル
  -ca-key=ca-key.pem \                # CAの秘密鍵ファイル
  -config=ca-config.json \            # CAの設定ファイル
  -profile=kubernetes \               # 使用するプロファイル(kubernetes)
  admin-csr.json | cfssljson -bare admin  # 管理者用CSRファイルと出力ファイルの指定


for instance in ${NODE1_HOSTNAME} ${NODE2_HOSTNAME} ${NODE3_HOSTNAME}; do
cat > ${instance}-csr.json <<EOF
{
  "CN": "system:node:${instance}",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "JP",
      "L": "Setagaya",
      "O": "system:nodes",
      "OU": "Kubernetes The Hard Way",
      "ST": "Chiba"
    }
  ]
}
EOF
done

echo "---> Generate certificate for kubelet"
cfssl gencert \
  -ca=ca.pem \                        # CAの証明書ファイル
  -ca-key=ca-key.pem \                # CAの秘密鍵ファイル
  -config=ca-config.json \            # CAの設定ファイル
  -hostname=${NODE1_HOSTNAME},${NODE1_ADDRESS} \  # 証明書に含めるホスト名とIPアドレス
  -profile=kubernetes \               # 使用するプロファイル(kubernetes)
  ${NODE1_HOSTNAME}-csr.json | cfssljson -bare ${NODE1_HOSTNAME}  # NODE1のCSRファイルと出力ファイルの指定
cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -hostname=${NODE2_HOSTNAME},${NODE2_ADDRESS} \
  -profile=kubernetes \
  ${NODE2_HOSTNAME}-csr.json | cfssljson -bare ${NODE2_HOSTNAME}
cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -hostname=${NODE3_HOSTNAME},${NODE3_ADDRESS} \
  -profile=kubernetes \
  ${NODE3_HOSTNAME}-csr.json | cfssljson -bare ${NODE3_HOSTNAME}


cat > kube-controller-manager-csr.json <<EOF
{
  "CN": "system:kube-controller-manager",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "JP",
      "L": "Setagaya",
      "O": "system:kube-controller-manager",
      "OU": "Kubernetes The Hard Way",
      "ST": "Tokyo"
    }
  ]
}
EOF

echo "---> Generate certificate for kube-controller-manager"
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


cat > kube-proxy-csr.json <<EOF
{
  "CN": "system:kube-proxy",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "JP",
      "L": "Setagaya",
      "O": "system:node-proxier",
      "OU": "Kubernetes The Hard Way",
      "ST": "Tokyo"
    }
  ]
}
EOF

echo "---> Generate certificate for kube-proxy"
cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -profile=kubernetes \
  kube-proxy-csr.json | cfssljson -bare kube-proxy


cat > kube-scheduler-csr.json <<EOF
{
  "CN": "system:kube-scheduler",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "JP",
      "L": "Setagaya",
      "O": "system:kube-scheduler",
      "OU": "Kubernetes The Hard Way",
      "ST": "Tokyo"
    }
  ]
}
EOF

echo "---> Generate certificate for kube-scheduler"
cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -profile=kubernetes \
  kube-scheduler-csr.json | cfssljson -bare kube-scheduler


KUBERNETES_HOSTNAMES=kubernetes,kubernetes.default,kubernetes.default.svc,kubernetes.default.svc.cluster,kubernetes.default.svc.cluster.local

cat > kubernetes-csr.json <<EOF
{
  "CN": "kubernetes",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "JP",
      "L": "Setagaya",
      "O": "Kubernetes",
      "OU": "Kubernetes The Hard Way",
      "ST": "Tokyo"
    }
  ]
}
EOF

echo "---> Generate certificate for kube-api-server"
cfssl gencert \
  -ca=ca.pem \                        # CAの証明書ファイル
  -ca-key=ca-key.pem \                # CAの秘密鍵ファイル
  -config=ca-config.json \            # CAの設定ファイル
  -hostname=${KUBERNETES_SVC_ADDRESS},${NODE1_ADDRESS},${NODE2_ADDRESS},${NODE3_ADDRESS},127.0.0.1,${KUBERNETES_HOSTNAMES} \  # 証明書に関連付けるホスト名とIPアドレス
  -profile=kubernetes \               # 使用するプロファイル(kubernetes)
  kubernetes-csr.json | cfssljson -bare kubernetes  # Kubernetes APIサーバー用CSRファイルと出力ファイルの指定



cat > service-account-csr.json <<EOF
{
  "CN": "service-accounts",
  "key": {
    "algo": "rsa",
    "size": 2048
  },
  "names": [
    {
      "C": "JP",
      "L": "Setagaya",
      "O": "Kubernetes",
      "OU": "Kubernetes The Hard Way",
      "ST": "Tokyo"
    }
  ]
}
EOF

echo "---> Generate certificate for generating token of ServiceAccount"
cfssl gencert \
  -ca=ca.pem \
  -ca-key=ca-key.pem \
  -config=ca-config.json \
  -profile=kubernetes \
  service-account-csr.json | cfssljson -bare service-account


echo "---> Complete to generate certificate"

# 実行例
./generate-cert.sh
# Hostname of Node1: master1
# Hostname of Node2: worker1
# Hostname of Node3: worker2
# Addresses of Node1 (x.x.x.x[,x.x.x.x]): 192.168.0.5
# Addresses of Node2 (x.x.x.x[,x.x.x.x]): 192.168.0.7
# Addresses of Node3 (x.x.x.x[,x.x.x.x]): 192.168.0.8
# Address of Kubernetes ClusterIP (first address of ClusterIP subnet): 10.32.0.1
# ...
# ...
# ---> Complete to generate certificate

作成したca.pemと各hostnameに対応した<hostname>.pem<hostname>-key.pemはそれぞれのホストにscpコマンドで送ります。

# 例
scp ca.pem worker1.pem worker1-key.pem ubuntu@192.168.0.7:~/

これらはkubeletがkube-apiserverを認証したり、kube-apiserverがkubeletを認証するために使います。残りの証明書はmaster1で使用します。

kubeconfigの作成

各コンポーネント(kubelet, kube-proxy, kube-controller-manager, kube-scheduler, kubectlコマンド)が、kube-apiserverと通信するための設定ファイルを作成します。

設定ファイルを作成するためにkubectlコマンドを使用するので、まずはkubectlのバイナリをインストールします。

curl -LO https://dl.k8s.io/release/v1.29.0/bin/linux/arm64/kubectl
chmod +x kubectl
sudo mv kubectl /usr/local/bin # kubectlのパスが通るようにする

そして以下の設定ファイル作成用シェルスクリプトをgenerate-kubeconfig.shとして作成し、実行権限を与えて実行します。内容は、各コンポーネント用の証明書情報をkubeconfigに組み込み、kube-apiserverのアドレスを指定するというものです。

generate-kubeconfig.shを見る
#!/bin/bash

ls cert >/dev/null 2>&1
if [[ $? != 0 ]]; then
  echo "Please run in the same directory as cert" 
  exit
fi

mkdir kubeconfig && cd kubeconfig
if [[ $? != 0 ]]; then
  exit
fi

CERT_DIR="../cert"

echo -n "Hostname of Node1: "
read NODE1_HOSTNAME

echo -n "Hostname of Node2: "
read NODE2_HOSTNAME

echo -n "Hostname of Node3: "
read NODE3_HOSTNAME

echo -n "Address of Master Node: "
read MASTER_ADDRESS

echo "---> Generate kubelet kubeconfig"
for instance in ${NODE1_HOSTNAME} ${NODE2_HOSTNAME} ${NODE3_HOSTNAME}; do
  kubectl config set-cluster kubernetes-the-hard-way \
    --certificate-authority=${CERT_DIR}/ca.pem \  # CAの証明書ファイル
    --embed-certs=true \                          # 証明書をkubeconfigに埋め込む
    --server=https://${MASTER_ADDRESS}:6443 \     # Kubernetes APIサーバのアドレス
    --kubeconfig=${instance}.kubeconfig

  kubectl config set-credentials system:node:${instance} \
    --client-certificate=${CERT_DIR}/${instance}.pem \  # クライアント証明書ファイル
    --client-key=${CERT_DIR}/${instance}-key.pem \      # クライアント秘密鍵ファイル
    --embed-certs=true \                                # 証明書をkubeconfigに埋め込む
    --kubeconfig=${instance}.kubeconfig

  kubectl config set-context default \
    --cluster=kubernetes-the-hard-way \
    --user=system:node:${instance} \
    --kubeconfig=${instance}.kubeconfig

  kubectl config use-context default --kubeconfig=${instance}.kubeconfig
done


echo "---> Generate kube-proxy kubeconfig"
kubectl config set-cluster kubernetes-the-hard-way \
  --certificate-authority=${CERT_DIR}/ca.pem \
  --embed-certs=true \
  --server=https://${MASTER_ADDRESS}:6443 \
  --kubeconfig=kube-proxy.kubeconfig

kubectl config set-credentials system:kube-proxy \
  --client-certificate=${CERT_DIR}/kube-proxy.pem \
  --client-key=${CERT_DIR}/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


echo "---> Generate kube-controller-manager kubeconfig"
kubectl config set-cluster kubernetes-the-hard-way \
  --certificate-authority=${CERT_DIR}/ca.pem \
  --embed-certs=true \
  --server=https://127.0.0.1:6443 \
  --kubeconfig=kube-controller-manager.kubeconfig

kubectl config set-credentials system:kube-controller-manager \
  --client-certificate=${CERT_DIR}/kube-controller-manager.pem \
  --client-key=${CERT_DIR}/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


echo "---> Generate kube-scheduler kubeconfig"
kubectl config set-cluster kubernetes-the-hard-way \
  --certificate-authority=${CERT_DIR}/ca.pem \
  --embed-certs=true \
  --server=https://127.0.0.1:6443 \
  --kubeconfig=kube-scheduler.kubeconfig

kubectl config set-credentials system:kube-scheduler \
  --client-certificate=${CERT_DIR}/kube-scheduler.pem \
  --client-key=${CERT_DIR}/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


echo "---> Generate admin user kubeconfig"
kubectl config set-cluster kubernetes-the-hard-way \
  --certificate-authority=${CERT_DIR}/ca.pem \
  --embed-certs=true \
  --server=https://127.0.0.1:6443 \
  --kubeconfig=admin.kubeconfig

kubectl config set-credentials admin \
  --client-certificate=${CERT_DIR}/admin.pem \
  --client-key=${CERT_DIR}/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

echo "---> Complete to generate kubeconfig"
chmod +x generate-kubeconfig.sh
./generate-kubeconfig.sh
# Hostname of Node1: master1
# Hostname of Node2: worker1
# Hostname of Node3: worker2
# Address of Master Node: 192.168.0.5
# ...
# ...
# ---> Complete to generate kubeconfig

作成した<hostname>.kubeconfigkube-proxy.kubeconfigはそれぞれのホストにscpで送ります。

etcdのデプロイ(Master)

このステップはマスターノードのみで行います。
まずetcdのバイナリをインストールしてパスが通るようにします。

curl -LO https://github.com/etcd-io/etcd/releases/download/v3.5.11/etcd-v3.5.11-linux-arm64.tar.gz
tar -xvf etcd-v3.5.11-linux-arm64.tar.gz
sudo mv etcd-v3.5.11-linux-arm64/etcd* /usr/local/bin/

etcd用の設定ファイルや証明書の配置を行うディレクトリを作成して配置します。これらのパスは後でsystemd用のユニットファイルを作成する際に指定します。

sudo mkdir -p /etc/etcd /var/lib/etcd
sudo chmod 700 /var/lib/etcd
sudo cp ca.pem kubernetes-key.pem kubernetes.pem /etc/etcd/

etcdを動かすためのsystemd用のユニットファイルを作成します。ETCD_NAMEは、マスターノードが複数台ある場合に各マスターノードのetcdが同期するときに使用する名前です。私はmaster1としました。

ETCD_NAME="<etcd_name>"
INTERNAL_IP="<master_ip>"

cat <<EOF | sudo tee /etc/systemd/system/etcd.service
[Unit]
Description=etcd
Documentation=https://github.com/coreos

[Service]
Type=notify
ExecStart=/usr/local/bin/etcd \\
  --name ${ETCD_NAME} \\  # このetcdインスタンスの名前
  --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 \\  # CA証明書ファイル
  --peer-trusted-ca-file=/etc/etcd/ca.pem \\  # ピア通信用のCA証明書ファイル
  --peer-client-cert-auth \\  # ピア通信の証明書認証を要求
  --client-cert-auth \\  # クライアント証明書認証を有効化
  --initial-advertise-peer-urls https://${INTERNAL_IP}:2380 \\  # 初期ピアURLの公開先を指定
  --listen-peer-urls https://${INTERNAL_IP}:2380 \\  # ピア通信のリッスンURL
  --listen-client-urls https://${INTERNAL_IP}:2379,https://127.0.0.1:2379 \\  # クライアント通信のリッスンURL
  --advertise-client-urls https://${INTERNAL_IP}:2379 \\  # クライアントURLの公開先を指定
  --initial-cluster-token etcd-initial-token \\  # クラスタトークン
  --initial-cluster ${ETCD_NAME}=https://${INTERNAL_IP}:2380 \\  # 初期クラスタ構成
  --initial-cluster-state new \\  # クラスタの初期状態
  --data-dir=/var/lib/etcd  # データディレクトリ
Restart=on-failure
RestartSec=5
Environment=ETCD_UNSUPPORTED_ARCH=arm64

[Install]
WantedBy=multi-user.target
EOF

ユニットファイルで記述されている各オプションについての説明や、その他オプションについてはetcd公式ドキュメントから確認できます。
これをset-etcd.shとして作成し、実行権限を与え実行します。

chmod +x set-etcd.sh
./set-etcd.sh

次にetcdをデーモンとして動くように以下のコマンドを実行します

sudo systemctl daemon-reload
sudo systemctl start etcd

以下のコマンドを実行して、Active: activeになっていればOKです

systemctl status etcd
#● etcd.service - etcd
#     Loaded: loaded (/etc/systemd/system/etcd.service; disabled; vendor preset: enabled)
#     Active: active (running) since Sun 2023-12-31 08:26:28 UTC; 2s ago

kube-apiserverのデプロイ(Master)

このステップもマスターノードのみで行います。
まずkube-apiserverのバイナリをインストールしてパスが通るようにします。

curl -LO curl -LO https://dl.k8s.io/release/v1.29.0/bin/linux/arm64/kube-apiserver
chmod +x kube-apiserver
sudo mv kube-apiserver /usr/local/bin/

次にetcdのデータを暗号化する機能のための設定ファイルを作成します。このステップは必須ではありませんが、必須レベルなので行います。
encryption configurationに関するドキュメントはこちらです
https://kubernetes.io/docs/tasks/administer-cluster/encrypt-data/

# 32バイトのランダムデータを生成し、Base64エンコードして暗号化キーを作成
ENCRYPTION_KEY=$(head -c 32 /dev/urandom | base64)

# 暗号化設定ファイルを作成
cat > encryption-config.yaml <<EOF
kind: EncryptionConfiguration
apiVersion: apiserver.config.k8s.io/v1
resources:
  - resources:
      - secrets  # 暗号化対象のリソース(ここではsecrets)
    providers:
      - aescbc:  # AES-CBCモードでの暗号化を使用
          keys:
            - name: key1
              secret: ${ENCRYPTION_KEY}  # 生成した暗号化キー
      - identity: {}  # 後退プロバイダとしてidentityを指定
EOF

kube-apiserver用の設定ファイルや証明書の配置を行うディレクトリを作成して配置します。これらのパスは後でsystemd用のユニットファイルで指定します。
-aiオプションについては、証明書ファイルの様々な情報を保持しながらコピーするために使用します。

sudo mkdir -p /etc/kubernetes/config
sudo mkdir -p /var/lib/kubernetes/
sudo cp -ai ca.pem ca-key.pem kubernetes-key.pem kubernetes.pem \
  service-account-key.pem service-account.pem encryption-config.yaml /var/lib/kubernetes/

kube-apiserverをデーモンとして動かすためのユニットファイルを作成し、起動します。

INTERNAL_IP="<master_ip>"
CLUSTER_IP_NETWORK="<cluster_ip_network>"

cat <<EOF | sudo tee /etc/systemd/system/kube-apiserver.service
[Unit]
Description=Kubernetes API Server
Documentation=https://github.com/kubernetes/kubernetes

[Service]
ExecStart=/usr/local/bin/kube-apiserver \\
  --advertise-address=${INTERNAL_IP} \\  # クラスタ内で使用するAPIサーバーのIPアドレスを指定
  --allow-privileged=true \\  # 特権コンテナの使用を許可
  --apiserver-count=3 \\  # クラスタ内のAPIサーバの数
  --audit-log-maxage=30 \\  # 監査ログの保持日数
  --audit-log-maxbackup=3 \\  # 監査ログの最大バックアップ数
  --audit-log-maxsize=100 \\  # 監査ログファイルの最大サイズ(MB)
  --audit-log-path=/var/log/audit.log \\  # 監査ログの保存先
  --authorization-mode=Node,RBAC \\  # 認証モード(NodeとRBAC)
  --bind-address=0.0.0.0 \\  # APIサーバがリッスンするIPアドレス
  --client-ca-file=/var/lib/kubernetes/ca.pem \\  # クライアント証明書認証用のCA証明書ファイル
  --enable-admission-plugins=NamespaceLifecycle,NodeRestriction,LimitRanger,ServiceAccount,DefaultStorageClass,ResourceQuota \\  # 使用するアドミッションコントローラー
  --etcd-cafile=/var/lib/kubernetes/ca.pem \\  # etcdのCA証明書ファイル
  --etcd-certfile=/var/lib/kubernetes/kubernetes.pem \\  # etcdのクライアント証明書ファイル
  --etcd-keyfile=/var/lib/kubernetes/kubernetes-key.pem \\  # etcdのクライアント鍵ファイル
  --etcd-servers=https://${INTERNAL_IP}:2379 \\  # etcdサーバのURL
  --event-ttl=1h \\  # イベントのTTL
  --encryption-provider-config=/var/lib/kubernetes/encryption-config.yaml \\  # 暗号化プロバイダの設定ファイル
  --kubelet-certificate-authority=/var/lib/kubernetes/ca.pem \\  # kubeletの証明書認証用CA
  --kubelet-client-certificate=/var/lib/kubernetes/kubernetes.pem \\  # kubeletのクライアント証明書
  --kubelet-client-key=/var/lib/kubernetes/kubernetes-key.pem \\  # kubeletのクライアント鍵
  --kubelet-https=true \\  # kubeletとの通信にHTTPSを使用
  --runtime-config='api/all=true' \\  # APIのランタイム設定
  --service-account-key-file=/var/lib/kubernetes/service-account.pem \\  # サービスアカウントの鍵ファイル
  --service-cluster-ip-range=${CLUSTER_IP_NETWORK} \\  # サービスクラスタのIP範囲
  --service-node-port-range=30000-32767 \\  # ノードポートの範囲
  --tls-cert-file=/var/lib/kubernetes/kubernetes.pem \\  # TLS証明書ファイル
  --tls-private-key-file=/var/lib/kubernetes/kubernetes-key.pem \\  # TLS秘密鍵ファイル
  --v=2  # ロギングの詳細度レベル
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl start kube-apiserver

kube-apiserverの実行オプションについてはこちらの公式ドキュメントから確認できます
https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/

kube-controller-managerのデプロイ(Master)

これもマスターノードのみで行います。
まずkube-controller-managerのバイナリをインストールしてパスが通るようにします。

curl -LO curl -LO https://dl.k8s.io/release/v1.29.0/bin/linux/arm64/kube-controller-manager

chmod +x kube-controller-manager
sudo mv kube-controller-manager /usr/local/bin/

kubeconfigの配置を行います

sudo cp -ai kube-controller-manager.kubeconfig /var/lib/kubernetes/

kube-controller-managerをデーモンとして動かすためのユニットファイルを作成し、起動します。
私はpod_network10.10.0.0/16, cluster_ip_network10.32.0.0/24を指定しました。

POD_NETWORK="<pod_network>"
CLUSTER_IP_NETWORK="<cluster_ip_network>"

cat <<EOF | sudo tee /etc/systemd/system/kube-controller-manager.service
[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 \\  # コントローラーマネージャがリッスンするIPアドレス
  --cluster-cidr=${POD_NETWORK} \\  # PodネットワークのCIDR範囲
  --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 \\  # kubeconfigファイルのパス
  --leader-elect=true \\  # リーダー選出を有効化
  --root-ca-file=/var/lib/kubernetes/ca.pem \\  # ルート証明書ファイルのパス
  --service-account-private-key-file=/var/lib/kubernetes/service-account-key.pem \\  # サービスアカウントの秘密鍵ファイルのパス
  --service-cluster-ip-range=${CLUSTER_IP_NETWORK} \\  # サービスクラスタIP範囲
  --use-service-account-credentials=true \\  # サービスアカウントの認証情報を使用
  --v=2  # ロギングの詳細度レベル
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl start kube-controller-manager

kube-controller-managerについてのオプションについてはこちらの公式ドキュメントから確認できます。
https://kubernetes.io/docs/reference/command-line-tools-reference/kube-controller-manager/

kube-schedulerのデプロイ(Master)

これもマスターノードのみで行います。
まずkube-schedulerのバイナリをインストールしてパスが通るようにします。

curl -LO curl -LO https://dl.k8s.io/release/v1.29.0/bin/linux/arm64/kube-scheduler

chmod +x kube-scheduler
sudo mv kube-scheduler /usr/local/bin/

kubeconfigの配置を行います。

sudo cp -ai kube-scheduler.kubeconfig /var/lib/kubernetes/

kube-schedulerをデーモンとして動かすためのユニットファイルを作成し、起動します。

cat <<EOF | sudo tee /etc/kubernetes/config/kube-scheduler.yaml
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
clientConnection:
  kubeconfig: "/var/lib/kubernetes/kube-scheduler.kubeconfig"  # Kube-Scheduler用のkubeconfigファイルのパス
leaderElection:
  leaderElect: true  # リーダー選出を有効化
EOF

cat <<EOF | sudo tee /etc/systemd/system/kube-scheduler.service
[Unit]
Description=Kubernetes Scheduler
Documentation=https://github.com/kubernetes/kubernetes

[Service]
ExecStart=/usr/local/bin/kube-scheduler \\  # Kube-Schedulerの実行コマンド
  --config=/etc/kubernetes/config/kube-scheduler.yaml \\  # 設定ファイルのパス
  --v=2  # ログの詳細レベル
Restart=on-failure  # 失敗時に再起動
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl start kube-scheduler

kube-schedulerの実行オプションについてはこちらの公式ドキュメントから確認できます。
https://kubernetes.io/docs/reference/command-line-tools-reference/kube-scheduler/

Masterの動作チェック

Masterの各コンポーネント(scheduler, controller-manager, etcd)が動いているかを確認します。

# 実行例(成功)
kubectl get --raw='/readyz?verbose'
# [+]ping ok
# [+]log ok
# [+]etcd ok
# [+]poststarthook/start-kube-apiserver-admission-initializer ok
# [+]poststarthook/generic-apiserver-start-informers ok
# [+]poststarthook/start-apiextensions-informers ok
# [+]poststarthook/start-apiextensions-controllers ok
# [+]poststarthook/crd-informer-synced ok
# [+]poststarthook/bootstrap-controller ok
# [+]poststarthook/rbac/bootstrap-roles ok
# [+]poststarthook/scheduling/bootstrap-system-priority-classes ok
# [+]poststarthook/start-cluster-authentication-info-controller ok
# [+]poststarthook/start-kube-aggregator-informers ok
# [+]poststarthook/apiservice-registration-controller ok
# [+]poststarthook/apiservice-status-available-controller ok
# [+]poststarthook/kube-apiserver-autoregistration ok
# [+]autoregister-completion ok
# [+]poststarthook/apiservice-openapi-controller ok

非推奨になったものですが、こちらでも一応確認できます

ubuntu@master1:~$ kubectl get componentstatuses --kubeconfig kubeconfig/admin.kubeconfig
# NAME                 STATUS    MESSAGE   ERROR
# scheduler            Healthy   ok
# controller-manager   Healthy   ok
# etcd-0               Healthy   ok

もし動いていない場合は、journalctl等のコマンドを実行し、エラーになっている箇所などを調べます。バージョンの変化により撤廃されたオプションをユニットファイルやconfigファイルで指定しているなどがエラーの原因になります。

journalctl -u <component> -r

Kubernetes Node構築の前準備(Node)

これからはワーカーノードで作業を行っていきます。
まずノードのcgroupのMemory Subsystemを有効化します。デフォルトでは無効化されているので有効にする必要があります。vim等を使用し、/boot/firmware/cmdline.txtに下記を追記して再起動します。

cgroup_memory=1 cgroup_enable=memory

Kubernetesの内部で使われているパッケージをインストールします。

sudo apt update
sudo apt -y install socat conntrack ipset

kubeletのデプロイ(Node)

kubelet用の設定ファイルや証明書の配置を行うディレクトリを作成して配置します。
これらのパスは後でsystemd用のユニットファイルで指定します。

sudo mkdir -p \
  /etc/cni/net.d \
  /opt/cni/bin \
  /var/lib/kubelet \
  /var/lib/kubernetes \
  /etc/containerd

sudo cp -ai ${HOSTNAME}-key.pem ${HOSTNAME}.pem /var/lib/kubelet/
sudo cp -ai ${HOSTNAME}.kubeconfig /var/lib/kubelet/kubeconfig
sudo cp -ai ca.pem /var/lib/kubernetes/

コンテナ作成に必要なツールやcniプラグイン, kubeletをインストールします。cniプラグインにはflannelcalicoではなく、最低限の機能を持ったプラグインを使用します。これはkubernetes the hard wayに従っています。

curl -LO https://dl.k8s.io/release/v1.29.0/bin/linux/arm64/kubelet
curl -LO https://github.com/kubernetes-sigs/cri-tools/releases/download/v1.29.0/crictl-v1.29.0-linux-arm64.tar.gz 
curl -LO https://github.com/containernetworking/plugins/releases/download/v1.4.0/cni-plugins-linux-arm64-v1.4.0.tgz

tar -xvf crictl-v1.29.0-linux-arm64.tar.gz
sudo tar -xvf cni-plugins-linux-arm64-v1.4.0.tgz -C /opt/cni/bin/
chmod +x crictl kubelet
sudo mv crictl kubelet /usr/local/bin/

sudo apt -y install containerd runc

cniプラグインを動作させるためにPodネットワークの設定をします
POD_CIDRは、先述の設定において、worker1であれば10.10.1.0/24, worker2であれば10.10.2.0/24を指定するものです。

# POD_CIDR変数を設定(ノード固有のPodネットワークCIDRを指定)
POD_CIDR=<node_pod_cidr>

# ブリッジネットワークのCNI設定ファイルを作成
cat <<EOF | sudo tee /etc/cni/net.d/10-bridge.conf
{
    "cniVersion": "0.3.1",  # CNIのバージョン
    "name": "bridge",       # ネットワーク名
    "type": "bridge",       # ネットワークタイプ(ブリッジ)
    "bridge": "cnio0",      # 使用するブリッジインターフェース名
    "isGateway": true,      # ゲートウェイとして機能
    "ipMasq": true,         # IPマスカレードを有効化(NAT)
    "ipam": {               # IPアドレス管理(IPAM)の設定
        "type": "host-local",  # ローカルホストでのIPアドレス管理
        "ranges": [
          [{"subnet": "${POD_CIDR}"}]  # Podに割り当てるIP範囲
        ],
        "routes": [{"dst": "0.0.0.0/0"}]  # ルーティング設定
    }
}
EOF

# ループバックネットワークのCNI設定ファイルを作成
cat <<EOF | sudo tee /etc/cni/net.d/99-loopback.conf
{
    "cniVersion": "0.3.1",  # CNIのバージョン
    "name": "lo",           # ネットワーク名(ループバック)
    "type": "loopback"      # ネットワークタイプ(ループバック)
}
EOF

kubeletをデーモンとして動かすためのユニットファイルを作成し、起動します

POD_CIDR=<node_pod_cidr>

cat <<EOF | sudo tee /var/lib/kubelet/kubelet-config.yaml
kind: KubeletConfiguration
apiVersion: kubelet.config.k8s.io/v1beta1
authentication:
  anonymous:
    enabled: false  # 匿名認証を無効化
  webhook:
    enabled: true  # Webhookによる認証を有効化
  x509:
    clientCAFile: "/var/lib/kubernetes/ca.pem"  # x509認証用のCA証明書
authorization:
  mode: Webhook  # 認可はWebhookを使用
clusterDomain: "cluster.local"  # クラスタのドメイン
clusterDNS:
  - "10.32.0.10"  # クラスタのDNSサーバアドレス
podCIDR: "${POD_CIDR}"  # このノードが使用するPodのCIDR
resolvConf: "/run/systemd/resolve/resolv.conf"  # DNS解決用の設定ファイル
runtimeRequestTimeout: "15m"  # ランタイムリクエストのタイムアウト時間
tlsCertFile: "/var/lib/kubelet/${HOSTNAME}.pem"  # TLS証明書ファイル
tlsPrivateKeyFile: "/var/lib/kubelet/${HOSTNAME}-key.pem"  # TLS秘密鍵ファイル

EOF


cat <<EOF | sudo tee /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 \\  # Kubeletの設定ファイル
  --container-runtime-endpoint=unix:///var/run/containerd/containerd.sock \\  # コンテナランタイムのエンドポイント
  --kubeconfig=/var/lib/kubelet/kubeconfig \\  # Kubeletのkubeconfigファイル
  --register-node=true \\  # ノードを自動でクラスタに登録
  --v=2  # ロギングの詳細度レベル
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

EOF

kubeletのconfigurationについては以下の公式ドキュメントから確認できます。
https://kubernetes.io/docs/reference/config-api/kubelet-config.v1beta1/
kubeletの実行オプションについては以下の公式ドキュメントから確認できます。
https://kubernetes.io/docs/reference/command-line-tools-reference/kubelet/

kube-proxyのデプロイ(Node)

kube-proxyのバイナリをインストールします。

curl -LO https://dl.k8s.io/release/v1.29.0/bin/linux/arm64/kube-proxy
chmod +x kube-proxy
sudo mv kube-proxy /usr/local/bin/

kube-proxy用の設定ファイルや証明書の配置を行うディレクトリを作成して配置します。

sudo mkdir -p /var/lib/kube-proxy
sudo mv kube-proxy.kubeconfig /var/lib/kube-proxy/kubeconfig

kube-proxyをデーモンとして動かすためのユニットファイルを作成し、起動します。

# POD_NETWORK変数を設定(PodネットワークのCIDRを指定)
POD_NETWORK="<pod_network>"

# Kube-Proxyの設定ファイルを作成
cat <<EOF | sudo tee /var/lib/kube-proxy/kube-proxy-config.yaml
kind: KubeProxyConfiguration
apiVersion: kubeproxy.config.k8s.io/v1alpha1
clientConnection:
  kubeconfig: "/var/lib/kube-proxy/kubeconfig"  # Kube-Proxy用のkubeconfigファイルのパス
mode: "iptables"  # ネットワークトラフィックを処理するモード(iptables)
clusterCIDR: "${POD_NETWORK}"  # クラスタ内のPodネットワークのCIDR
EOF

# Kube-Proxyのsystemdサービスユニットを作成
cat <<EOF | sudo tee /etc/systemd/system/kube-proxy.service
[Unit]
Description=Kubernetes Kube Proxy  # サービスの説明
Documentation=https://github.com/kubernetes/kubernetes

[Service]
ExecStart=/usr/local/bin/kube-proxy \\  # Kube-Proxyの実行コマンド
  --config=/var/lib/kube-proxy/kube-proxy-config.yaml  # 設定ファイルのパス
Restart=on-failure  # 失敗時に再起動
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

kube-proxyのconfigurationについては以下の公式ドキュメントから確認できます。
https://kubernetes.io/docs/reference/config-api/kube-proxy-config.v1alpha1/
kube-proxyの実行オプションについては以下の公式ドキュメントから確認できます。
https://kubernetes.io/docs/reference/command-line-tools-reference/kube-proxy/

ルーティング設定の追加

今回はiptablesを利用したPodネットワークであるため、Nodeは自身以外のPodネットワークを知りません。そこで他のNodeへのルーティング情報を与えてあげます。

# 特定のサブネットへのルーティングを設定
# master1において実行
# 10.10.1.0/24のサブネット宛のパケットは192.168.0.7を経由してルーティングされる
sudo ip route add 10.10.1.0/24 via 192.168.0.7 dev eth0

# 10.10.2.0/24のサブネット宛のパケットは192.168.0.8を経由してルーティングされる
sudo ip route add 10.10.2.0/24 via 192.168.0.8 dev eth0

# worker1において実行
sudo ip route add 10.10.2.0/24 via 192.168.08 dev eth0
# worker2において実行
sudo ip route add 10.10.1.0/24 via 192.168.07 dev eth0

Nodeの動作チェック

Nodeのコンポーネントをsystemctlコマンドで起動し終えたら、kube-apiserverに認識されているかどうかを確認します

ubuntu@master1:~$ k get node
# NAME      STATUS   ROLES    AGE     VERSION
# worker1   Ready    <none>   3d20h   v1.29.0
# worker2   Ready    <none>   3d19h   v1.29.0

このようになったら、各ノードがkubernetesクラスタに参加できているということになります。

nginxのデプロイチェック

Podが起動できることを確認します。試しにnginxをデプロイしてみます。

kubectl create deployment nginx --image=nginx
kubectl get pods
# NAME                    READY   STATUS    RESTARTS   AGE
# nginx-f89759699-t9vkd   1/1     Running   0          61s

ここでkubectlコマンドの実行がうまくいかないときは次のようにconfigファイルを直接指定してみてください

kubectl get pods --kubeconfig admin.kubeconfig

実際にアクセスできるかを確認します
ServiceリソースのNodePortをデプロイして、外部に公開できるようにします。

kubectl expose deployment nginx --type=NodePort --port=80
kubectl get service nginx
# NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)        AGE
# kubernetes   ClusterIP   10.32.0.1    <none>        443/TCP        44m
# nginx        NodePort    10.32.0.77   <none>        80:32105/TCP   33s

このように出力されたら、curlでアクセスしてみます。

curl http://192.168.0.7:32105

これでhtmlが取得できていれば成功です。これで最低限のKubernetesクラスタを構築することができました。

最後に

Kubernetesクラスタを構築することはできましたでしょうか。もしできないという方は、CyberAgentさんのリポジトリや、kubernetes-the-hard-way 日本語も参考にしてみてください。
本記事を読んで頂きありがとうございました。

参考にしたリンク集

https://github.com/kelseyhightower/kubernetes-the-hard-way

https://github.com/inductor/kubernetes-the-hard-way

https://github.com/CyberAgentHack/home-kubernetes-2020

https://developers.cyberagent.co.jp/blog/archives/27443/

Discussion