首页
/ Kubernetes The Hard Way:无脚本手工引导 Kubernetes v1.32 集群的完整实战教程

Kubernetes The Hard Way:无脚本手工引导 Kubernetes v1.32 集群的完整实战教程

2026-09-05 13:08:31作者:裴麒琰

本文基于 README.md 及配套 labs 文档,系统讲解 kubernetes-the-hard-way 项目「不用任何自动化脚本、从最底层组件开始手工搭建 Kubernetes 集群」的完整方法论。读完本篇,你将掌握 13 个实验环节的完整操作:从 jumpbox 准备、二进制下载、CA 与 TLS 证书签发、kubeconfig 生成、数据加密配置,到 etcd、控制面、工作节点的逐个引导,最终完成 Pod 网络路由与冒烟测试——理解每一个 Kubernetes 组件「为什么这样配置、启动后如何验证」。

一、设计理念:为什么选择 the hard way

这个项目的定位在 README.md 中写得很明确:

This tutorial walks you through setting up Kubernetes the hard way. This guide is not for someone looking for a fully automated tool to bring up a Kubernetes cluster. Kubernetes The Hard Way is optimized for learning, which means taking the long route to ensure you understand each task required to bootstrap a Kubernetes cluster.

它刻意放弃了 kubeadm、kops 等自动化工具,用「长路线」换取对每个引导任务的理解。README 同时给出了一条重要的事实边界声明:

The results of this tutorial should not be viewed as production ready, and may receive limited support from the community, but don't let that stop you from learning!

也就是说:本教程产出的集群不面向生产环境,社区支持也有限,其价值在于学习核心概念。目标读者是希望理解 Kubernetes 基础以及各核心组件如何协作起来的开发者("The target audience for this tutorial is someone who wants to understand the fundamentals of Kubernetes and how the core components fit together.")。

二、集群形态与组件版本

2.1 单控制面 + 两工作节点的最小拓扑

README 的 "Cluster Details" 一节说明:

Kubernetes The Hard Way guides you through bootstrapping a basic Kubernetes cluster with all control plane components running on a single node, and two worker nodes, which is enough to learn the core concepts.

即:全部控制面组件(API Server、Scheduler、Controller Manager、etcd)跑在一台 server 节点上,加上两台工作节点 node-0 / node-1,这是足够理解核心概念的最小集群。

整个教程要求 4 台 ARM64 或 AMD64 的虚拟机或物理机处于同一网络中,第 4 台是用于远程操作的 jumpboxdocs/01-prerequisites.md 给出了精确的机器规格(Debian 12 bookworm):

Name Description CPU RAM Storage
jumpbox Administration host 1 512MB 10GB
server Kubernetes server 1 2GB 20GB
node-0 Kubernetes worker node 1 2GB 20GB
node-1 Kubernetes worker node 1 2GB 20GB

机器如何创建不限(VMware、云主机、物理机均可),唯一的硬性要求是规格达标且系统为 Debian 12,可通过 cat /etc/os-release 确认 PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"

2.2 组件版本矩阵

README 锁定的组件版本如下,这也是全仓库 downloads-*.txt 下载清单与 labs 命令中版本号的来源:

组件 版本
Kubernetes v1.32.x(实际下载 v1.32.3)
containerd v2.1.x(实际 v2.1.0-beta.0)
CNI plugins v1.6.x(实际 v1.6.2)
etcd v3.6.x(实际 v3.6.0-rc.3)

downloads-amd64.txt 中可以看到各组件的精确下载地址与版本:dl.k8s.io/v1.32.3 下的 6 个二进制(kubectl、kube-apiserver、kube-controller-manager、kube-scheduler、kube-proxy、kubelet),以及 crictl v1.32.0、runc v1.3.0-rc.1、cni-plugins v1.6.2、containerd v2.1.0-beta.0、etcd v3.6.0-rc.3。仓库同时提供 downloads-arm64.txt 供 ARM64 架构使用。

三、13 个实验的完整路线图

README 的 "Labs" 一节列出了全部实验及其顺序。这个顺序本身就是一部「Kubernetes 引导依赖关系图」:先有网络可达与命名,才有证书;有了证书才能签 kubeconfig;有了 CA 密钥与证书才能起 API Server;API Server 起来后 kubelet 才能注册节点。完整清单:

  1. Prerequisites — 机器要求
  2. Setting up the Jumpbox — 管理机与二进制下载
  3. Provisioning Compute Resources — 机器数据库、SSH 与主机名
  4. Provisioning the CA and Generating TLS Certificates — PKI 基础设施
  5. Generating Kubernetes Configuration Files for Authentication — 6 份 kubeconfig
  6. Generating the Data Encryption Config and Key — Secrets 静态加密
  7. Bootstrapping the etcd Cluster — 单节点 etcd
  8. Bootstrapping the Kubernetes Control Plane — API Server/Scheduler/Controller Manager
  9. Bootstrapping the Kubernetes Worker Nodes — runc/CNI/containerd/kubelet/kube-proxy
  10. Configuring kubectl for Remote Access — 远程 kubectl
  11. Provisioning Pod Network Routes — 跨节点 Pod 路由
  12. Smoke Test — 端到端功能验证
  13. Cleaning Up — 资源回收

