はじめに
KubernetesクラスタをVMware Workstationで構築する際の備忘録。ディプロイ環境はオンプレ想定で構成してみる。セットアップオプションの選択はRHEL/Red Hat寄りな感じですすめる。
詳細はこちらを参照。
利用するソフトウェア
- VMware Workstation 17 Pro (Windows 11 / X86_64)
 - RHEL 9.5 (VM)
 - Dnsmasq (2.79)
 - HA-Proxy (1.8.27)
 - Squid Cache (4.15)
 - Kubernetes (v1.32)
 - CRI-O (v1.32)
 - Calico (v3.29.3)
 - MetalLB (v0.14.9)
 - kubernetes-dashboard (7.12.0)
 - Helm (v3.17.3)
 - nginx (1.27.5)
 
※ RHEL 8系についてはKubernetesがサポートするカーネルバージョンかどうかを要確認
関連記事
ネットワーク構成
以下のようにコントロールプレーンノード(3台)およびワーカーノード(2台)の構成で構築する(後ほどワーカーノードを1台追加する)。
- 
ドメイン名: test.k8s.local
 - 
クラスタ名: test
 - 
Kubernetes API通信(コントロールプレーンノード)はHA-Proxyで負荷分散 (L4分散)
 
- アプリケーション通信(ワーカーノード)はMetalLB(L2モード)で冗長化
 
Kubernetesクラスタ
| ホスト名 (FQDN) | 種類 | OS | 備考 | 
|---|---|---|---|
| k8s-master0.test.k8s.local | コントロールプレーン | RHEL 9.5 | |
| k8s-master1.test.k8s.local | コントロールプレーン | RHEL 9.5 | |
| k8s-master2.test.k8s.local | コントロールプレーン | RHEL 9.5 | |
| k8s-worker0.test.k8s.local | ワーカーノード | RHEL 9.5 | |
| k8s-worker1.test.k8s.local | ワーカーノード | RHEL 9.5 | |
| k8s-worker2.test.k8s.local | ワーカーノード | RHEL 9.5 | 後で追加 | 
外部ノード
| ホスト名 (FQDN) | 種類 | OS | 備考 | 
|---|---|---|---|
| lb.test.k8s.local | ロードバランサー/DNS/DHCP | RHEL 9.5 | HA-Proxy / Dnsmasq | 
| proxy.test.k8s.local | プロキシ/デフォルトゲートウェイ | RHEL 9.5 | Squid Cache | 
| mng.test.k8s.local | 管理端末 | RHEL 9.5 | kubectl | 
アドレス設定
vmnet2 (VMware Workstation仮想ネットワーク )
| 設定 | 値 | 
|---|---|
| 名前 | vmnet2 | 
| 種別 | ホストオンリー | 
| ネットワークアドレス | 10.0.0.0/24 | 
| DHCP | 無効 | 
| ホスト仮想アダプタ | 接続する | 
lb.test.k8s.local
DNSサービス (Dnsmasq)
| サーバIP | ポート | 備考 | 
|---|---|---|
| 10.0.0.50 | 53/udp | 
DHCPサービス (Dnsmasq)
| サーバIP | アドレス範囲 | 備考 | 
|---|---|---|
| 10.0.0.50 | 10.0.0.100 - 10.0.0.250 | 
Load Balancerサービス (HA-Proxy)
(1) コントロールプレーンノードの仮想IP (Kubernetes API)
| 仮想IP | ポート | 分散先ノード | 備考 | 
|---|---|---|---|
| 10.0.0.51 | Kubernetes API用 | ||
| 6443/tcp | k8s-master0, k8s-master1, k8s-master2 | Kubernetes API Server | 
(2) NodePortタイプ構成: アプリケーションの仮想IP
| 仮想IP | ポート | 分散先ノード | 備考 | 
|---|---|---|---|
| 10.0.0.52 | |||
| 80/tcp | k8s-worker0, k8s-worker1 | HTTPアプリケーション | |
| 443/tcp | k8s-worker0, k8s-worker1 | HTTPSアプリケーション | 
proxy.test.k8s.local
Proxyサービス (Squid Cache)
| IP | ポート | 備考 | 
|---|---|---|
| 10.0.0.53 | 3128/tcp | HTTP/HTTPS | 
デフォルトゲートウェイ
| IP | 備考 | 
|---|---|
| 10.0.0.53 | NATルーター | 
Kubernetesノード
コントロールプレーンノード
本構成ではDHCPサービスによりStatic IPアドレスを割り当てる (OKDっぽく)。
| ホスト名 | IP | 備考 | 
|---|---|---|
| k8s-master0.test.k8s.local | 10.0.0.150 | |
| k8s-master1.test.k8s.local | 10.0.0.151 | |
| k8s-master2.test.k8s.local | 10.0.0.152 | 
ワーカーノード
本構成ではDHCPサービスによりStatic IPアドレスを割り当てる。
| ホスト名 | IP | 備考 | 
|---|---|---|
| k8s-worker0.test.k8s.local | 10.0.0.155 | |
| k8s-worker1.test.k8s.local | 10.0.0.156 | 
LoadBalancerタイプ構成 (MetalLB): アプリケーションの仮想IP
| 仮想IP | ポート | 分散先ノード | 備考 | 
|---|---|---|---|
| 10.0.0.52 | |||
| 80/tcp | k8s-worker0, k8s-worker1 | HTTPアプリケーション | |
| 443/tcp | k8s-worker0, k8s-worker1 | HTTPSアプリケーション | 
Kubernetesクラスタ
MetalLB
| 仮想VIPの割り当てアドレス範囲 | 備考 | 
|---|---|
| 10.0.0.60 - 10.0.0.70 | 
Loadbalancerタイプ構成 (MetalLB): アプリケーションの仮想IP
| ホスト名 | IP | 備考 | 
|---|---|---|
| apps.test.k8s.local | 10.0.0.60 | MetalLBによる割り当て | 
Podネットワークのアドレス範囲
| Pod CIDR | 備考 | 
|---|---|
| 172.16.0.0/12 | 
インフラ設定
proxy.test.k8s.local (コントロールプレーンノード)
$ sudo hostnamectl set-hostname proxy.test.k8s.local
$ sudo nmcli con add type ethernet ifname ens192 con-name k8s ipv4.address 10.0.0.53/24 ipv4.method manual
NATルーター (デフォルトゲートウェイ)
$ cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.ipv4.ip_forward = 1
EOF
$ sudo sysctl --system
$ sudo firewall-cmd --zone=public --add-interface=ens160 --permanent
$ sudo firewall-cmd --zone=public --add-masquerade --permanent
$ sudo firewall-cmd --zone=internal --add-interface=ens192 --permanent
$ sudo firewall-cmd --reload
Squid Cache (プロキシサービス)
$ sudo dnf update -y
$ sudo dnf install squid -y
$ sudo systemctl enable --now squid
$ sudo systemctl status squid
$ sudo firewall-cmd --permanent --add-port=3128/tcp
$ sudo firewall-cmd --reload
$ sudo firewall-cmd --list-ports
lb.test.k8s.local (コントロールプレーンノード)
$ sudo hostnamectl set-hostname lb.test.k8s.local
$ sudo nmcli con add type ethernet ifname ens192 con-name k8s ipv4.address 10.0.0.50/24 ipv4.method manual ipv4.gateway 10.0.0.53
$ sudo nmcli con modify k8s +ipv4.addresses 10.0.0.51/24
$ sudo nmcli con modify k8s +ipv4.addresses 10.0.0.52/24
$ sudo nmcli con up k8s
Dnsmasq (DNSサービス/DHCPサービス)
$ sudo dnf update -y
$ sudo dnf install -y dnsmasq
$ sudo vi /etc/dnsmasq.d/k8s.conf
domain=test.k8s.local
#
# DNS
#
server=8.8.8.8
address=/lb.test.k8s.local/10.0.0.50
address=/proxy.test.k8s.local/10.0.0.53
address=/api.test.k8s.local/10.0.0.51
address=/apps-via-haproxy.test.k8s.local/10.0.0.52
# VIP by MetalLB
address=/apps.test.k8s.local/10.0.0.60
address=/dashboard.test.k8s.local/10.0.0.62
address=/k8s-master0.test.k8s.local/10.0.0.150
address=/k8s-master1.test.k8s.local/10.0.0.151
address=/k8s-master2.test.k8s.local/10.0.0.152
address=/k8s-worker0.test.k8s.local/10.0.0.155
address=/k8s-worker1.test.k8s.local/10.0.0.156
address=/k8s-worker2.test.k8s.local/10.0.0.157
#
# DHCP
#
dhcp-range=10.0.0.100,10.0.0.250,255.255.255.0,12h
dhcp-option=option:router,10.0.0.53
dhcp-option=option:dns-server,10.0.0.50
dhcp-option=option:domain-name,test.k8s.local
dhcp-host=00:0c:29:13:d4:ac,mng,10.0.0.190
dhcp-host=00:50:56:3A:76:23,k8s-master0,10.0.0.150
dhcp-host=00:50:56:38:D6:6F,k8s-master1,10.0.0.151
dhcp-host=00:50:56:26:10:57,k8s-master2,10.0.0.152
dhcp-host=00:50:56:24:5C:22,k8s-worker0,10.0.0.155
dhcp-host=00:50:56:20:B8:67,k8s-worker1,10.0.0.156
dhcp-host=00:0C:29:64:A6:51,k8s-worker2,10.0.0.157
$ sudo firewall-cmd --permanent --add-service=dns
$ sudo firewall-cmd --permanent --add-service=dhcp
$ sudo firewall-cmd --reload
$ sudo firewall-cmd --list-services
$ sudo systemctl enable --now dnsmasq
$ sudo systemctl status dnsmasq
$ sudo nmcli connection modify k8s ipv4.dns "127.0.0.1"
$ sudo nmcli connection modify k8s ipv4.ignore-auto-dns yes
$ nslookup api.test.k8s.local
Server:         127.0.0.1
Address:        127.0.0.1#53
Name:   api.test.k8s.local
Address: 10.0.0.51
HA-Proxy (Load Balancerサービス)
$ sudo sed -i 's/^SELINUX=enforcing/SELINUX=permissive/' /etc/selinux/config
システム再起動
$ sudo dnf update -y
$ sudo dnf install -y haproxy
sudo vi /etc/haproxy/haproxy.cfg
とりあえずシンプルに以下で設定しておく。
- 分散方式: ラウンドロビン
 - プロトコル: TCP (L4)
 
