Kubernetes The Hard Way:无脚本手工引导 Kubernetes v1.32 集群的完整实战教程
本文基于 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 台是用于远程操作的 jumpbox。docs/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 才能注册节点。完整清单:
- Prerequisites — 机器要求
- Setting up the Jumpbox — 管理机与二进制下载
- Provisioning Compute Resources — 机器数据库、SSH 与主机名
- Provisioning the CA and Generating TLS Certificates — PKI 基础设施
- Generating Kubernetes Configuration Files for Authentication — 6 份 kubeconfig
- Generating the Data Encryption Config and Key — Secrets 静态加密
- Bootstrapping the etcd Cluster — 单节点 etcd
- Bootstrapping the Kubernetes Control Plane — API Server/Scheduler/Controller Manager
- Bootstrapping the Kubernetes Worker Nodes — runc/CNI/containerd/kubelet/kube-proxy
- Configuring kubectl for Remote Access — 远程 kubectl
- Provisioning Pod Network Routes — 跨节点 Pod 路由
- Smoke Test — 端到端功能验证
- Cleaning Up — 资源回收
仓库目录结构也与实验一一对应:docs/ 存放 13 篇实验文档,units/ 存放 7 个 systemd unit 文件(etcd.service、kube-apiserver.service、kube-controller-manager.service、kube-scheduler.service、kubelet.service、kube-proxy.service、containerd.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.txt 或 downloads-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@server、ssh root@node-0、ssh 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=admin、O=system:masters——这是集群管理组,kubeconfig 中以admin用户出现;[node-0]/[node-1]:CN=system:node:node-0、O=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-proxy、O=system:node-proxier;[kube-controller-manager]:CN=system:kube-controller-manager、O=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 的 ClusterIP10.32.0.1,以及kubernetes、kubernetes.default.svc、server.kubernetes.local等 6 个 DNS 名。注释说明:API Server 会自动获得集群内 DNS 名kubernetes,关联到 Service 网段10.32.0.0/24的首个 IP10.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.md 用 kubectl config 子命令为 6 个身份各生成一份 kubeconfig:kubelet(node-0 / node-1 各一份)、kube-proxy、kube-controller-manager、kube-scheduler、admin。每份 kubeconfig 都由四步组成,这是 Kubernetes 客户端配置的通用范式:
set-cluster:指定 CA 证书(--embed-certs=true内嵌)+ API Server 地址;set-credentials:绑定客户端证书与私钥;set-context:绑定 cluster 与 user;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;server:admin.kubeconfig、kube-controller-manager.kubeconfig、kube-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提供程序,密钥名key1(ENCRYPTION_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.md 在 server 上安装三大控制面组件。
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-certificate用kube-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.md 在 node-0 / node-1 上安装完整的容器运行时栈。
11.1 模板化下发配置(jumpbox 侧)
每个工作节点需要不同的 Pod 网段,教程用 sed 把 SUBNET 占位符替换为该节点在 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.conf、configs/containerd-config.toml、configs/kube-proxy-config.yaml、三个 unit 文件与 CNI 插件目录。
11.2 节点侧安装步骤
{
apt-get update
apt-get -y install socat conntrack ipset kmod
}
socat 支撑 kubectl port-forward;conntrack/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 台虚拟机。
最后重申三条适用边界,避免把学习集群当生产集群:
- 非生产就绪:单节点 etcd 与单节点控制面无高可用,自签名 CA,root SSH 全局开启,静态路由手工维护——README 已明确声明不作为生产方案;
- 版本绑定:整套命令与配置针对 Kubernetes v1.32.3、containerd v2.1.0-beta.0、etcd v3.6.0-rc.3、CNI v1.6.2,升级大版本时需重新核对各二进制的启动参数是否仍有效;
- 架构支持: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 底层知识。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00