仓库目录结构也与实验一一对应:docs/ 存放 13 篇实验文档,units/ 存放 7 个 systemd unit 文件(etcd.servicekube-apiserver.servicekube-controller-manager.servicekube-scheduler.servicekubelet.servicekube-proxy.servicecontainerd.service),configs/ 存放各组件的配置文件模板,ca.conf 是 openssl 的 CA 模板。

四、Jumpbox:管理机、二进制仓库与 kubectl

4.1 角色定位

docs/02-jumpbox.md 将 jumpbox 定义为整个教程的「基地」:所有后续命令都从这台机器发起。文档也说明 jumpbox 不是必须——"these commands can also be run from just about any machine including your personal workstation running macOS or Linux"——但专用机器能保证一致性。教程全程以 root 用户操作,这是明确的便利性取舍。

4.2 安装工具并克隆仓库

ssh root@jumpbox

{
  apt-get update
  apt-get -y install wget curl vim openssl git
}

随后克隆本教程仓库——仓库中包含后续各实验所需的配置文件与模板:

git clone --depth 1 \
  https://github.com/kelseyhightower/kubernetes-the-hard-way.git
cd kubernetes-the-hard-way

此后 kubernetes-the-hard-way 目录就是工作目录,迷路时可用 pwd 确认(应为 /root/kubernetes-the-hard-way)。

4.3 集中下载全部二进制

为了不在每台机器上重复消耗带宽,教程把全部二进制集中下载到 jumpbox 的 downloads 目录。下载清单按架构区分:

cat downloads-$(dpkg --print-architecture).txt

wget -q --show-progress \
  --https-only \
  --timestamping \
  -P downloads \
  -i downloads-$(dpkg --print-architecture).txt

dpkg --print-architecture 会自动选到 downloads-amd64.txtdownloads-arm64.txt。下载量约 500MB。

接着解压并按「client / cni-plugins / controller / worker」四个角色重组目录——这一步的组织方式揭示了组件的角色划分:

{
  ARCH=$(dpkg --print-architecture)
  mkdir -p downloads/{client,cni-plugins,controller,worker}
  tar -xvf downloads/crictl-v1.32.0-linux-${ARCH}.tar.gz \
    -C downloads/worker/
  tar -xvf downloads/containerd-2.1.0-beta.0-linux-${ARCH}.tar.gz \
    --strip-components 1 \
    -C downloads/worker/
  tar -xvf downloads/cni-plugins-linux-${ARCH}-v1.6.2.tgz \
    -C downloads/cni-plugins/
  tar -xvf downloads/etcd-v3.6.0-rc.3-linux-${ARCH}.tar.gz \
    -C downloads/ \
    --strip-components 1 \
    etcd-v3.6.0-rc.3-linux-${ARCH}/etcdctl \
    etcd-v3.6.0-rc.3-linux-${ARCH}/etcd
  mv downloads/{etcdctl,kubectl} downloads/client/
  mv downloads/{etcd,kube-apiserver,kube-controller-manager,kube-scheduler} \
    downloads/controller/
  mv downloads/{kubelet,kube-proxy} downloads/worker/
  mv downloads/runc.${ARCH} downloads/worker/runc
}
rm -rf downloads/*gz
chmod +x downloads/{client,cni-plugins,controller,worker}/*

4.4 安装 kubectl

cp downloads/client/kubectl /usr/local/bin/
kubectl version --client
Client Version: v1.32.3
Kustomize Version: v5.5.0

五、机器数据库、SSH 与主机名

docs/03-compute-resources.md 用一个纯文本文件作为「机器数据库」,这是后续几乎所有批量脚本(while read ... done < machines.txt)的数据源。

5.1 machines.txt 的 Schema

每行四列:

IPV4_ADDRESS FQDN HOSTNAME POD_SUBNET
  • IPV4_ADDRESS:机器内网 IP;
  • FQDN:完全限定域名,如 node-0.kubernetes.local
  • HOSTNAME:短主机名,Kubernetes 节点注册与 jumpbox 命令分发都用它;
  • POD_SUBNET:该机器专属的 Pod IP 网段。Kubernetes 为每个 Pod 分配一个 IP,POD_SUBNET 就是分配给这台机器的地址池。

示例(IP 已打码):

XXX.XXX.XXX.XXX server.kubernetes.local server
XXX.XXX.XXX.XXX node-0.kubernetes.local node-0 10.200.0.0/24
XXX.XXX.XXX.XXX node-1.kubernetes.local node-1 10.200.1.0/24

注意 server 行没有 POD_SUBNET——因为控制面节点不承载工作负载 Pod。

5.2 开启 root SSH 与分发密钥

Debian 默认禁用 root 的 SSH 登录,教程为简化流程选择打开它,并坦承「Security is a tradeoff, and in this case, we are optimizing for convenience」:

sed -i \
  's/^#*PermitRootLogin.*/PermitRootLogin yes/' \
  /etc/ssh/sshd_config