#---------------------------------------------------------------------
# Example configuration for a possible web application.  See the
# full configuration options online.
#
#   https://www.haproxy.org/download/1.8/doc/configuration.txt
#
#---------------------------------------------------------------------
#---------------------------------------------------------------------
# Global settings
#---------------------------------------------------------------------
global
    # to have these messages end up in /var/log/haproxy.log you will
    # need to:
    #
    # 1) configure syslog to accept network log events.  This is done
    #    by adding the '-r' option to the SYSLOGD_OPTIONS in
    #    /etc/sysconfig/syslog
    #
    # 2) configure local2 events to go to the /var/log/haproxy.log
    #   file. A line like the following can be added to
    #   /etc/sysconfig/syslog
    #
    #    local2.*                       /var/log/haproxy.log
    #
    log         127.0.0.1 local2
    chroot      /var/lib/haproxy
    pidfile     /var/run/haproxy.pid
    maxconn     4000
    user        haproxy
    group       haproxy
    daemon
    # turn on stats unix socket
    stats socket /var/lib/haproxy/stats
    # utilize system-wide crypto-policies
    ssl-default-bind-ciphers PROFILE=SYSTEM
    ssl-default-server-ciphers PROFILE=SYSTEM
#---------------------------------------------------------------------
# common defaults that all the 'listen' and 'backend' sections will
# use if not designated in their block
#---------------------------------------------------------------------
defaults
    mode                    tcp
    log                     global
    option                  tcplog
    #option                  dontlognull
    #option http-server-close
    #option forwardfor       except 127.0.0.0/8
    #option                  redispatch
    retries                 3
    timeout http-request    10s
    timeout queue           1m
    timeout connect         10s
    timeout client          1m
    timeout server          1m
    timeout http-keep-alive 10s
    timeout check           10s
    maxconn                 3000
frontend api_frontend
    bind *:6443
    default_backend api_backend
backend api_backend
    balance roundrobin
    mode tcp
    option tcp-check
    server k8s-master0 k8s-master0.test.k8s.local:6443 check
    server k8s-master1 k8s-master1.test.k8s.local:6443 check
    server k8s-master2 k8s-master2.test.k8s.local:6443 check
frontend app_http
    bind 10.0.0.52:80
    default_backend app_http_backend
backend app_http_backend
    balance roundrobin
    mode tcp
    option tcp-check
    server k8s-worker0 k8s-worker0.test.k8s.local:30080 check
    server k8s-worker1 k8s-worker1.test.k8s.local:30080 check
    server k8s-worker2 k8s-worker2.test.k8s.local:30080 check
frontend app_https
    bind 10.0.0.52:443
    default_backend app_https_backend
backend app_https_backend
    balance roundrobin
    mode tcp
    option tcp-check
    server k8s-worker0 k8s-worker0.test.k8s.local:30443 check
    server k8s-worker1 k8s-worker1.test.k8s.local:30443 check
    server k8s-worker2 k8s-worker2.test.k8s.local:30443 chec
$ sudo systemctl enable --now haproxy
$ sudo systemctl status haproxy
$ sudo firewall-cmd --permanent --add-port=6443/tcp # Kubernetes API (コントロールプレーンノード)
$ sudo firewall-cmd --permanent --add-port=80/tcp
$ sudo firewall-cmd --permanent --add-port=443/tcp
$ sudo firewall-cmd --reload
$ sudo firewall-cmd --list-ports
k8s-master0/1/2.test.k8s.local (コントロールプレーンノード)
# DHCPサーバからホスト名を設定
$ sudo hostnamectl set-hostname ""
システム再起動
$ sudo sed -i '/ swap / s/^/#/' /etc/fstab
システム再起動
$ free
              total        used        free      shared  buff/cache   available
Mem:        1984984      279736     1538352       17112      166896     1538140
Swap:             0
とりあえず・・・
$ sudo sed -i 's/^SELINUX=enforcing/SELINUX=permissive/' /etc/selinux/config (無効化)
システム再起動
$ getenforce
Permissive
$ cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
システム再起動
$ sudo lsmod | grep br_netfilter
br_netfilter           28672  0
bridge                294912  1 br_netfilter
$ sudo lsmod | grep overlay
overlay               139264  0
デフォルトで以下になる様子
$ sudo sysctl -a |grep net.bridge.bridge-nf-call-
net.bridge.bridge-nf-call-arptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
$ cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.ipv4.ip_forward = 1
EOF
$ sudo sysctl --system
$ sudo sysctl -a |grep net.ipv4.ip_forward
net.ipv4.ip_forward = 1
$ sudo firewall-cmd --add-port=6443/tcp --permanent
$ sudo firewall-cmd --add-port=2379-2380/tcp --permanent
$ sudo firewall-cmd --add-port=10250/tcp --permanent
$ sudo firewall-cmd --add-port=10259/tcp --permanent
$ sudo firewall-cmd --add-port=10257/tcp --permanent
$ sudo firewall-cmd --reload
$ firewall-cmd --list-ports
またはとりあえず・・・
$ sudo systemctl disable --now firewalld (無効化)
$ systemctl status firewalld
k8s-worker0/1.test.k8s.local (ワーカーノード)
# DHCPサーバからホスト名を設定
$ sudo hostnamectl set-hostname ""
システム再起動
$ sudo sed -i '/ swap / s/^/#/' /etc/fstab
システム再起動
$ free
              total        used        free      shared  buff/cache   available
