kind:把一次性 Kubernetes 集群变成可重复的开发证据
“我的 YAML 没问题”,为什么进 CI 就坏了
开发机上的容器已经能返回 200,Deployment 也通过了编辑器校验;进入 CI 后,Pod 却停在 ErrImageNeverPull,或者 Service 创建成功但端口始终不通。更麻烦的是,大家反复删除 Pod、重跑流水线,最后谁也说不清失败来自 YAML、镜像、节点,还是宿主机网络。
kind 很适合处理这种现场。它把 Kubernetes 节点运行成宿主容器,在这些节点里启动 kubelet、containerd、API Server 等组件。集群创建快、拓扑能写进 YAML、删除后不留长期控制面,所以特别适合验证清单、控制器、Helm Chart 和 CI 集成。它并不会把一台开发机变成生产集群:多个 worker 仍共享同一宿主内核、磁盘、网络和故障域,LoadBalancer、存储、云身份、硬件拓扑也与真实环境不同。
先记住一条排障主线:
“宿主机有镜像”与“节点 containerd 有镜像”是两个状态;“Service 存在”与“Service 有 Ready 后端”也是两个状态。后面的实验会故意打断这两条链路,再用状态字段和事件把原因找回来。
安装时同时固定工具和节点版本
kind 可运行在 Linux、macOS 和 Windows,宿主机还需要 Docker、Podman 或 nerdctl 等受支持的容器提供者。最稳妥的团队入口是从 kind Quick Start 下载发布二进制并校验发布资产;Homebrew、Chocolatey、Scoop、Winget 等包管理入口由社区维护,企业镜像仓库要单独记录来源与校验值。kind 使用 Apache-2.0 许可证,不收取软件许可费,真实成本来自 Docker Desktop 许可与资源、CI 分钟、镜像流量和维护时间。
Windows 可以使用官方文档列出的 Winget 包;安装后重新打开终端,让 PATH 生效:
winget install Kubernetes.kind
kind versionmacOS 使用 Homebrew 时执行:
brew install kind
kind versionLinux amd64 若直接使用发布二进制,可以把版本写进命令,随后对照 Release Assets 提供的 SHA-256 再放入 PATH:
KIND_VERSION=v0.32.0
curl -fsSLO "https://github.com/kubernetes-sigs/kind/releases/download/${KIND_VERSION}/kind-linux-amd64"
curl -fsSLO "https://github.com/kubernetes-sigs/kind/releases/download/${KIND_VERSION}/kind-linux-amd64.sha256sum"
sha256sum -c kind-linux-amd64.sha256sum
chmod +x kind-linux-amd64
sudo install -m 0755 kind-linux-amd64 /usr/local/bin/kind
kind versionsha256sum -c 必须返回 kind-linux-amd64: OK;只打印本地摘要却不与发布资产比较,不能证明下载完整。不一致时删除文件并检查下载代理、缓存和来源,不要继续安装。ARM64 要改用对应二进制和校验文件,不能在不同架构间复用。kubectl 从 Install Tools 安装,避免把 Docker Desktop 附带的旧客户端默认为团队基线。
安装后先确认三个版本:
kind version
docker version
kubectl version --clientkind version 应返回固定发布版而不是自行编译的未知提交;docker version 必须同时出现 Client 与 Server,否则 kind 只有命令行却没有可用容器守护进程。kubectl 与 kube-apiserver 的受支持偏差是一个 minor 版本,团队升级 Kubernetes 节点镜像时,应一起检查 Kubernetes version skew policy。
CI 不要跟随 latest。kind v0.32.0 默认节点是 Kubernetes v1.36.1,但这里显式选择同一发布说明列出的 kindest/node:v1.35.5@sha256:ce977ae6d65918d0b58a5f8b5e940429c2ce42fa3a5619ec2bbc60b949c0ac95,使本机 kubectl v1.34 仍处于受支持的一个 minor 偏差内。kind 二进制、kindest/node 和 kubectl 是三个独立版本面,只固定其中一个仍可能让同一提交隔天跑出不同结果;升级时应从目标 kind 的 Release Notes 重新选择完整镜像摘要。
企业代理环境还要让容器提供者能够访问镜像仓库。kind 会读取 HTTP_PROXY、HTTPS_PROXY 和 NO_PROXY 并传入节点,但内部 API、Service 网段和私有 registry 地址不能被错误送进代理。离线机器应按 Working Offline 先导入带摘要的 node image,再用 kind create cluster --image ... 创建;业务镜像和测试依赖也必须一并进入离线清单,只有 node image 并不足以完成部署。
用配置文件把拓扑和宿主端口变成项目契约
在项目里创建 k8s/kind/cluster.yaml:
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
extraPortMappings:
- containerPort: 30080
hostPort: 18080
listenAddress: "127.0.0.1"
protocol: TCP
- role: worker
- role: workerapiVersion 决定 kind 如何解释配置字段;当前配置 API 是 kind.x-k8s.io/v1alpha4,升级 kind 时要先用临时集群验证。role 只决定节点承担 control-plane 还是 worker 角色,不会产生独立宿主故障域。extraPortMappings 把控制面节点容器的 30080 映射到宿主 127.0.0.1:18080,它是 Docker 层端口映射,不是 Kubernetes Service。绑定回环地址可避免演示服务意外暴露到局域网。
创建命令显式给出名称和配置:
kind create cluster --name tooling-dev \
--image kindest/node:v1.35.5@sha256:ce977ae6d65918d0b58a5f8b5e940429c2ce42fa3a5619ec2bbc60b949c0ac95 \
--config k8s/kind/cluster.yaml --wait 120s
kubectl cluster-info --context kind-tooling-dev
kubectl --context kind-tooling-dev get nodes -o wide第一条命令创建三个节点并等待控制面 Ready;成功时会出现 Set kubectl context to "kind-tooling-dev"。后两条命令不依赖当前终端的 context,能证明 API Server 可达且三节点均为 Ready。若停在 Ensuring node image,先查 registry、代理和磁盘;若节点容器已启动但 --wait 超时,立即保留证据:
kind export logs ./artifacts/kind-bootstrap --name tooling-dev
docker ps -a --filter name=tooling-dev
kubectl --context kind-tooling-dev get pods -A -o wide导出目录包含节点 inspect、kubelet、journal 和 Pod 日志。不要先删除集群,否则最有价值的启动现场会一起消失。日志可能包含环境变量、镜像地址和挂载路径,上传 CI artifact 前要限制可见范围并做敏感字段扫描。
先构建镜像,再证明它真的进入了节点
下面的应用只返回一个静态页面,足以验证构建、导入、调度、Service 和端口。项目根目录放置 Dockerfile.kind-lab:
FROM python:3.13-alpine
WORKDIR /srv
RUN printf 'kind-local-image-ok\n' > index.html
EXPOSE 8080
USER 65532:65532
CMD ["python", "-m", "http.server", "8080", "--bind", "0.0.0.0"]构建时使用唯一 tag,不用 latest:
docker build -f Dockerfile.kind-lab -t local/kind-lab:dev-001 .
kind load docker-image local/kind-lab:dev-001 --name tooling-dev
docker exec tooling-dev-worker crictl images | grep kind-labkind load 会把宿主镜像保存并导入每个目标节点的 containerd。最后一条命令应显示 local/kind-lab 与 dev-001;这比只看 docker images 更接近 kubelet 实际看到的镜像状态。使用命名集群却漏掉 --name tooling-dev,是镜像被装进另一个集群的常见原因。
项目的 k8s/kind/lab.yaml 可以同时建立资源边界和访问入口:
apiVersion: v1
kind: Namespace
metadata:
name: tooling-dev
labels:
owner: local-platform
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: namespace-budget
namespace: tooling-dev
spec:
hard:
requests.cpu: "1"
requests.memory: 512Mi
limits.cpu: "2"
limits.memory: 1Gi
pods: "10"
---
apiVersion: v1
kind: LimitRange
metadata:
name: container-defaults
namespace: tooling-dev
spec:
limits:
- type: Container
defaultRequest:
cpu: 50m
memory: 32Mi
default:
cpu: 200m
memory: 128Mi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: kind-lab
namespace: tooling-dev
spec:
replicas: 2
selector:
matchLabels:
app: kind-lab
template:
metadata:
labels:
app: kind-lab
spec:
containers:
- name: app
image: local/kind-lab:dev-001
imagePullPolicy: Never
ports:
- name: http
containerPort: 8080
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 1
periodSeconds: 2
resources:
requests:
cpu: 50m
memory: 32Mi
limits:
cpu: 200m
memory: 128Mi
---
apiVersion: v1
kind: Service
metadata:
name: kind-lab
namespace: tooling-dev
spec:
type: NodePort
selector:
app: kind-lab
ports:
- name: http
port: 80
targetPort: http
nodePort: 30080imagePullPolicy: Never 把实验收紧为“节点必须已有这一 tag”,避免 registry 恰好可访问而掩盖导入步骤。生产清单通常使用可验证 registry 与 IfNotPresent/摘要,不应照搬这个本地策略。requests 参与调度,limits 由运行时和 cgroup 执行;ResourceQuota 约束 namespace 总预算,却不能增加宿主机真实容量。
部署并逐层验证:
kubectl --context kind-tooling-dev apply -f k8s/kind/lab.yaml
kubectl --context kind-tooling-dev -n tooling-dev rollout status deploy/kind-lab --timeout=90s
kubectl --context kind-tooling-dev -n tooling-dev get pods -o wide
kubectl --context kind-tooling-dev -n tooling-dev get endpointslice -l kubernetes.io/service-name=kind-lab
curl http://127.0.0.1:18080预期 Deployment 完成 rollout,两个 Pod 分布结果可从 NODE 列看到,EndpointSlice 的 endpoints 中出现 Ready 地址,最后返回 kind-local-image-ok。如果 Pod 已 Ready 而 curl 失败,应沿 host 18080 -> 节点容器 30080 -> NodePort -> Service -> EndpointSlice -> Pod 8080 逐段检查,不能只反复重启应用。
反向实验:让节点找不到镜像
把 Deployment 改成一个没有导入、也不允许拉取的 tag:
kubectl --context kind-tooling-dev -n tooling-dev set image deploy/kind-lab app=local/kind-lab:missing
kubectl --context kind-tooling-dev -n tooling-dev rollout status deploy/kind-lab --timeout=20s
kubectl --context kind-tooling-dev -n tooling-dev get pods
kubectl --context kind-tooling-dev -n tooling-dev describe pod -l app=kind-labrollout 应超时,新 Pod 的状态通常为 ErrImageNeverPull,事件里出现容器镜像不在节点且拉取策略为 Never 的证据。这个结果同时排除了 DNS、registry 凭证和公网故障,因为 kubelet 根本没有尝试远程拉取。
修复时重新指向已导入 tag,并等待新 ReplicaSet 接管:
kubectl --context kind-tooling-dev -n tooling-dev set image deploy/kind-lab app=local/kind-lab:dev-001
kubectl --context kind-tooling-dev -n tooling-dev rollout status deploy/kind-lab --timeout=90s
kubectl --context kind-tooling-dev -n tooling-dev get rs,pods若团队选择 IfNotPresent,镜像名、tag 与节点缓存仍必须一致;若选择远程 registry,还要继续检查 DNS、TLS、认证和网络。三类失败不能混为一个 ImagePullBackOff。
反向实验:让调度器明确拒绝容量请求
新建一个请求 100 CPU 的 Pod,不需要真的运行镜像:
apiVersion: v1
kind: Pod
metadata:
name: impossible-cpu
namespace: tooling-dev
spec:
containers:
- name: app
image: local/kind-lab:dev-001
imagePullPolicy: Never
resources:
requests:
cpu: "100"
memory: 16Mi
limits:
cpu: "100"
memory: 32Mi将它保存为 k8s/kind/impossible-cpu.yaml 后执行:
kubectl --context kind-tooling-dev apply -f k8s/kind/impossible-cpu.yaml
kubectl --context kind-tooling-dev -n tooling-dev describe pod impossible-cpu
kubectl --context kind-tooling-dev -n tooling-dev get events --sort-by=.lastTimestamp如果 ResourceQuota 先拒绝创建,会直接得到 exceeded quota,说明 admission 在对象落库前阻断了请求;这也是有效证据。若临时删除或调高 quota,Pod 会停在 Pending,事件出现 FailedScheduling 与 Insufficient cpu。前者是 namespace 预算拒绝,后者是节点可分配量不足,修复责任完全不同。实验结束后删除 Pod,不要通过调高宿主资源把一个故意失败的对象“修好”。
kubectl --context kind-tooling-dev -n tooling-dev delete pod impossible-cpu --ignore-not-found端口、Ingress 和 registry 是三条不同入口
NodePort 配合 extraPortMappings 最容易跨 Docker Desktop、Windows 和 macOS 重复,适合固定的单服务开发入口。临时调试则用:
kubectl --context kind-tooling-dev -n tooling-dev port-forward svc/kind-lab 18081:80它会占用当前终端并在中断后撤销,适合定位 Service 后端,不适合团队长期共享。
需要验证 Ingress 对象时,kind 的 Ingress 指南 使用 Cloud Provider KIND。当前稳定版 v0.10.0 修复了 macOS/Windows 的 Ingress 路径,但版本仍应由团队升级 PR 显式调整。该进程运行在宿主机,监听 kind 集群,为 LoadBalancer Service 创建额外负载均衡容器,并为 Ingress 提供入口;它需要访问容器运行时和打开宿主端口的权限。Linux amd64 可以校验发布归档后安装:
CPK_VERSION=0.10.0
curl -fsSLO "https://github.com/kubernetes-sigs/cloud-provider-kind/releases/download/v${CPK_VERSION}/cloud-provider-kind_${CPK_VERSION}_linux_amd64.tar.gz"
curl -fsSLO "https://github.com/kubernetes-sigs/cloud-provider-kind/releases/download/v${CPK_VERSION}/cloud-provider-kind_${CPK_VERSION}_checksums.txt"
grep "cloud-provider-kind_${CPK_VERSION}_linux_amd64.tar.gz" \
"cloud-provider-kind_${CPK_VERSION}_checksums.txt" | sha256sum -c -
tar -xzf "cloud-provider-kind_${CPK_VERSION}_linux_amd64.tar.gz"
sudo install -m 0755 cloud-provider-kind /usr/local/bin/cloud-provider-kind
cloud-provider-kind version
cloud-provider-kind保持进程运行,再应用 Ingress:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: kind-lab
namespace: tooling-dev
spec:
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: kind-lab
port:
number: 80kubectl get ingress -n tooling-dev 的 ADDRESS 应出现地址,再对该地址执行 curl。若地址长期为空,先确认 cloud-provider-kind 进程、版本和权限,而不是给 Ingress 乱加 annotation。rootless 容器提供者还需要按 kind 的 rootless 指南处理 cgroup 与低端口限制。
偶尔构建一次镜像时,kind load 最直接;频繁构建、多服务联调或 CI 并行时,本地 registry 能避免向每个节点重复导入。kind 的 Local Registry 方案会启动 registry 容器、把它接入 kind 网络,并在节点的 containerd hosts.toml 中把宿主看到的 localhost:5001 映射到 registry 容器。这里的 localhost 在宿主、节点和 Pod 中并不是同一个网络命名空间,不能只启动 registry:3 就假定节点可拉取。
下面把 registry 接到已经创建的 tooling-dev 集群;registry 只监听宿主回环地址,节点通过容器名访问它:
REGISTRY_NAME=kind-registry
REGISTRY_PORT=5001
docker run -d --restart=always \
-p "127.0.0.1:${REGISTRY_PORT}:5000" \
--name "${REGISTRY_NAME}" registry:3
docker network connect kind "${REGISTRY_NAME}"
for node in $(kind get nodes --name tooling-dev); do
registry_dir="/etc/containerd/certs.d/localhost:${REGISTRY_PORT}"
docker exec "${node}" mkdir -p "${registry_dir}"
cat <<EOF | docker exec -i "${node}" cp /dev/stdin "${registry_dir}/hosts.toml"
[host."http://${REGISTRY_NAME}:5000"]
EOF
done
cat <<EOF | kubectl --context kind-tooling-dev apply -f -
apiVersion: v1
kind: ConfigMap
metadata:
name: local-registry-hosting
namespace: kube-public
data:
localRegistryHosting.v1: |
host: "localhost:${REGISTRY_PORT}"
help: "https://kind.sigs.k8s.io/docs/user/local-registry/"
EOF构建、推送并让工作负载从 registry 拉取一个新 tag:
docker build -f Dockerfile.kind-lab \
-t "localhost:${REGISTRY_PORT}/kind-lab:registry-001" .
docker push "localhost:${REGISTRY_PORT}/kind-lab:registry-001"
kubectl --context kind-tooling-dev -n tooling-dev set image \
deploy/kind-lab app="localhost:${REGISTRY_PORT}/kind-lab:registry-001"
kubectl --context kind-tooling-dev -n tooling-dev patch deploy kind-lab \
--type strategic -p '{"spec":{"template":{"spec":{"containers":[{"name":"app","imagePullPolicy":"IfNotPresent"}]}}}}'
kubectl --context kind-tooling-dev -n tooling-dev rollout status \
deploy/kind-lab --timeout=90s
kubectl --context kind-tooling-dev -n tooling-dev get pods \
-l app=kind-lab -o custom-columns=NODE:.spec.nodeName,IMAGE:.spec.containers[0].image,IMAGE_ID:.status.containerStatuses[0].imageID预期 IMAGE 是 localhost:5001/kind-lab:registry-001,IMAGE_ID 能解析到实际摘要。若 push 成功但 Pod ImagePullBackOff,检查 registry 是否接入 kind 网络、节点 hosts.toml 是否存在,以及地址是否误写成节点自己的 localhost:5001。集群实验结束并删除 kind 集群后,再精确删除独立 registry:
docker rm -f "${REGISTRY_NAME}"
docker ps -a --filter "name=^/${REGISTRY_NAME}$"第二条应无结果。registry 中的镜像数据跟随该容器的可写层删除;若团队为 registry 配置命名卷,还必须先明确 owner、保留期和恢复需求,再单独处理该卷。
私有 registry 优先使用 Kubernetes imagePullSecrets,因为它更接近远端集群的工作方式。不要把宿主 ~/.docker/config.json 整体挂载到所有节点:文件可能包含多个仓库的长期凭证,日志导出和节点调试都会扩大暴露面。开发凭证应最小权限、短有效期、可撤销,Secret 清单只放引用,不放真实值。
把 kind 接进项目和 CI
项目至少应固定以下契约:
k8s/kind/cluster.yaml # 节点角色与宿主端口
k8s/kind/lab.yaml # 可重复部署的开发清单
k8s/kind/impossible-cpu.yaml
Dockerfile.kind-lab脚本中的每条 kubectl 命令都显式传 --context kind-tooling-dev 和 -n tooling-dev。这样即使开发者刚操作过别的集群,也不会把本地实验误发到共享环境。kubeconfig 可能含客户端证书和当前 context,不应复制进仓库或 CI artifact;kind 自动生成的管理员凭证只用于这座一次性本地集群。
CI 的价值是每次从空状态验证“创建、构建、导入、部署、取证、删除”。下面的 Bash 片段适合放进已有 Linux Runner 流水线;kind 版本被固定,node image 使用该版本默认的摘要化选择,升级时在一个 PR 内更新并检查 Release Notes:
set -euo pipefail
go install sigs.k8s.io/kind@v0.32.0
export PATH="$(go env GOPATH)/bin:$PATH"
cleanup() {
status=$?
if [ "$status" -ne 0 ]; then
kind export logs ./artifacts/kind --name ci
fi
kind delete cluster --name ci
exit "$status"
}
trap cleanup EXIT
kind create cluster --name ci \
--image kindest/node:v1.35.5@sha256:ce977ae6d65918d0b58a5f8b5e940429c2ce42fa3a5619ec2bbc60b949c0ac95 \
--wait 120s
docker build -f Dockerfile.kind-lab -t local/kind-lab:"$GIT_COMMIT" .
kind load docker-image local/kind-lab:"$GIT_COMMIT" --name ci
sed "s/dev-001/$GIT_COMMIT/g" k8s/kind/lab.yaml | kubectl --context kind-ci apply -f -
kubectl --context kind-ci -n tooling-dev rollout status deploy/kind-lab --timeout=90s
kubectl --context kind-ci -n tooling-dev run smoke --rm -i --restart=Never \
--image=curlimages/curl:8.12.1 -- curl -fsS http://kind-lab流水线还应预拉并固定 smoke 镜像,避免公网波动被误判成应用故障。失败分支先导出日志再删除集群,成功分支只保留必要测试报告。共享 Runner 若复用 Docker daemon,要给集群名、宿主端口和 registry 名增加 Job 唯一前缀,或者保证 Job 隔离;否则并行任务会争抢网络、容器名和端口。
删除之前先回答“要保留什么证据”
只重置业务资源时,删除 namespace 并等待清空:
kubectl --context kind-tooling-dev delete namespace tooling-dev --wait=true
kubectl --context kind-tooling-dev get namespace tooling-dev第二条命令应返回 NotFound。namespace 长期停在 Terminating 时,先检查 finalizer 所属控制器,不要直接清空 finalizers 掩盖清理缺陷。
整座集群重置前先导出需要的日志,再执行:
kind export logs ./artifacts/kind-final --name tooling-dev
kind delete cluster --name tooling-dev
kind get clusters
docker ps -a --filter name=tooling-devkind delete cluster 对不存在的集群也返回成功,便于幂等清理;所以删除是否完整要用后两条命令证明。项目创建的独立 registry 容器、镜像层和日志目录不属于 kind 集群删除动作,必须按 owner 清单单独清理。不要用全局 docker system prune 代替精确删除,它可能清掉其他项目正在使用的缓存和卷。
团队什么时候选择 kind
当目标是快速重建、多节点调度、Kubernetes 控制器测试、Chart/清单回归和 CI 临时集群时,kind 通常是优先选择。它的节点就是容器,启动和销毁成本低,配置文件也很适合进入仓库。需要 VM 内核隔离、长期保留的开发环境、丰富 addon、GUI 入口或方便的 service/tunnel 工作流时,minikube 往往更顺手。
评审 kind 方案时,应把“能验证什么”写成测试断言,而不是把三节点截图当作架构证明。本地通过可以证明 API 对象被目标版本接受、控制器能收敛、调度约束在模拟拓扑中生效、镜像和网络入口按约定工作;它不能证明云负载均衡、真实 CSI、跨机网络、节点宕机、性能容量、安全隔离和升级过程在生产成立。
长期治理的关键不是让每个人记住更多命令,而是固定版本更新节奏、配置 owner、CI 失败取证、镜像来源和清理不变量。每次升级 kind 或 node image,都应重跑正向部署、缺镜像反例、资源拒绝反例和端口访问;若底层 Docker Desktop、Podman、cgroup 或企业代理改变,也按同样证据链回归。这样 kind 才是一套可重复的开发实验装置,而不是“我机器上恰好能跑”的黑盒。