systemctl restart sshd

在 jumpbox 上生成密钥并分发:

ssh-keygen

while read IP FQDN HOST SUBNET; do
  ssh-copy-id root@${IP}
done < machines.txt

验证免密登录:

while read IP FQDN HOST SUBNET; do
  ssh -n root@${IP} hostname
done < machines.txt
server
node-0
node-1

5.3 主机名与 hosts 解析表

主机名在集群中承担关键角色:Kubernetes 客户端用 server 主机名而非 IP 访问 API Server,工作节点也用主机名向集群注册。设置主机名(批量 SSH 执行):

while read IP FQDN HOST SUBNET; do
    CMD="sed -i 's/^127.0.1.1.*/127.0.1.1\t${FQDN} ${HOST}/' /etc/hosts"
    ssh -n root@${IP} "$CMD"
    ssh -n root@${IP} hostnamectl set-hostname ${HOST}
    ssh -n root@${IP} systemctl restart systemd-hostnamed
done < machines.txt

然后由 machines.txt 生成一个 hosts 解析表(含 # Kubernetes The Hard Way 头注释),分别追加到 jumpbox 的 /etc/hosts 和每台远程机器的 /etc/hosts

echo "" > hosts
echo "# Kubernetes The Hard Way" >> hosts

while read IP FQDN HOST SUBNET; do
    ENTRY="${IP} ${FQDN} ${HOST}"
    echo $ENTRY >> hosts
done < machines.txt

cat hosts >> /etc/hosts

对远程机器:

while read IP FQDN HOST SUBNET; do
  scp hosts root@${HOST}:~/
  ssh -n root@${HOST} "cat hosts >> /etc/hosts"
done < machines.txt

完成后用主机名即可 SSH:ssh root@serverssh root@node-0ssh root@node-1

六、CA 与 TLS 证书:集群的信任根

docs/04-certificate-authority.md 用 openssl 搭建 PKI,为 kube-apiserver、kube-controller-manager、kube-scheduler、kubelet、kube-proxy 以及 admin 用户签发证书。

6.1 创建自签名 CA

{
  openssl genrsa -out ca.key 4096
  openssl req -x509 -new -sha512 -noenc \
    -key ca.key -days 3653 \
    -config ca.conf \
    -out ca.crt
}

文档特别强调:自签名 CA 仅够本教程使用,「this shouldn't be considered something you would do in a real-world production environment」。

6.2 ca.conf:理解 Kubernetes 证书身份的关键

仓库提供的 ca.conf 是本实验最重要的配置文件,每个组件一个 section,值得逐段精读,因为证书里的 CN 和 O 直接决定了该组件在 Kubernetes 认证体系中的身份

  • [admin]CN=adminO=system:masters——这是集群管理组,kubeconfig 中以 admin 用户出现;
  • [node-0] / [node-1]CN=system:node:node-0O=system:nodes——注释解释了原因:Kubernetes 的 Node Authorizer 要求 Kubelet 使用 system:nodes 组 + system:node:<nodeName> 用户名才能被正确授权,SAN 含 DNS:node-0, IP:127.0.0.1
  • [kube-proxy]CN=system:kube-proxyO=system:node-proxier
  • [kube-controller-manager]CN=system:kube-controller-managerO=system:kube-controller-manager
  • [kube-scheduler]CN=system:kube-scheduler(文件中 O=system:system:kube-scheduler 是模板中的一个笔误式写法,功能上依赖的是 CN);
  • [kube-api-server]:这是唯一「服务端证书」,CN=kubernetes,SAN 列表覆盖了 API Server 可能接收流量的全部地址——127.0.0.1、Service CIDR 的 ClusterIP 10.32.0.1,以及 kuberneteskubernetes.default.svcserver.kubernetes.local 等 6 个 DNS 名。注释说明:API Server 会自动获得集群内 DNS 名 kubernetes,关联到 Service 网段 10.32.0.0/24 的首个 IP 10.32.0.1
  • [service-accounts]CN=service-accounts——Controller Manager 用这对密钥为 ServiceAccount 签发 token;
  • 客户端证书统一携带 extendedKeyUsage = clientAuth(API Server 为 clientAuth, serverAuth),keyUsage = critical, digitalSignature, keyEncipherment

6.3 批量签发组件证书

certs=(
  "admin" "node-0" "node-1"
  "kube-proxy" "kube-scheduler"
  "kube-controller-manager"
  "kube-api-server"
  "service-accounts"
)

for i in ${certs[*]}; do
  openssl genrsa -out "${i}.key" 4096

  openssl req -new -key "${i}.key" -sha256 \
    -config "ca.conf" -section ${i} \
    -out "${i}.csr"

  openssl x509 -req -days 3653 -in "${i}.csr" \
    -copy_extensions copyall \
    -sha256 -CA "ca.crt" \
    -CAkey "ca.key" \
    -CAcreateserial \
    -out "${i}.crt"
done

ls -1 *.crt *.key *.csr 可见每个组件的私钥、CSR 与已签发证书。

6.4 证书分发

证书是各组件的认证凭据,应按敏感信息对待。工作节点上 Kubelet 的证书统一重命名为 kubelet.crt / kubelet.key

for host in node-0 node-1; do
  ssh root@${host} mkdir /var/lib/kubelet/
  scp ca.crt root@${host}:/var/lib/kubelet/
  scp ${host}.crt root@${host}:/var/lib/kubelet/kubelet.crt
  scp ${host}.key root@${host}:/var/lib/kubelet/kubelet.key
done

server 上放置 CA 密钥对、API Server 证书与 service-accounts 密钥对:

scp ca.key ca.crt kube-api-server.key kube-api-server.crt \
  service-accounts.key service-accounts.crt root@server:~/

注意 ca.key 只出现在 server 上——因为只有它需要签发/验证;kube-proxy、controller-manager、scheduler、kubelet 的客户端证书留给下一实验生成 kubeconfig。

七、六份 kubeconfig:组件如何认证到 API Server

docs/05-kubernetes-configuration-files.mdkubectl config 子命令为 6 个身份各生成一份 kubeconfig:kubelet(node-0 / node-1 各一份)、kube-proxy、kube-controller-manager、kube-scheduler、admin。每份 kubeconfig 都由四步组成,这是 Kubernetes 客户端配置的通用范式:

  1. set-cluster:指定 CA 证书(--embed-certs=true 内嵌)+ API Server 地址;
  2. set-credentials:绑定客户端证书与私钥;
  3. set-context:绑定 cluster 与 user;
  4. use-context:设为默认上下文。

Kubelet 的生成逻辑值得注意:用户名必须是 system:node:${host} 且使用与该节点同名的证书,这样才能被 Node Authorizer 正确授权:

for host in node-0 node-1; do
  kubectl config set-cluster kubernetes-the-hard-way \
    --certificate-authority=ca.crt \
    --embed-certs=true \
    --server=https://server.kubernetes.local:6443 \
    --kubeconfig=${host}.kubeconfig

  kubectl config set-credentials system:node:${host} \
    --client-certificate=${host}.crt \
    --client-key=${host}.key \
    --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 / kube-controller-manager / kube-scheduler 三段的唯一区别是用户名与证书文件名,例如:

kubectl config set-credentials system:kube-proxy \
  --client-certificate=kube-proxy.crt \
  --client-key=kube-proxy.key \
  --embed-certs=true \
  --kubeconfig=kube-proxy.kubeconfig

admin 的 kubeconfig 有两个不同点:API Server 地址写 https://127.0.0.1:6443(本机回环访问),用户为 admin

kubectl config set-credentials admin \
  --client-certificate=admin.crt \
  --client-key=admin.key \
  --embed-certs=true \
  --kubeconfig=admin.kubeconfig

分发规则

  • node-0 / node-1:kubelet 的放到 /var/lib/kubelet/kubeconfig(注意每台机器用自己的 ${host}.kubeconfig),kube-proxy 的放到 /var/lib/kube-proxy/kubeconfig
  • serveradmin.kubeconfigkube-controller-manager.kubeconfigkube-scheduler.kubeconfig 放 home 目录,后续实验会移入 /var/lib/kubernetes/

八、Secrets 静态加密

docs/06-data-encryption-keys.md 利用 Kubernetes 的 at-rest 加密能力保护 etcd 中的 Secrets:

export ENCRYPTION_KEY=$(head -c 32 /dev/urandom | base64)

envsubst < configs/encryption-config.yaml > encryption-config.yaml

scp encryption-config.yaml root@server:~/

仓库中的模板 configs/encryption-config.yaml 内容很短但每一行都有含义:

kind: EncryptionConfiguration
apiVersion: apiserver.config.k8s.io/v1
resources:
  - resources:
      - secrets
    providers:
      - aescbc:
          keys:
            - name: key1
              secret: ${ENCRYPTION_KEY}
      - identity: {}
  • 只对 secrets 资源生效;
  • 首选 aescbc 提供程序,密钥名 key1ENCRYPTION_KEY 环境变量由 envsubst 替换进来,32 字节随机数 base64 编码,正好是 AES-256 密钥长度);
  • 兜底 identity(明文),保证老数据可读。