Mem:        1984984      279736     1538352       17112      166896     1538140
Swap:             0
とりあえず・・・
$ sudo sed -i 's/^SELINUX=enforcing/SELINUX=permissive/' /etc/selinux/config (無効化)
システム再起動
$ getenforce
Permissive
$ cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
システム再起動
$ sudo lsmod | grep br_netfilter
br_netfilter           28672  0
bridge                294912  1 br_netfilter
$ sudo lsmod | grep overlay
overlay               139264  0
デフォルトで以下になる様子
$ sudo sysctl -a |grep net.bridge.bridge-nf-call-
net.bridge.bridge-nf-call-arptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
$ cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.ipv4.ip_forward = 1
EOF
$ sudo sysctl --system
$ sudo sysctl -a |grep net.ipv4.ip_forward
net.ipv4.ip_forward = 1
$ sudo firewall-cmd --add-port=10250/tcp --permanent
$ sudo firewall-cmd --add-port=10256/tcp --permanent
$ sudo firewall-cmd --add-port=30000-32767/tcp --permanent
$ sudo firewall-cmd --reload
$ sudo firewall-cmd --list-ports
またはとりあえず・・・
$ sudo systemctl disable --now firewalld (無効化)
$ systemctl status firewalld
Kubernetesクラスタのセットアップ
k8s-master0/1/2.test.k8s.local (コントロールプレーンノード共通)
コンテナランタイムをインストール
※ Kubernetes(= 1.32)のバージョンと合わせる。もしCRI-Oバージョンに該当のものがまだない場合にはKubernetesのバージョンの方を合わせる。
$ export CRIO_VERSION=v1.32
$ cat <<EOF | sudo tee /etc/yum.repos.d/cri-o.repo
[cri-o]
name=CRI-O
baseurl=https://download.opensuse.org/repositories/isv:/cri-o:/stable:/$CRIO_VERSION/rpm/
enabled=1
gpgcheck=1
gpgkey=https://download.opensuse.org/repositories/isv:/cri-o:/stable:/$CRIO_VERSION/rpm/repodata/repomd.xml.key
EOF
$ sudo dnf install -y container-selinux
$ sudo dnf install -y cri-o
$ sudo dnf install -y podman
$ sudo systemctl enable --now crio
$ sudo systemctl status crio
$ ls -l /var/run/crio/crio.sock
srw-rw----. 1 root root 0 May  2 03:06 /var/run/crio/crio.sock
Kubernetesをインストール
$ export KUBERNETES_VERSION=v1.32
$ cat <<EOF | sudo tee /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://pkgs.k8s.io/core:/stable:/$KUBERNETES_VERSION/rpm/
enabled=1
gpgcheck=1
gpgkey=https://pkgs.k8s.io/core:/stable:/$KUBERNETES_VERSION/rpm/repodata/repomd.xml.key
exclude=kubelet kubeadm kubectl cri-tools kubernetes-cni
EOF
$ sudo dnf install -y kubelet kubeadm kubectl --disableexcludes=kubernetes
$ sudo systemctl enable --now kubelet
$ sudo systemctl status kubelet
k8s-master0.test.k8s.local (コントロールプレーンノード)
kubeadm initでクラスタを初期化
$ sudo kubeadm init --control-plane-endpoint "api.test.k8s.local:6443" --upload-certs --pod-network-cidr=172.16.0.0/12 --kubernetes-version 1.32.4
...
Your Kubernetes control-plane has initialized successfully!
To start using your cluster, you need to run the following as a regular user:
  mkdir -p $HOME/.kube
  sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
  sudo chown $(id -u):$(id -g) $HOME/.kube/config
Alternatively, if you are the root user, you can run:
  export KUBECONFIG=/etc/kubernetes/admin.conf
You should now deploy a pod network to the cluster.
Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:
  https://kubernetes.io/docs/concepts/cluster-administration/addons/
You can now join any number of control-plane nodes running the following command on each as root:
  kubeadm join api.test.k8s.local:6443 --token xxxx \
        --discovery-token-ca-cert-hash sha256:yyyy \
        --control-plane --certificate-key zzzzz
Please note that the certificate-key gives access to cluster sensitive data, keep it secret!
As a safeguard, uploaded-certs will be deleted in two hours; If necessary, you can use
"kubeadm init phase upload-certs --upload-certs" to reload certs afterward.
Then you can join any number of worker nodes by running the following on each as root:
kubeadm join api.test.k8s.local:6443 --token aaaaa \
        --discovery-token-ca-cert-hash sha256:bbbb
上記の出力内容(「kubeadm join ...」)は後で他のコントロールプレーンノードとワーカーノードを追加する際に使うので保存しておく。
※ kubeadmコマンドでは「--apiserver-advertise-address 10.0.0.150 (このノードのIP)」もネットワークインターフェースが複数ある場合には指定してもよいかも。
$ systemctl status kubelet
● kubelet.service - kubelet: The Kubernetes Node Agent
     Loaded: loaded (/usr/lib/systemd/system/kubelet.service; enabled; preset: disabled)
    Drop-In: /usr/lib/systemd/system/kubelet.service.d
             mq10-kubeadm.conf
     Active: active (running) since Sat 2025-05-03 06:22:51 EDT; 9min ago
       Docs: https://kubernetes.io/docs/
   Main PID: 2579 (kubelet)
      Tasks: 10 (limit: 10868)
     Memory: 38.0M
        CPU: 12.093s
     CGroup: /system.slice/kubelet.service
              +- /usr/bin/kubelet --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf --config=/var/lib/kubelet/config.yaml --container-runtime-endpoint=unix:///var/run/crio/crio.sock --pod-infra-container-image=registry.k8s.io/pause:3.10
※ Active: active (running)になっていればOK。
$ mkdir -p $HOME/.kube
$ sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
$ sudo chown $(id -u):$(id -g) $HOME/.kube/config
$ kubectl get nodes
NAME          STATUS     ROLES           AGE   VERSION
k8s-master0   NotReady   control-plane   65s   v1.32.4
※ まだCNIインストールしていないので「kubectl get nodes」のSTATUSはNotReadyのまま。
CNI - Calicoのインストール
CNIは1台目のコントロールプレーンノードにのみインストールすればOK。
まずはマニフェスト方式でインストールする(必要に応じてオペレータ方式へ移行)。
- Manifest - "Install Calico with Kubernetes API datastore, 50 nodes or less"
※ 本記事の構成ではTyphaは使わない。 
$ curl https://raw.githubusercontent.com/projectcalico/calico/v3.29.3/manifests/calico-typha.yaml -o calico.yaml
vi calico.yaml
...
            - name: CALICO_IPV4POOL_CIDR
              value: "172.16.0.0/12"
...
※ kubeadm initコマンド時に指定した「--pod-network-cidr=172.16.0.0/12」と同じサブネットアドレスをCIDR指定しておく(念のため)。しなくてもよいかも。
$ kubectl apply -f calico.yaml
$ kubectl get pods -A
NAMESPACE     NAME                                      READY   STATUS    RESTARTS   AGE
kube-system   calico-kube-controllers-79949b87d-6q86g   1/1     Running   0          16m
kube-system   calico-node-xxxxxx                        1/1     Running   0          16m
kube-system   calico-typha-87cb5c68d-pcwd5              1/1     Running   0          16m
kube-system   coredns-668d6bf9bc-2n9f4                  1/1     Running   0          76m
kube-system   coredns-668d6bf9bc-tmfm8                  1/1     Running   0          76m
kube-system   etcd-k8s-master0                          1/1     Running   1          76m
kube-system   kube-apiserver-k8s-master0                1/1     Running   1          76m
kube-system   kube-controller-manager-k8s-master0       1/1     Running   1          76m
kube-system   kube-proxy-nbhbv                          1/1     Running   1          76m
kube-system   kube-scheduler-k8s-master0                1/1     Running   1          76m
(少々待ってから)calico-node-xxxxxxがRunningならOK。
$ kubectl get nodes
NAME          STATUS   ROLES           AGE   VERSION
k8s-master0   Ready    control-plane   76m   v1.32.4
STATUS: ReadyになっていればOK。
$ curl -v -k https://api.test.k8s.local:6443/
...
HTTPエラー応答が返ってきていればOK
...
k8s-master1.test.k8s.local (コントロールプレーンノード)
kubeadm joinでクラスタへ追加
$ kubeadm join api.test.k8s.local:6443 --token xxxx --discovery-token-ca-cert-hash sha256:yyyy --control-plane --certificate-key zzzzz
$ kubeadm join ...
...
[download-certs] Downloading the certificates in Secret "kubeadm-certs" in the "kube-system" Namespace
error execution phase control-plane-prepare/download-certs: error downloading certs: error downloading the secret: Secret "kubeadm-certs" was not found in the "kube-system" Namespace. This Secret might have expired. Please, run `kubeadm init phase upload-certs --upload-certs` on a control plane to generate a new one
To see the stack trace of this error execute with --v=5 or higher
$ sudo kubeadm init phase upload-certs --upload-certs
...
[upload-certs] Storing the certificates in Secret "kubeadm-certs" in the "kube-system" Namespace
[upload-certs] Using certificate key:
new_key_zzzzz
$ kubeadm join api.test.k8s.local:6443 --token xxxx --discovery-token-ca-cert-hash sha256:yyyy --control-plane --certificate-key new_key_zzzzz
$ mkdir -p $HOME/.kube
$ sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
$ sudo chown $(id -u):$(id -g) $HOME/.kube/config
$  kubectl get nodes
NAME          STATUS   ROLES           AGE    VERSION
k8s-master0   Ready    control-plane   142m   v1.32.4
k8s-master1   Ready    control-plane   85s    v1.32.4
STATUSがReadyになっていればOK。
k8s-master2.test.k8s.local (コントロールプレーンノード)
kubeadm joinでクラスタへ追加
$ kubeadm join api.test.k8s.local:6443 --token xxxx --discovery-token-ca-cert-hash sha256:yyyy --control-plane --certificate-key zzzzz
$ mkdir -p $HOME/.kube
$ sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
$ sudo chown $(id -u):$(id -g) $HOME/.kube/config
$  kubectl get nodes
NAME          STATUS   ROLES           AGE     VERSION
k8s-master0   Ready    control-plane   154m    v1.32.4
k8s-master1   Ready    control-plane   13m     v1.32.4
k8s-master2   Ready    control-plane   3m33s   v1.32.4
STATUSがReadyになっていればOK。
k8s-worker0/1.test.k8s.local (ワーカーノード共通)
コンテナランタイムをインストール
$ export CRIO_VERSION=v1.32
$ cat <<EOF | sudo tee /etc/yum.repos.d/cri-o.repo
[cri-o]
name=CRI-O
baseurl=https://download.opensuse.org/repositories/isv:/cri-o:/stable:/$CRIO_VERSION/rpm/
enabled=1
gpgcheck=1
gpgkey=https://download.opensuse.org/repositories/isv:/cri-o:/stable:/$CRIO_VERSION/rpm/repodata/repomd.xml.key
EOF
$ sudo dnf install -y container-selinux
$ sudo dnf install -y cri-o
$ sudo dnf install -y podman
$ sudo systemctl enable --now crio
$ sudo systemctl status crio
$ ls -l /var/run/crio/crio.sock
srw-rw----. 1 root root 0 May  2 03:06 /var/run/crio/crio.sock
Kubernetesをインストール
$ export KUBERNETES_VERSION=v1.32
$ cat <<EOF | sudo tee /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://pkgs.k8s.io/core:/stable:/$KUBERNETES_VERSION/rpm/
enabled=1
gpgcheck=1
gpgkey=https://pkgs.k8s.io/core:/stable:/$KUBERNETES_VERSION/rpm/repodata/repomd.xml.key
exclude=kubelet kubeadm kubectl cri-tools kubernetes-cni
EOF
$ sudo dnf install -y kubelet kubeadm kubectl --disableexcludes=kubernetes
$ sudo systemctl enable --now kubelet
$ sudo systemctl status kubelet
kubeadm joinでクラスタへ追加
$ kubeadm join api.test.k8s.local:6443 --token aaaaa --discovery-token-ca-cert-hash sha256:bbbb
※ tokenはコントロールプレーンノードで「sudo kubeadm token list」でも参照できる。
コントロールプレーンノードで確認
$ kubectl get nodes
NAME          STATUS   ROLES           AGE     VERSION
k8s-master0   Ready    control-plane   3h14m   v1.32.4
k8s-master1   Ready    control-plane   53m     v1.32.4
k8s-master2   Ready    control-plane   43m     v1.32.4
k8s-worker0   Ready    <none>          10m     v1.32.4
k8s-worker1   Ready    <none>          9m27s   v1.32.4
STATUSがReadyならOK。joinしただけではROLESが「none」で表示される。
$ kubectl get pods -A
NAMESPACE     NAME                                      READY   STATUS    RESTARTS   AGE
kube-system   calico-kube-controllers-79949b87d-6q86g   1/1     Running   1          134m
kube-system   calico-node-2qc5q                         1/1     Running   0          52m
kube-system   calico-node-cljll                         1/1     Running   0          42m
kube-system   calico-node-kxnw4                         1/1     Running   1          134m
kube-system   calico-node-l9vrw                         1/1     Running   0          9m45s
kube-system   calico-node-ntndk                         1/1     Running   0          8m55s
kube-system   calico-typha-87cb5c68d-pcwd5              1/1     Running   1          134m
kube-system   coredns-668d6bf9bc-2n9f4                  1/1     Running   1          3h13m
kube-system   coredns-668d6bf9bc-tmfm8                  1/1     Running   1          3h13m
kube-system   etcd-k8s-master0                          1/1     Running   2          3h13m
kube-system   etcd-k8s-master1                          1/1     Running   0          52m
kube-system   etcd-k8s-master2                          1/1     Running   0          42m
kube-system   kube-apiserver-k8s-master0                1/1     Running   2          3h13m
kube-system   kube-apiserver-k8s-master1                1/1     Running   0          52m
kube-system   kube-apiserver-k8s-master2                1/1     Running   0          42m
kube-system   kube-controller-manager-k8s-master0       1/1     Running   2          3h13m
kube-system   kube-controller-manager-k8s-master1       1/1     Running   0          52m
kube-system   kube-controller-manager-k8s-master2       1/1     Running   0          42m
kube-system   kube-proxy-6r9kf                          1/1     Running   0          8m55s
kube-system   kube-proxy-b97jk                          1/1     Running   0          9m45s
kube-system   kube-proxy-lxdnz                          1/1     Running   0          42m
kube-system   kube-proxy-nbhbv                          1/1     Running   2          3h13m
kube-system   kube-proxy-ng8r8                          1/1     Running   0          52m
kube-system   kube-scheduler-k8s-master0                1/1     Running   2          3h13m
kube-system   kube-scheduler-k8s-master1                1/1     Running   0          52m
kube-system   kube-scheduler-k8s-master2                1/1     Running   0          42m
$ kubectl get pods -n kube-system -o wide | grep calico
calico-kube-controllers-79949b87d-6q86g   1/1     Running   1          144m    192.168.70.134    k8s-master0   <none>           <none>
calico-node-2qc5q                         1/1     Running   0          63m     10.0.0.151   k8s-master1   <none>           <none>
calico-node-cljll                         1/1     Running   0          52m     10.0.0.152   k8s-master2   <none>           <none>
calico-node-kxnw4                         1/1     Running   1          144m    10.0.0.150   k8s-master0   <none>           <none>
calico-node-l9vrw                         1/1     Running   0          20m     10.0.0.155   k8s-worker0   <none>           <none>
calico-node-ntndk                         1/1     Running   0          19m     10.0.0.156   k8s-worker1   <none>           <none>
calico-typha-87cb5c68d-pcwd5              1/1     Running   1          144m    10.0.0.150   k8s-master0   <none>           <none>
※ 各ノードの詳細は「kubectl describe node ノード名」で参照できる。
mng.test.k8s.local (管理端末)
kubectlをインストール
[mng ~]$ export KUBERNETES_VERSION=v1.32
[mng ~]$ cat <<EOF | sudo tee /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://pkgs.k8s.io/core:/stable:/$KUBERNETES_VERSION/rpm/
enabled=1
gpgcheck=1
gpgkey=https://pkgs.k8s.io/core:/stable:/$KUBERNETES_VERSION/rpm/repodata/repomd.xml.key
exclude=kubelet kubeadm kubectl cri-tools kubernetes-cni
EOF
[mng ~]$ sudo dnf install -y kubectl --disableexcludes=kubernetes
[mng ~]$ mkdir -p ~/.kube
[mng ~]$ scp root@10.0.0.150:/etc/kubernetes/admin.conf ~/.kube/config
[mng ~]$ chmod 600 .kube/config
[mng ~]$ kubectl get nodes
NAME          STATUS   ROLES           AGE   VERSION
k8s-master0   Ready    control-plane   18h   v1.32.4
k8s-master1   Ready    control-plane   15h   v1.32.4
k8s-master2   Ready    control-plane   15h   v1.32.4
k8s-worker0   Ready    <none>          15h   v1.32.4
k8s-worker1   Ready    <none>          15h   v1.32.4
calicoctlを取得
[mng ~]$ curl -L https://github.com/projectcalico/calico/releases/download/v3.29.3/calicoctl-linux-amd64 -o calicoctl
[mng ~]$ chmod +x ./calicoctl
[mng ~]$ ./calicoctl get nodes -o wide
NAME          ASN       IPV4            IPV6
k8s-master0   (64512)   10.0.0.150/24
k8s-master1   (64512)   10.0.0.151/24
k8s-master2   (64512)   10.0.0.152/24
k8s-worker0   (64512)   10.0.0.155/24
k8s-worker1   (64512)   10.0.0.156/24
[mng ~]$ ./calicoctl get ippools -o yaml
apiVersion: projectcalico.org/v3
items:
- apiVersion: projectcalico.org/v3
  kind: IPPool
  metadata:
    creationTimestamp: "2025-05-04T17:51:48Z"
    name: default-ipv4-ippool
    resourceVersion: "1139"
    uid: 73d5b037-5d18-455d-850f-5b25685f2f2f
  spec:
    allowedUses:
    - Workload
    - Tunnel
    blockSize: 26
    cidr: 172.16.0.0/12
    ipipMode: Always
    natOutgoing: true
    nodeSelector: all()
    vxlanMode: Never