冒烟测试阶段会验证这一配置:etcd 中 Secret 的 value 会以 k8s:enc:aescbc:v1:key1: 前缀开头(见第十二章)。

九、etcd:集群状态的唯一落盘点

docs/07-bootstrapping-etcd.md 强调「Kubernetes components are stateless and store cluster state in etcd」。本教程部署单节点 etcd。

9.1 安装与配置

scp downloads/controller/etcd downloads/client/etcdctl units/etcd.service root@server:~/
ssh root@server

{
  mv etcd etcdctl /usr/local/bin/
}

{
  mkdir -p /etc/etcd /var/lib/etcd
  chmod 700 /var/lib/etcd
  cp ca.crt kube-api-server.key kube-api-server.crt /etc/etcd/
}

mv etcd.service /etc/systemd/system/

数据目录权限收紧到 700,并预置了 CA 与 API Server 证书(为将来 etcd 启用 TLS 留好位置)。

9.2 systemd unit 中的启动参数

units/etcd.service 完整展示了单节点 etcd 的引导参数:

[Service]
Type=notify
ExecStart=/usr/local/bin/etcd \
  --name controller \
  --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

几个参数要点:

  • --name controller:每个 etcd 成员在集群内必须有唯一名,这里与主机名/节点角色对应(etcdctl member list 输出中会看到 controller);
  • 监听全部绑定 127.0.0.1:单节点场景下 peer(2380)与 client(2379)端口都不暴露到网络;
  • --initial-cluster-state new:声明这是新建集群;
  • Type=notify + Restart=on-failure:systemd 感知就绪状态,失败 5 秒后自动拉起。