kind: IPPoolList
metadata:
  resourceVersion: "45272"
※デフォルトではPod間通信に対してIPIPモードがAlways、natOutgoingが有効。
[mng ~]$ kubectl get pods -A -o wide |grep calico-node |grep k8s-worker
kube-system   calico-node-6kknp                         1/1     Running   2             11h   10.0.0.155       k8s-worker0   <none>           <none>
kube-system   calico-node-zmc92                         1/1     Running   2             11h   10.0.0.156       k8s-worker1   <none>           <none>
※ calico-node pod: Podネットワーク上でのLinuxカーネル(ルーティング/netfilter)へのIPレイヤのルーティング設定、NAT設定、ネットワークポリシー設定を実行するまたはBGPを話し連携することもできる仮想ルーター的なポッド
[mng ~]$ kubectl exec -n kube-system -it calico-node-6kknp -- ip a
Defaulted container "calico-node" out of: calico-node, upgrade-ipam (init), install-cni (init), mount-bpffs (init)
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:50:56:24:5c:22 brd ff:ff:ff:ff:ff:ff
    altname enp3s0
    inet 10.0.0.155/24 brd 10.0.0.255 scope global dynamic noprefixroute ens160
       valid_lft 37955sec preferred_lft 37955sec
    inet6 fe80::250:56ff:fe24:5c22/64 scope link noprefixroute
       valid_lft forever preferred_lft forever
3: tunl0@NONE: <NOARP,UP,LOWER_UP> mtu 1480 qdisc noqueue state UNKNOWN group default qlen 1000
    link/ipip 0.0.0.0 brd 0.0.0.0
    inet 172.23.229.128/32 scope global tunl0
       valid_lft forever preferred_lft forever
4: calic98d3ed38a4@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1480 qdisc noqueue state UP group default qlen 1000
    link/ether ee:ee:ee:ee:ee:ee brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::ecee:eeff:feee:eeee/64 scope link
       valid_lft forever preferred_lft forever
※ tunl0@NONE: Podネットワークインタフェース
※ ens160: 物理ネットワークインタフェース
[mng ~]$ kubectl exec -n kube-system -it calico-node-6kknp -- ip ro
Defaulted container "calico-node" out of: calico-node, upgrade-ipam (init), install-cni (init), mount-bpffs (init)
default via 10.0.0.53 dev ens160 proto dhcp src 10.0.0.155 metric 100
10.0.0.0/24 dev ens160 proto kernel scope link src 10.0.0.155 metric 100
172.23.229.130 dev calic98d3ed38a4 scope link
172.16.35.128/26 via 10.0.0.150 dev tunl0 proto bird onlink
172.20.194.64/26 via 10.0.0.156 dev tunl0 proto bird onlink
172.25.115.64/26 via 10.0.0.152 dev tunl0 proto bird onlink
172.26.159.128/26 via 10.0.0.151 dev tunl0 proto bird onlink
blackhole 172.23.229.128/26 proto bird
[mng ~]$ kubectl exec -n kube-system -it calico-node-zmc92 -- ip a
Defaulted container "calico-node" out of: calico-node, upgrade-ipam (init), install-cni (init), mount-bpffs (init)
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: ens160: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:50:56:20:b8:67 brd ff:ff:ff:ff:ff:ff
    altname enp3s0
    inet 10.0.0.156/24 brd 10.0.0.255 scope global dynamic noprefixroute ens160
       valid_lft 37734sec preferred_lft 37734sec
    inet6 fe80::250:56ff:fe20:b867/64 scope link noprefixroute
       valid_lft forever preferred_lft forever
3: tunl0@NONE: <NOARP,UP,LOWER_UP> mtu 1480 qdisc noqueue state UNKNOWN group default qlen 1000
    link/ipip 0.0.0.0 brd 0.0.0.0
    inet 172.20.194.64/32 scope global tunl0
       valid_lft forever preferred_lft forever
4: calie0bb82fb17d@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1480 qdisc noqueue state UP group default qlen 1000
    link/ether ee:ee:ee:ee:ee:ee brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::ecee:eeff:feee:eeee/64 scope link
       valid_lft forever preferred_lft forever
$ kubectl exec -n kube-system -it calico-node-zmc92 -- ip ro
Defaulted container "calico-node" out of: calico-node, upgrade-ipam (init), install-cni (init), mount-bpffs (init)
default via 10.0.0.53 dev ens160 proto dhcp src 10.0.0.156 metric 100
10.0.0.0/24 dev ens160 proto kernel scope link src 10.0.0.156 metric 100
172.20.194.66 dev calie0bb82fb17d scope link
172.16.35.128/26 via 10.0.0.150 dev tunl0 proto bird onlink
172.23.229.128/26 via 10.0.0.155 dev tunl0 proto bird onlink
172.25.115.64/26 via 10.0.0.152 dev tunl0 proto bird onlink
172.26.159.128/26 via 10.0.0.151 dev tunl0 proto bird onlink
blackhole 172.20.194.64/26 proto bird
ディプロイ確認 (HA-Proxy + NodePort + nginx)
アプリケーション通信についてまずはHA-Proxy + NodePort構成で分散する環境においてディプロイ確認を実施する。作業は管理端末(mng.test.k8s.local)から行う。
nginxのディプロイ
$ vi nginx-deployment.yaml
apiVersion: apps/v1             # Deploymentはapps/v1 APIグループを使う
kind: Deployment                # リソースの種類はDeployment
metadata:
  name: nginx-test              # Deploymentの名前(kubectlで識別する名前)
spec:
  replicas: 2                   # 起動するPodの数(= レプリカ数)
  selector:                     # Deploymentが管理するPodの条件
    matchLabels:
      app: nginx-test           # このラベルを持つPodが対象になる
  template:                     # Podテンプレート(新しく作るPodの定義)
    metadata:
      labels:
        app: nginx-test         # Podに付与するラベル(Serviceとマッチさせるために必要)
    spec:
      containers:
      - name: nginx             # コンテナ名(Pod内で識別)
        image: nginx            # 使用するDockerイメージ(Docker Hubの公式nginx)
        ports:
        - containerPort: 80     # コンテナがリッスンするポート(nginxのHTTPデフォルト)(= targetPort)
---
apiVersion: v1                  # Serviceはv1 APIグループを使う
kind: Service                   # リソースの種類はService
metadata:
  name: nginx-service           # Serviceの名前(kubectlで識別する名前)
spec:
  type: NodePort                # 外部アクセス可能なNodePortタイプ
  selector:
    app: nginx-test             # 対象のPodをラベルで指定(Deploymentと一致させる)
  ports:
    - port: 80                  # Serviceが受け付ける仮想ポート(Cluster内用)
      targetPort: 80            # 実際にPod側でリッスンしているポート
      nodePort: 30080           # ノードIP+このポートで外部公開(http://<NodeIP>:30080)