9.3 启动与验证

{
  systemctl daemon-reload
  systemctl enable etcd
  systemctl start etcd
}

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

十、控制面:API Server、Scheduler 与 Controller Manager

docs/08-bootstrapping-kubernetes-controllers.mdserver 上安装三大控制面组件。

10.1 前置文件传输

scp \
  downloads/controller/kube-apiserver \
  downloads/controller/kube-controller-manager \
  downloads/controller/kube-scheduler \
  downloads/client/kubectl \
  units/kube-apiserver.service \
  units/kube-controller-manager.service \
  units/kube-scheduler.service \
  configs/kube-scheduler.yaml \
  configs/kube-apiserver-to-kubelet.yaml \
  root@server:~/

然后在 server 上:创建 /etc/kubernetes/config 目录,把 4 个二进制移入 /usr/local/bin/;把 ca.crt ca.key kube-api-server.key kube-api-server.crt service-accounts.key service-accounts.crt encryption-config.yaml 移入 /var/lib/kubernetes/;三份 kubeconfig(admin / controller-manager / scheduler)也落到 /var/lib/kubernetes/kube-scheduler.yaml 落到 /etc/kubernetes/config/;三个 unit 文件移入 /etc/systemd/system/

10.2 kube-apiserver 的完整启动参数

units/kube-apiserver.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.crt \
  --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.crt \
  --kubelet-client-certificate=/var/lib/kubernetes/kube-api-server.crt \
  --kubelet-client-key=/var/lib/kubernetes/kube-api-server.key \
  --runtime-config='api/all=true' \
  --service-account-key-file=/var/lib/kubernetes/service-accounts.crt \
  --service-account-signing-key-file=/var/lib/kubernetes/service-accounts.key \
  --service-account-issuer=https://server.kubernetes.local:6443 \
  --service-node-port-range=30000-32767 \
  --tls-cert-file=/var/lib/kubernetes/kube-api-server.crt \
  --tls-private-key-file=/var/lib/kubernetes/kube-api-server.key \
  --v=2