[mng ~]$ kubectl apply -f nginx-deployment.yaml
[mng ~]$ kubectl get svc nginx-service
NAME            TYPE       CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
nginx-service   NodePort   10.105.109.0   <none>        80:30080/TCP   7m24s
[mng ~]$ kubectl get pods -o wide
NAME                          READY   STATUS    RESTARTS   AGE   IP               NODE          NOMINATED NODE   READINESS GATES
nginx-test-598898876f-lgdj8   1/1     Running   1          10h   172.20.194.66    k8s-worker1   <none>           <none>
nginx-test-598898876f-xb9l5   1/1     Running   1          10h   172.23.229.130   k8s-worker0   <none>           <none>
$ kubectl get endpoints
NAMESPACE     NAME            ENDPOINTS                                                        AGE
default       kubernetes      10.0.0.150:6443,10.0.0.151:6443,10.0.0.152:6443                  11h
default       nginx-service   172.20.194.66:80,172.23.229.130:80                               10h
外部クライアントからのHTTPアクセスを確認
※ とりあえず管理端末(mng.test.k8s.local)からアクセスする。
[mng ~]$ curl -v http://apps-via-haproxy.test.k8s.local/
* Rebuilt URL to: http://apps-via-haproxy.test.k8s.local/
*   Trying 10.0.0.52...
* TCP_NODELAY set
* Connected to apps-via-haproxy.test.k8s.local (10.0.0.52) port 80 (#0)
> GET / HTTP/1.1
> Host: apps-via-haproxy.test.k8s.local
> User-Agent: curl/7.61.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: nginx/1.27.5
< Date: Sun, 04 May 2025 18:54:41 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
<
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
</body>
</html>
[mng ~]$ kubectl get pods -l app=nginx-test
NAME                          READY   STATUS    RESTARTS   AGE
nginx-test-598898876f-lgdj8   1/1     Running   1          13h
nginx-test-598898876f-xb9l5   1/1     Running   1          13h
[mng ~]$ kubectl logs nginx-test-598898876f-lgdj8
...
172.23.229.128 - - [05/May/2025:04:42:39 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.61.1" "-"
172.23.229.128 - - [05/May/2025:04:54:43 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.61.1" "-"
...
[mng ~]$ curl -v http://k8s-worker0.test.k8s.local:30080/
※ ワーカーノードでconntrackを生成するために管理端末から直接ワーカーノードのアドレス/Portを指定して実施
k8s-worker0のPodへ転送された場合:
[k8s-worker0 ~]$ sudo conntrack -L
tcp 6 114 TIME_WAIT src=10.0.0.190 dst=10.0.0.155 sport=46226 dport=30080 src=172.23.229.130 dst=10.0.0.155 sport=80 dport=8714 [ASSURED] mark=0 secctx=system_u:object_r:unlabeled_t:s0 use=1
行き:
src=10.0.0.190 dst=10.0.0.155 sport=46226 dport=30080
戻り:
src=172.23.229.130 dst=10.0.0.155 sport=80 dport=8714
コネクション状態: ASSURED
k8s-worker1のPodへ転送された場合:
[k8s-worker0 ~]$ sudo conntrack -L
tcp 6 112 TIME_WAIT src=10.0.0.190 dst=10.0.0.155 sport=45444 dport=30080 src=172.20.194.66 dst=172.23.229.128 sport=80 dport=4373 [ASSURED] mark=0 secctx=system_u:object_r:unlabeled_t:s0 use=1
行き:
src=10.0.0.190 dst=10.0.0.155 sport=45444 dport=30080
戻り:
src=172.20.194.66 dst=172.23.229.128 sport=80 dport=4373
コネクション状態: ASSURED
※ ASSUREDならとりあえずOK
MetalLBのセットアップ
アプリケーション通信の冗長化のためにMetalLBをL2モードでセットアップする。
事前確認
[mng ~]$kubectl -n kube-system get pods -l k8s-app=kube-proxy
NAME               READY   STATUS    RESTARTS   AGE
kube-proxy-84t4k   1/1     Running   2          14h
kube-proxy-9wrbg   1/1     Running   2          14h
kube-proxy-br8j4   1/1     Running   2          14h
kube-proxy-v8xd5   1/1     Running   2          14h
kube-proxy-vnmqt   1/1     Running   2          14h
[mng ~]$ kubectl -n kube-system logs kube-proxy-84t4k |grep Using
I0505 04:30:50.027916       1 server_linux.go:66] "Using iptables proxy"
I0505 04:30:50.293854       1 server_linux.go:170] "Using iptables Proxier"
※ kube-proxyがipvsモードでなく(デフォルトの)iptablesモードで動作中なのでstrictARP設定はスキップ。
CNI/CalicoのIPIPおよびVXLANモード(Pod間通信のトンネリング)を無効化
ノードは同一サブネットに配置し、MetalLBはL2モードで動作させるのでIPIPモードおよびVXLANモードを無効化。
[mng ~]$ calicoctl patch ippool default-ipv4-ippool -p '{"spec": {"ipipMode": "Never", "vxlanMode": "Never"}}'
[mng ~]$ ./calicoctl get ippool default-ipv4-ippool -o yaml
apiVersion: projectcalico.org/v3
kind: IPPool
metadata:
  creationTimestamp: "2025-05-04T17:51:48Z"
  name: default-ipv4-ippool
  resourceVersion: "4048"
  uid: 73d5b037-5d18-455d-850f-5b25685f2f2f
spec:
  allowedUses:
  - Workload
  - Tunnel
  blockSize: 26
  cidr: 172.16.0.0/12
  ipipMode: Never
  natOutgoing: true
  nodeSelector: all()
  vxlanMode: Never
            - name: CALICO_IPV4POOL_IPIP
              value: "Never"
            - name: CALICO_IPV4POOL_VXLAN
              value: "Never"
MetalLBインストール
[mng ~]$ kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.9/config/manifests/metallb-native.yaml
[mng ~]$ kubectl get ns metallb-system
NAME             STATUS   AGE
metallb-system   Active   2m8s
[mng ~]$ kubectl get pods -n metallb-system
NAME                         READY   STATUS    RESTARTS   AGE
controller-bb5f47665-zcl8v   1/1     Running   0          2m17s
speaker-glk7p                1/1     Running   0          2m16s
speaker-m62rc                1/1     Running   0          2m16s
speaker-m9bdh                1/1     Running   0          2m17s
speaker-wwqfd                1/1     Running   0          2m17s
speaker-xzscq                1/1     Running   0          2m17s
[mng ~]$ kubectl get secret -A
NAMESPACE        NAME                     TYPE                            DATA   AGE
kube-system      bootstrap-token-l6pmgm   bootstrap.kubernetes.io/token   7      17h
metallb-system   memberlist               Opaque                          1      13m
metallb-system   metallb-webhook-cert     Opaque                          4      13m
※ memberlist secretが自動で作成されていないようなら以下のように作成
[mng ~]$ kubectl create secret generic -n metallb-system memberlist --from-literal=secretkey="$(openssl rand -base64 128)"
[mng ~]$ vi metallb-config.yaml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: vip-pool
  namespace: metallb-system
spec:
  addresses:
    - 10.0.0.60-10.0.0.70
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: l2-adv
  namespace: metallb-system
spec:
  ipAddressPools:
    - vip-pool
[mng ~]$ kubectl apply -f metallb-config.yaml
[mng ~]$ kubectl get ipaddresspool -n metallb-system
NAME       AUTO ASSIGN   AVOID BUGGY IPS   ADDRESSES
vip-pool   true          false             ["10.0.0.60-10.0.0.70"]
[mng ~]$ kubectl get l2advertisement -n metallb-system
NAME     IPADDRESSPOOLS   IPADDRESSPOOL SELECTORS   INTERFACES
l2-adv   ["vip-pool"]
ディプロイ確認 (MetalLB + nginx)
nginxのディプロイ
[mng ~]$ vi nginx-metallb-deployment.yaml
# Deployment: nginx アプリケーションのPodを定義
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-test  # Deploymentリソースの名前
spec:
  replicas: 2              # Podのレプリカ数(2つ起動)
  selector:
    matchLabels:
      app: nginx-test      # このラベルに一致するPodを対象とする
  template:
    metadata:
      labels:
        app: nginx-test         # このPodが持つラベル
    spec:
      containers:
      - name: nginx        # コンテナの名前
        image: nginx       # 使用するnginxイメージ
        ports:
        - containerPort: 80 # コンテナ内で開くポート番号
---
# Service: nginx Pod にアクセスするための LoadBalancer型サービス
apiVersion: v1
kind: Service
metadata:
  name: nginx-service       # Serviceリソースの名前
spec:
  selector:
    app: nginx-test         # 上記DeploymentのPodと一致するラベル
  type: LoadBalancer        # MetalLBにEXTERNAL-IPを割り当てさせるタイプ
  ports:
  - protocol: TCP
    port: 80                # クライアントがアクセスするポート
    targetPort: 80          # Pod内のコンテナに転送するポート
[mng ~]$ kubectl apply -f nginx-metallb-deployment.yaml
[mng ~]$ kubectl get deploy nginx-test
NAME         READY   UP-TO-DATE   AVAILABLE   AGE
nginx-test   1/2     2            1           17s
[mng ~]$ kubectl get pods -l app=nginx-test
NAME                          READY   STATUS    RESTARTS   AGE
nginx-test-598898876f-j6j4q   1/1     Running   0          25s
nginx-test-598898876f-t28rg   1/1     Running   0          25s
[mng ~]$ kubectl get svc nginx-service
NAME            TYPE           CLUSTER-IP    EXTERNAL-IP   PORT(S)        AGE
nginx-service   LoadBalancer   10.97.120.8   10.0.0.60     80:30224/TCP   2m17s
※ EXTERNAL-IP(10.0.0.60 )が割り当てられた仮想IP
外部クライアントからのHTTPアクセスを確認
※ とりあえず管理端末(mng.test.k8s.local)からアクセスする。
[mng ~]$ curl -v http://10.0.0.60/
*   Trying 10.0.0.60...
* TCP_NODELAY set
* Connected to 10.0.0.60 (10.0.0.60) port 80 (#0)
> GET / HTTP/1.1
> Host: 10.0.0.60
> User-Agent: curl/7.61.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: nginx/1.27.5
< Date: Mon, 05 May 2025 11:39:12 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
<
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
</html>
[mng ~]$ curl -v http://apps.test.k8s.local/
同様
[mng ~]$ kubectl get pods -l app=nginx-test
NAME                          READY   STATUS    RESTARTS   AGE
nginx-test-598898876f-j6j4q   1/1     Running   0          8m3s
nginx-test-598898876f-t28rg   1/1     Running   0          8m3s
[mng ~]$ kubectl logs nginx-test-598898876f-j6j4q
...
10.0.0.155 - - [05/May/2025:11:39:12 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.61.1" "-"
10.0.0.155 - - [05/May/2025:11:55:52 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.61.1" "-"
ワーカーノードを追加
k8s-worker2.test.k8s.local
ワーカーノード(k8s-worker2.test.k8s.local)を追加する。他のワーカーノードのセットアップと共通の手順を実施。
コンテナランタイムをインストール
$ export CRIO_VERSION=v1.32
$ cat <<EOF | sudo tee /etc/yum.repos.d/cri-o.repo
[cri-o]
name=CRI-O
baseurl=https://download.opensuse.org/repositories/isv:/cri-o:/stable:/$CRIO_VERSION/rpm/
enabled=1
gpgcheck=1
gpgkey=https://download.opensuse.org/repositories/isv:/cri-o:/stable:/$CRIO_VERSION/rpm/repodata/repomd.xml.key
EOF
$ sudo dnf install -y container-selinux
$ sudo dnf install -y cri-o
$ sudo dnf install -y podman
$ sudo systemctl enable --now crio
$ sudo systemctl status crio
$ ls -l /var/run/crio/crio.sock
srw-rw----. 1 root root 0 May  2 03:06 /var/run/crio/crio.sock
Kubernetesをインストール
$ export KUBERNETES_VERSION=v1.32
$ cat <<EOF | sudo tee /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://pkgs.k8s.io/core:/stable:/$KUBERNETES_VERSION/rpm/
enabled=1
gpgcheck=1
gpgkey=https://pkgs.k8s.io/core:/stable:/$KUBERNETES_VERSION/rpm/repodata/repomd.xml.key
exclude=kubelet kubeadm kubectl cri-tools kubernetes-cni
EOF
$ sudo dnf install -y kubelet kubeadm kubectl --disableexcludes=kubernetes
$ sudo systemctl enable --now kubelet
$ sudo systemctl status kubelet
[mng ~]$ kubectl get pods -A -o wide | grep k8s-worker
kube-system      calico-node-6kknp                         1/1     Running   3             2d18h   10.0.0.155      k8s-worker0   <none>           <none>
kube-system      calico-node-zmc92                         1/1     Running   3             2d18h   10.0.0.156      k8s-worker1   <none>           <none>
kube-system      kube-proxy-9wrbg                          1/1     Running   3             2d18h   10.0.0.155      k8s-worker0   <none>           <none>
kube-system      kube-proxy-vnmqt                          1/1     Running   3             2d18h   10.0.0.156      k8s-worker1   <none>           <none>
metallb-system   controller-bb5f47665-zcl8v                1/1     Running   1             2d2h    172.20.194.66   k8s-worker1   <none>           <none>
metallb-system   speaker-glk7p                             1/1     Running   2 (50s ago)   2d2h    10.0.0.156      k8s-worker1   <none>           <none>
metallb-system   speaker-xzscq                             1/1     Running   2 (50s ago)   2d2h    10.0.0.155      k8s-worker0   <none>           <none>
コントロールプレーンノードでトークンを再作成
k8s-master0.test.k8s.localで作業する。
[k8s-master0 ~]$ kubeadm token create --print-join-command
kubeadm join api.test.k8s.local:6443 --token aaaaa --discovery-token-ca-cert-hash sha256:bbbb
ワーカーノードをクラスタへ追加
k8s-worker2.test.k8s.localで作業する。
[k8s-worker2 ~]$ kubeadm join api.test.k8s.local:6443 --token aaaaa --discovery-token-ca-cert-hash sha256:bbbb
[mng ~]$ kubectl get nodes -o wide
NAME          STATUS   ROLES           AGE     VERSION   INTERNAL-IP   EXTERNAL-IP   OS-IMAGE                              KERNEL-VERSION                 CONTAINER-RUNTIME
k8s-master0   Ready    control-plane   2d19h   v1.32.4   10.0.0.150    <none>        Red Hat Enterprise Linux 9.5 (Plow)   5.14.0-503.40.1.el9_5.x86_64   cri-o://1.32.4
k8s-master1   Ready    control-plane   2d18h   v1.32.4   10.0.0.151    <none>        Red Hat Enterprise Linux 9.5 (Plow)   5.14.0-503.40.1.el9_5.x86_64   cri-o://1.32.4
k8s-master2   Ready    control-plane   2d18h   v1.32.4   10.0.0.152    <none>        Red Hat Enterprise Linux 9.5 (Plow)   5.14.0-503.40.1.el9_5.x86_64   cri-o://1.32.4
k8s-worker0   Ready    <none>          2d18h   v1.32.4   10.0.0.155    <none>        Red Hat Enterprise Linux 9.5 (Plow)   5.14.0-503.40.1.el9_5.x86_64   cri-o://1.32.4
k8s-worker1   Ready    <none>          2d18h   v1.32.4   10.0.0.156    <none>        Red Hat Enterprise Linux 9.5 (Plow)   5.14.0-503.40.1.el9_5.x86_64   cri-o://1.32.4
k8s-worker2   Ready    <none>          17m     v1.32.4   10.0.0.157    <none>        Red Hat Enterprise Linux 9.5 (Plow)   5.14.0-503.40.1.el9_5.x86_64   cri-o://1.32.4
[mng ~]$ kubectl get pods -A -o wide | grep k8s-worker
kube-system      calico-node-6kknp                         1/1     Running   3               2d19h   10.0.0.155      k8s-worker0   <none>           <none>
kube-system      calico-node-dx9lg                         1/1     Running   0               82s     10.0.0.157      k8s-worker2   <none>           <none>
kube-system      calico-node-zmc92                         1/1     Running   3               2d19h   10.0.0.156      k8s-worker1   <none>           <none>
kube-system      kube-proxy-9wrbg                          1/1     Running   3               2d19h   10.0.0.155      k8s-worker0   <none>           <none>
kube-system      kube-proxy-bfzmw                          1/1     Running   0               82s     10.0.0.157      k8s-worker2   <none>           <none>
kube-system      kube-proxy-vnmqt                          1/1     Running   3               2d19h   10.0.0.156      k8s-worker1   <none>           <none>
metallb-system   controller-bb5f47665-zcl8v                1/1     Running   1               2d2h    172.20.194.66   k8s-worker1   <none>           <none>
metallb-system   speaker-glk7p                             1/1     Running   2 (4m21s ago)   2d2h    10.0.0.156      k8s-worker1   <none>           <none>
metallb-system   speaker-slmcg                             1/1     Running   0               56s     10.0.0.157      k8s-worker2   <none>           <none>
metallb-system   speaker-xzscq                             1/1     Running   2 (4m21s ago)   2d2h    10.0.0.155      k8s-worker0   <none>           <none>
nginx podを追加ワーカーノードにも配置する
nginx podを追加ワーカーノードにも配置するためにDeploymentへレプリカを追加する。
mng ~]$ kubectl get deployment
NAME         READY   UP-TO-DATE   AVAILABLE   AGE
nginx-test   2/2     2            2           2m38s
[mng ~]$ kubectl scale deployment nginx-test --replicas=3
[mng ~]$ kubectl get deployment
NAME         READY   UP-TO-DATE   AVAILABLE   AGE
nginx-test   3/3     3            3           4m40s
[mng ~]$ kubectl get pods -l app=nginx-test -o wide
NAME                          READY   STATUS    RESTARTS   AGE     IP               NODE          NOMINATED NODE   READINESS GATES
nginx-test-598898876f-g8ncx   1/1     Running   0          6m26s   172.30.126.0     k8s-worker2   <none>           <none>
nginx-test-598898876f-jbdmt   1/1     Running   0          6m26s   172.23.229.129   k8s-worker0   <none>           <none>
nginx-test-598898876f-zbh5g   1/1     Running   0          2m6s    172.20.194.68    k8s-worker1   <none>           <none>
[@mng ~]$ curl -v http://apps.test.k8s.local/
*   Trying 10.0.0.60...
* TCP_NODELAY set
* Connected to apps.test.k8s.local (10.0.0.60) port 80 (#0)
> GET / HTTP/1.1
> Host: apps.test.k8s.local
> User-Agent: curl/7.61.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: nginx/1.27.5
< Date: Wed, 07 May 2025 13:31:53 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
<
<!DOCTYPE html>
<html>
<head>
...
</html>
[mng ~]$ kubectl get pods -l app=nginx-test -o wide | grep k8s-worker2
nginx-test-598898876f-g8ncx   1/1     Running   0          10m     172.30.126.0     k8s-worker2   <none>           <none>
[mng ~]$ kubectl logs nginx-test-598898876f-g8ncx
...
10.0.0.155 - - [07/May/2025:13:24:25 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.61.1" "-"
Web Dashboardのインストール
Helmインストール
[mng ~]$ curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
[mng ~]$ chmod +x get_helm.sh
[mng ~]$ ./get_helm.sh
Downloading https://get.helm.sh/helm-v3.17.3-linux-amd64.tar.gz
Verifying checksum... Done.
Preparing to install helm into /usr/local/bin
helm installed into /usr/local/bin/helm
[mng ~]$ helm version
version.BuildInfo{Version:"v3.17.3", GitCommit:"e4da49785aa6e6ee2b86efd5dd9e43400318262b", GitTreeState:"clean", GoVersion:"go1.23.7"}
Dashboardインストール
helm upgrade --install kubernetes-dashboard kubernetes-dashboard/kubernetes-dashboard --create-namespace --namespace kubernetes-dashboard
Release "kubernetes-dashboard" does not exist. Installing it now.
NAME: kubernetes-dashboard
LAST DEPLOYED: Fri May  9 22:47:40 2025
NAMESPACE: kubernetes-dashboard
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
*************************************************************************************************
*** PLEASE BE PATIENT: Kubernetes Dashboard may need a few minutes to get up and become ready ***
*************************************************************************************************
Congratulations! You have just installed Kubernetes Dashboard in your cluster.
To access Dashboard run:
  kubectl -n kubernetes-dashboard port-forward svc/kubernetes-dashboard-kong-proxy 8443:443
NOTE: In case port-forward command does not work, make sure that kong service name is correct.
      Check the services in Kubernetes Dashboard namespace using:
        kubectl -n kubernetes-dashboard get svc
Dashboard will be available at:
  https://localhost:8443
To access Dashboard run:
kubectl -n kubernetes-dashboard port-forward svc/kubernetes-dashboard-kong-proxy 8443:443
とあり、
[mng ~]$ kubectl get svc -n kubernetes-dashboard
NAME                                           TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
service/kubernetes-dashboard-api               ClusterIP   10.106.95.223    <none>        8000/TCP   15s
service/kubernetes-dashboard-auth              ClusterIP   10.109.10.202    <none>        8000/TCP   15s
service/kubernetes-dashboard-kong-proxy        ClusterIP   10.96.109.14     <none>        443/TCP    15s
service/kubernetes-dashboard-metrics-scraper   ClusterIP   10.100.73.253    <none>        8000/TCP   15s
service/kubernetes-dashboard-web               ClusterIP   10.106.227.137   <none>        8000/TCP   15s
かつ、Service一覧を確認するとすでにkubernetes-dashboard-kong-proxy用に作成されているので、Serviceのタイプを「ClusterIP」からMetalLBで公開するために「LoadBalancer」へ変更する。
[mng ~]$ kubectl edit service kubernetes-dashboard-kong-proxy -n kubernetes-dashboard
...
spec:
  type: LoadBalancer
...
[mng ~]$ kubectl get svc -n kubernetes-dashboard kubernetes-dashboard-kong-proxy
NAME                              TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)         AGE
kubernetes-dashboard-kong-proxy   LoadBalancer   10.96.109.14   10.0.0.62     443:30698/TCP   54m
MetalLBにより割り当てられたVIPは「10.0.0.62」となる。ブラウザから「hxxps://10.0.0.62:443」または「hxxps://10.0.0.62」へアクセスする。
認証トークンを作成
ログイン画面を開くと認証トークンが求められる(ユーザアカウントでなくサービスアカウント(アプリケーション/プログラム用)としての認証情報の作成が必要)。
[mng ~]$ kubectl create sa admin-user -n kubernetes-dashboard
serviceaccount/admin-user created
[mng ~]$ kubectl get sa admin-user -n kubernetes-dashboard
NAME         SECRETS   AGE
admin-user   0         11s
[mng ~]$ kubectl create clusterrolebinding admin-user-binding --clusterrole=cluster-admin --serviceaccount=kubernetes-dashboard:admin-user
clusterrolebinding.rbac.authorization.k8s.io/admin-user-binding created
[mng ~]$ kubectl get clusterrolebinding admin-user-binding -o wide
NAME                 ROLE                        AGE   USERS   GROUPS   SERVICEACCOUNTS
admin-user-binding   ClusterRole/cluster-admin   31s                    kubernetes-dashboard/admin-user
$ kubectl -n kubernetes-dashboard create token admin-user --duration=8760h (= 24h x 365)
eyJhbGciOiJSUzI1NiI...
※ 1年間(24h x 365日)有効なトークンを発行
出力されたトークンでログイン。
(おまけ) トークン(JWT)をダンプ
ちなみに出力されるトークン(JWT)をダンプすると以下のような感じ。
{
  "alg": "RS256",
  "kid": "5vBzOOElA9x8rFMUYWdf8BH4INglLbbAVRgV1wsQGoY"
}
{
  "aud": [
    "https://kubernetes.default.svc.cluster.local"
  ],
  "exp": 1778427001,
  "iat": 1746891001,
  "iss": "https://kubernetes.default.svc.cluster.local",
  "jti": "ea050fcb-a7db-4dee-a9a6-4a7297b32802",
  "kubernetes.io": {
    "namespace": "kubernetes-dashboard",
    "serviceaccount": {
      "name": "admin-user",
      "uid": "35816fd8-a549-4e70-808e-5257722b94dd"
    }
  },
  "nbf": 1746804601,
  "sub": "system:serviceaccount:kubernetes-dashboard:admin-user"
}
...
またマニフェストで作成する方法は以下を参照。
KubernetesでPrivateCA証明書や自己署名証明書を使う場合の考慮事項
今後の作業でPrivateCAによる証明書や自己署名証明書を利用することがでてくるので考慮事項をまとめておく。
プライベートレジストリ
Podを起動するコンテナイメージをプライベートレジストリからPullする場合にはコンテナランタイムにPrivateCA/自己署名の証明書を登録しておく。
CRI-Oの場合にはノードのシステムに登録する。
[mng ~]$ scp tls/private_ca.crt k8s-worker0.test.k8s.local:/tmp/
[mng ~]$ ssh k8s-worker0.test.k8s.local "sudo -S cp /tmp/private_ca.crt /etc/pki/ca-trust/source/anchors/test_k8s_local_ca.crt && sudo -S update-ca-trust extract"
Podのヘルスチェック (Probe)
Pod内のコンテナに対してHTTP/HTTPSによりヘルスチェックする際の留意事項についてメモしておく。
Probe種類
| Probe種類 | 目的 | 失敗時の動作 | 実行タイミング | 
|---|---|---|---|
| livenessProbe | コンテナが生きているかの判定 | コンテナを再起動 | Pod 起動後に定期実行 | 
| readinessProbe | トラフィック受け付け可能かの判定 | Service から除外 | Pod 起動後に定期実行 | 
| startupProbe | 起動遅延の検出 | 起動失敗時に再起動 | Pod 起動直後のみ | 
Probe方式
- httpGet: HTTP(S)リクエストを送信し、レスポンスコードを確認(200~399 で成功)
 - tcpSocket: 指定ポートへTCP接続(接続成功でOK)
 - exec: コンテナ内でコマンドを実行し、終了コードが0なら成功
 
HTTPSプローブ (httpGet) 時のケース別まとめ
| ケース | 推奨プローブ方式 | 備考 | 
|---|---|---|
| Public CA | httpGet + HTTPS | 特別な設定不要。Kubeletが信頼可能なCAならそのまま利用可能 | 
| Private CA | (1) ノードにCA証明書を登録してhttpGet + HTTPS (2) exec (curl --cacert or -k)  | 
kubelet/コンテナランタイムがCAを信頼していない場合は失敗するため(1) or (2)で実施 | 
| 自己署名証明書 | (1) exec (curl --cacert or -k) (2) tcpSocket  | 
|
| Sorryサーバ(常に404応答) | (1) exec (2) tcpSocket  | 
httpGetでは失敗扱い。別途監視用のAPIパスを用意するでも可 | 
※ 同一IngressのバックエンドプロトコルにHTTPSを利用する場合にはSorryサーバも同じルールに記述するならばそれへの接続方法もHTTPSになるので留意