参数分组解读:

  • 认证--client-ca-file 指向第六章生成的 CA 证书,客户端 x509 认证由它校验;--service-account-signing-key-file / --service-account-key-file 正是 service-accounts 那对密钥,用于签发/验证 ServiceAccount token,--service-account-issuer 声明 issuer 为 https://server.kubernetes.local:6443
  • 授权--authorization-mode=Node,RBAC——Node Authorizer 负责 Kubelet 请求(呼应 ca.conf 中 system:nodes 组的设计),RBAC 负责其余;
  • 准入--enable-admission-plugins 启用 6 个内置准入控制器,其中 NodeRestriction 限制 Kubelet 只能写自己节点的资源;
  • 存储--etcd-servers=http://127.0.0.1:2379 对应第九章的单节点 etcd;--encryption-provider-config 挂载第八章的加密配置;
  • 与 Kubelet 通信--kubelet-client-certificatekube-api-server 证书访问各节点 10250 端口的 Kubelet API(--kubelet-certificate-authority 用 CA 校验 Kubelet 服务端证书);
  • 网络--bind-address=0.0.0.0 使 API Server 可被远程访问(配合 ca.conf 里 server.kubernetes.local 的 SAN),--service-node-port-range=30000-32767 是 NodePort 默认范围。

10.3 scheduler 配置与启动

configs/kube-scheduler.yaml 采用声明式配置而非命令行参数:

apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
clientConnection:
  kubeconfig: "/var/lib/kubernetes/kube-scheduler.kubeconfig"
leaderElection:
  leaderElect: true

leaderElect: true 表明该 unit 天然支持多实例部署,由 API Server 的选主机制保证单活——单节点教程中它只是空转的保险丝。

启动三个服务并做状态检查(等约 10 秒让 API Server 完全初始化):

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

systemctl is-active kube-apiserver
systemctl status kube-apiserver
journalctl -u kube-apiserver

用 admin kubeconfig 验证控制面可达:

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

10.4 Kubelet 授权的 RBAC 补丁

API Server 要通过 Webhook 模式(SubjectAccessReview API)访问各节点 Kubelet API 来取 metrics、日志、执行 pod 内命令,因此需要一个集群级授权。教程应用 configs/kube-apiserver-to-kubelet.yaml

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: ["*"]
---
# ClusterRoleBinding: 绑定到 User "kubernetes"(API Server 证书 CN=kubernetes)
kubectl apply -f kube-apiserver-to-kubelet.yaml --kubeconfig admin.kubeconfig

10.5 外部视角验证

从 jumpbox 直接对 API Server 发未认证请求也能拿到版本信息(/version 是公开的):

curl --cacert ca.crt https://server.kubernetes.local:6443/version
{
  "major": "1",
  "minor": "32",
  "gitVersion": "v1.32.3",
  ...
}

十一、工作节点:runc、CNI、containerd、kubelet 与 kube-proxy

docs/09-bootstrapping-kubernetes-workers.mdnode-0 / node-1 上安装完整的容器运行时栈。

11.1 模板化下发配置(jumpbox 侧)

每个工作节点需要不同的 Pod 网段,教程用 sedSUBNET 占位符替换为该节点在 machines.txt 中的第四列:

for HOST in node-0 node-1; do
  SUBNET=$(grep ${HOST} machines.txt | cut -d " " -f 4)
  sed "s|SUBNET|$SUBNET|g" configs/10-bridge.conf > 10-bridge.conf
  sed "s|SUBNET|$SUBNET|g" configs/kubelet-config.yaml > kubelet-config.yaml
  scp 10-bridge.conf kubelet-config.yaml root@${HOST}:~/
done

然后统一下发 downloads/worker/*、kubectl、configs/99-loopback.confconfigs/containerd-config.tomlconfigs/kube-proxy-config.yaml、三个 unit 文件与 CNI 插件目录。

11.2 节点侧安装步骤

{
  apt-get update
  apt-get -y install socat conntrack ipset kmod
}

socat 支撑 kubectl port-forwardconntrack/ipset 是 kube-proxy iptables 模式的依赖。

必须关闭 swap——Kubernetes 对 swap 的支持有限,存在 swap 时难以对 Pod 内存使用做保证与核算:

swapon --show   # 输出为空即已禁用
swapoff -a

创建目录并安装二进制:

mkdir -p /etc/cni/net.d /opt/cni/bin /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/
}

11.3 CNI bridge 网络

mv 10-bridge.conf 99-loopback.conf /etc/cni/net.d/

configs/10-bridge.conf 是每节点一份的 CNI 配置,SUBNET 已被替换为该节点专属网段:

{
  "cniVersion": "1.0.0",
  "name": "bridge",
  "type": "bridge",
  "bridge": "cni0",
  "isGateway": true,
  "ipMasq": true,
  "ipam": {
    "type": "host-local",
    "ranges": [
      [{"subnet": "SUBNET"}]
    ],
    "routes": [{"dst": "0.0.0.0/0"}]
  }
}

要点:bridge 插件创建 cni0 网桥并作为 Pod 网关(isGateway),ipMasq 为出集群流量做 SNAT;host-local IPAM 从本节点网段给每个 Pod 发 IP,默认路由 0.0.0.0/0 指向网桥网关。

让穿越 CNI bridge 的流量经过 iptables,需加载 br-netfilter 并设置 sysctl:

{
  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
}

11.4 containerd / kubelet / kube-proxy 配置

mkdir -p /etc/containerd/
mv containerd-config.toml /etc/containerd/config.toml
mv containerd.service /etc/systemd/system/
mv kubelet-config.yaml /var/lib/kubelet/
mv kubelet.service /etc/systemd/system/
mv kube-proxy-config.yaml /var/lib/kube-proxy/
mv kube-proxy.service /etc/systemd/system/

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

configs/kubelet-config.yaml 是 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.crt"
authorization:
  mode: Webhook
cgroupDriver: systemd
containerRuntimeEndpoint: "unix:///var/run/containerd/containerd.sock"
enableServer: true
failSwapOn: false
maxPods: 16
memorySwap:
  swapBehavior: NoSwap
port: 10250
resolvConf: "/etc/resolv.conf"
registerNode: true
runtimeRequestTimeout: "15m"
tlsCertFile: "/var/lib/kubelet/kubelet.crt"
tlsPrivateKeyFile: "/var/lib/kubelet/kubelet.key"

关键项:containerRuntimeEndpoint 指向 containerd 的 CRI socket(不再走 dockershim);authentication.webhook + authorization: Webhook 表示 Kubelet 把认证/授权决策回调给 API Server(呼应第十章的 system:kube-apiserver-to-kubelet ClusterRole);registerNode: true 让 kubelet 自动向集群注册 Node 对象;tlsCertFile/tlsPrivateKeyFile 正是第六章分发的 kubelet.crt/key

configs/kube-proxy-config.yaml 则声明 iptables 代理模式与集群网段:

kind: KubeProxyConfiguration
apiVersion: kubeproxy.config.k8s.io/v1alpha1
clientConnection:
  kubeconfig: "/var/lib/kube-proxy/kubeconfig"
mode: "iptables"
clusterCIDR: "10.200.0.0/16"

clusterCIDR: 10.200.0.0/16 覆盖了 10.200.0.0/24(node-0)与 10.200.1.0/24(node-1)两个 Pod 网段,kube-proxy 据此为 Service 编写 iptables 规则。

两台节点都要完成后,从 jumpbox 验证节点注册:

ssh root@server "kubectl get nodes --kubeconfig admin.kubeconfig"
NAME     STATUS   ROLES    AGE    VERSION
node-0   Ready    <none>   1m     v1.32.3
node-1   Ready    <none>   10s    v1.32.3

十二、kubectl 远程访问与 Pod 网络路由

12.1 jumpbox 上的 kubectl

docs/10-configuring-kubectl.md 先用 curl --cacert ca.crt https://server.kubernetes.local:6443/version 确认网络可达,再生成默认位置的 ~/.kube/config(注意这里没有 --kubeconfig 参数、--embed-certs 只用于 cluster 段):

{
  kubectl config set-cluster kubernetes-the-hard-way \
    --certificate-authority=ca.crt \
    --embed-certs=true \
    --server=https://server.kubernetes.local:6443

  kubectl config set-credentials admin \
    --client-certificate=admin.crt \
    --client-key=admin.key

  kubectl config set-context kubernetes-the-hard-way \
    --cluster=kubernetes-the-hard-way \
    --user=admin

  kubectl config use-context kubernetes-the-hard-way
}

kubectl version
kubectl get nodes
Client Version: v1.32.3
Kustomize Version: v5.5.0
Server Version: v1.32.3
NAME     STATUS   ROLES    AGE     VERSION
node-0   Ready    <none>   10m     v1.32.3
node-1   Ready    <none>   10m     v1.32.3

12.2 跨节点 Pod 路由

docs/11-pod-network-routes.md 解决的问题:Pod 各自拿到了本节点网段的 IP,但节点之间还没有指向对方 Pod 网段的路由。教程选择最朴素的方式——手工 ip route add(并明确注明「There are other ways to implement the Kubernetes networking model」,即本教程用静态路由代替了 Flannel/Calico 这类路由自动发现方案):

{
  SERVER_IP=$(grep server machines.txt | cut -d " " -f 1)
  NODE_0_IP=$(grep node-0 machines.txt | cut -d " " -f 1)
  NODE_0_SUBNET=$(grep node-0 machines.txt | cut -d " " -f 4)
  NODE_1_IP=$(grep node-1 machines.txt | cut -d " " -f 1)
  NODE_1_SUBNET=$(grep node-1 machines.txt | cut -d " " -f 4)
}

ssh root@server <<EOF
  ip route add ${NODE_0_SUBNET} via ${NODE_0_IP}
  ip route add ${NODE_1_SUBNET} via ${NODE_1_IP}
EOF

ssh root@node-0 <<EOF
  ip route add ${NODE_1_SUBNET} via ${NODE_1_IP}
EOF

ssh root@node-1 <<EOF
  ip route add ${NODE_0_SUBNET} via ${NODE_0_IP}
EOF

规则很直观:每台机器为「别人的 Pod 网段」加一条指向「该节点自身 IP」的下一跳。用 ssh root@server ip route 应看到:

default via XXX.XXX.XXX.XXX dev ens160
10.200.0.0/24 via XXX.XXX.XXX.XXX dev ens160
10.200.1.0/24 via XXX.XXX.XXX.XXX dev ens160
XXX.XXX.XXX.0/24 dev ens160 proto kernel scope link src XXX.XXX.XXX.XXX

十三、冒烟测试:验证集群六大核心能力

docs/12-smoke-test.md 按能力维度逐项验证。

13.1 数据加密验证

kubectl create secret generic kubernetes-the-hard-way \
  --from-literal="mykey=mydata"

ssh root@server \
  'etcdctl get /registry/secrets/default/kubernetes-the-hard-way | hexdump -C'

期望看到 value 以 k8s:enc:aescbc:v1:key1: 开头——这正是第八章配置的 aescbc 提供程序 + key1 密钥的指纹,证明 Secrets 在 etcd 中已是密文(mydata 明文不会出现在 hexdump 里)。

13.2 Deployment

kubectl create deployment nginx --image=nginx:latest
kubectl get pods -l app=nginx
NAME                     READY   STATUS    RESTARTS   AGE
nginx-56fcf95486-c8dnx   1/1     Running   0          8s

13.3 Port Forwarding

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

# 新终端:
curl --head http://127.0.0.1:8080
HTTP/1.1 200 OK
Server: nginx/1.27.4

(该能力依赖工作节点上安装的 socat。)

13.4 Logs 与 Exec

kubectl logs $POD_NAME
# 127.0.0.1 - - [06/Apr/2025:17:17:12 +0000] "HEAD / HTTP/1.1" 200 0 ...

kubectl exec -ti $POD_NAME -- nginx -v
# nginx version: nginx/1.27.4

13.5 Service(NodePort)

kubectl expose deployment nginx --port 80 --type NodePort

NODE_PORT=$(kubectl get svc nginx --output=jsonpath='{range .spec.ports[0]}{.nodePort}')
NODE_NAME=$(kubectl get pods -l app=nginx -o jsonpath="{.items[0].spec.nodeName}")
curl -I http://${NODE_NAME}:${NODE_PORT}
Server: nginx/1.27.4
Content-Type: text/html
...

文档同时解释了为什么不用 LoadBalancer:本集群没有配置云 provider 集成,这超出了教程范围。

十四、清理与适用边界

docs/13-cleanup.md 指出当前版本已云厂商无关("the current version is agnostic, and all configuration is performed on the jumpbox, server, or nodes"),清理就是删掉为实验创建的全部 4 台虚拟机。

最后重申三条适用边界,避免把学习集群当生产集群:

  1. 非生产就绪:单节点 etcd 与单节点控制面无高可用,自签名 CA,root SSH 全局开启,静态路由手工维护——README 已明确声明不作为生产方案;
  2. 版本绑定:整套命令与配置针对 Kubernetes v1.32.3、containerd v2.1.0-beta.0、etcd v3.6.0-rc.3、CNI v1.6.2,升级大版本时需重新核对各二进制的启动参数是否仍有效;
  3. 架构支持:AMD64 与 ARM64 均可,但二进制清单(downloads-amd64.txt / downloads-arm64.txt)与机器架构必须一致。

十五、结语:这条「长路」教会你什么

kubernetes-the-hard-way 的价值不在结果,而在过程。完成全部 13 个实验后,你实际上亲手回答了一系列「黑盒问题」:

  • API Server 的 --client-ca-file 从哪里来?——ca.conf 自签 CA + openssl 签发链;
  • Kubelet 为什么能被 Node Authorizer 信任?——因为证书 CN=system:node:<hostname>O=system:nodes
  • kubectl port-forward 的链路在哪里断开?——工作节点缺 socat
  • Secrets 为什么在 etcd 里读不到明文?——encryption-provider-config 里的 aescbc + key1;
  • 跨节点 Pod 通信为什么一开始不通?——ip route 里没有对方 POD_SUBNET 的下一跳。

仓库的每个文件都是答案的一部分:ca.conf 是身份体系,units/ 是启动参数全集,configs/ 是声明式配置基线,docs/ 是操作顺序。按照本文的路径逐项执行与核对,你得到的不仅是一个可运行的 v1.32 集群,更是一整套可以迁移到 kubeadm 之外任何手工部署场景的 Kubernetes 底层知识。

登录后查看全文
热门项目推荐
相关项目推荐