k3d:用 k3s-in-Docker 建立可重复的本地 Kubernetes
一次“镜像明明存在”的本地联调
开发者刚在宿主机执行完 docker build -t checkout-api:dev .,把 Deployment 应用到本地 Kubernetes,Pod 却停在 ImagePullBackOff。另一个人把 Service 改成 NodePort,浏览器仍然连不上;第三个人重建集群后,测试数据又全部消失。三个现象看似无关,实际上都来自同一层错觉:k3d 的 Kubernetes 节点不是宿主机本身,而是 Docker 容器。
k3d 负责创建容器化的 k3s server、agent、网络和负载均衡器。k3s 节点里的 containerd 看不到宿主机 Docker 的镜像库;容器端口也不会自动出现在宿主机;写入节点容器可写层的数据会随集群删除。因此,镜像要导入节点或经过 registry,端口要在创建集群时映射,持久数据要挂载到明确的位置。先把这三条边界看清,k3d 才会从“轻量演示工具”变成可靠的开发底座。
把工具装到可验证的状态
k3d 当前稳定版仍是 v5.8.3。v5 以 Docker API 为主要运行入口,官方仓库给出的最低 Docker 版本是 20.10.5,并要求对应的 runc 至少为 1.0.0-rc93。创建集群前应先确认 Docker daemon 可访问,而不是只确认桌面图标已经启动。下面三条命令分别验证客户端、服务端和 k3d;docker info 若出现连接 daemon 失败,继续安装 k3d 也不会让集群成功。
docker version
docker info --format '{{.ServerVersion}}'
k3d version官方仓库提供安装脚本、Homebrew、Chocolatey、Scoop 和 release 二进制。自动化环境应固定 k3d v5 的补丁版本并校验下载物,不要让 latest 在无人审查时改变集群创建行为。Windows 可以用 choco install k3d 或 scoop install k3d,macOS/Linux 可以用 brew install k3d;受管开发机更适合从 k3d GitHub Releases 下载经内部制品库复核的二进制。k3d 采用 MIT License,本地使用本身没有商业席位费用,真正的成本来自 Docker Desktop 授权、CI 运行时长、镜像流量和开发机资源。
还要检查 kubectl。Kubernetes 的版本偏差策略允许受支持的 kubectl 与 API Server 相差一个次版本,但插件和清单仍可能依赖特定 API;团队应记录实际的客户端与 server 版本,而不是只写“装最新版”。k3d 若不指定节点镜像,会选择该版本内置的默认 k3s 镜像;需要稳定复现时,用 --image 明确 Kubernetes/k3s 基线,并在升级 k3d、k3s 或 kubectl 任一项时重跑集成实验。下面固定 rancher/k3s:v1.35.6-k3s1;这个 Docker tag 对应 k3s release v1.35.6+k3s1,因为镜像 tag 用 - 代替 release 名中的 +。项目应把验证通过的 tag 连同升级记录一起维护。
kubectl version --client
k3d cluster list首次执行时,k3d cluster list 应返回表头或空列表。若报 Docker socket 权限错误,Linux 上先检查当前用户是否被允许访问 daemon;不要用长期 sudo k3d 掩盖权限模型,否则 kubeconfig 和挂载目录会混入 root 所有权。Docker 组近似 root 权限,团队开发机仍要把成员资格当作高权限授权管理。
先跑通一个可丢弃的单节点集群
下面创建名为 tooling-lab 的集群。--servers 1 是一个控制面节点,--agents 1 增加一个工作节点;--image 固定节点中的 k3s/Kubernetes 版本;--wait 会等待 server 就绪,--timeout 120s 防止网络、镜像拉取或 Docker 资源不足时无限卡住。--api-port 127.0.0.1:6550 把 Kubernetes API 只暴露给本机,避免开发机在局域网中意外开放管理入口。
k3d cluster create tooling-lab \
--servers 1 \
--agents 1 \
--image rancher/k3s:v1.35.6-k3s1 \
--api-port 127.0.0.1:6550 \
--wait \
--timeout 120s成功输出会说明集群已创建并切换 kubeconfig context。不要只相信这行提示,继续核对 context、节点角色和容器:
kubectl config current-context
kubectl --context k3d-tooling-lab get nodes -o wide
docker ps --filter label=app=k3d --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'预期 context 是 k3d-tooling-lab,节点列表包含一个 server-0 和一个 agent-0,状态最终为 Ready。Docker 列表还会出现 serverlb:它是 k3d 默认创建的 Nginx 负载均衡器,API 端口先到它,再代理到 server。server 节点运行 k3s server,agent 运行 k3s agent;二者都在容器里运行 containerd 和 kubelet 所需组件。
如果节点长期 NotReady,先看容器而不是反复重建:
docker logs k3d-tooling-lab-server-0 --tail 100
docker inspect k3d-tooling-lab-server-0 --format '{{.State.Status}} {{.State.Health.Status}}'no space left on device 指向 Docker 存储容量,failed to pull image 指向网络、代理或 registry,端口占用会在创建阶段报告 bind 失败。--timeout 超时后 k3d 默认回滚已创建资源;只有排查 k3d 本身的创建残留时才考虑 --no-rollback,日常脚本不应打开它。
反向实验:宿主机镜像不等于节点镜像
用一个公开小镜像产生本地别名,故意要求 Kubernetes 不访问 registry:
docker pull nginx:1.27-alpine
docker tag nginx:1.27-alpine checkout-api:dev-local
kubectl --context k3d-tooling-lab create namespace tooling-lab
kubectl --context k3d-tooling-lab -n tooling-lab run image-gap \
--image=checkout-api:dev-local \
--image-pull-policy=NeverNever 的目的,是把“节点本地是否有镜像”变成唯一变量。观察 Pod:
kubectl --context k3d-tooling-lab -n tooling-lab get pod image-gap
kubectl --context k3d-tooling-lab -n tooling-lab describe pod image-gap预期状态是 ErrImageNeverPull,Event 中出现节点不存在该镜像的证据。宿主机 docker image inspect checkout-api:dev-local 成功,与 Pod 失败可以同时成立,因为 kubelet 委托节点内 containerd 查找镜像。
开发时有两种修法。一次性镜像适合直接导入:
k3d image import checkout-api:dev-local -c tooling-lab
kubectl --context k3d-tooling-lab -n tooling-lab delete pod image-gap
kubectl --context k3d-tooling-lab -n tooling-lab run image-gap \
--image=checkout-api:dev-local \
--image-pull-policy=Never
kubectl --context k3d-tooling-lab -n tooling-lab wait \
--for=condition=Ready pod/image-gap --timeout=60s导入会把镜像送进集群节点的镜像存储。重新创建 Pod 后应进入 Running;若仍失败,用 docker exec k3d-tooling-lab-agent-0 crictl images 验证镜像是否到达实际调度节点。导入适合个人快速试验,但每次重建都要重复,且多节点镜像分发会增加时间和磁盘占用。
让本地 registry 成为项目接入点
持续构建多个服务时,本地 registry 更接近真实交付链。先删除单节点实验集群,再创建固定主机端口的 registry 和新集群。删除前显式打印目标,防止名字写错:
k3d cluster list
k3d cluster delete tooling-lab
k3d registry create tooling-registry.localhost --port 127.0.0.1:5001k3d 给 registry 容器添加 k3d- 前缀,所以集群引用名称是 k3d-tooling-registry.localhost:5001。这里的 5001 是 k3d 为宿主机访问和镜像引用保留的逻辑端口,registry 容器进程仍监听 5000;k3d 生成的 mirror 配置负责把节点拉取请求送到同一 registry。宿主机可以向 localhost:5001 push,Deployment 则必须使用带 k3d- 前缀的名称。直接把节点镜像写成 localhost:5001 会指向节点容器自身,是本地 registry 最常见的失败。
k3d cluster create tooling-lab \
--servers 1 \
--agents 2 \
--image rancher/k3s:v1.35.6-k3s1 \
--api-port 127.0.0.1:6550 \
--registry-use k3d-tooling-registry.localhost:5001 \
--port '127.0.0.1:8080:80@loadbalancer' \
--wait --timeout 120s--port 的三段分别是宿主机地址与端口、目标容器端口、节点过滤器。这里把本机 8080 映射到 serverlb 的 80,为 k3s 默认 Traefik Ingress 留出入口。它不是 Service 的 port-forward,而是集群生命周期内持续存在的 Docker 端口映射。创建后可以用 docker port k3d-tooling-lab-serverlb 看见实际绑定。
接着把镜像推入 registry。镜像内容仍用已拉取的 Nginx,避免示例依赖项目 Dockerfile:
docker tag nginx:1.27-alpine localhost:5001/tooling/checkout-api:dev-001
docker push localhost:5001/tooling/checkout-api:dev-001Deployment 中必须写集群可解析的 registry 名称,而不是宿主机 push 地址:
apiVersion: apps/v1
kind: Deployment
metadata:
name: checkout-api
namespace: tooling-lab
spec:
replicas: 2
selector:
matchLabels:
app: checkout-api
template:
metadata:
labels:
app: checkout-api
spec:
containers:
- name: app
image: k3d-tooling-registry.localhost:5001/tooling/checkout-api:dev-001
imagePullPolicy: Always
ports:
- name: http
containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: checkout-api
namespace: tooling-lab
spec:
selector:
app: checkout-api
ports:
- name: http
port: 80
targetPort: http把清单保存为项目的 k8s/dev/checkout-api.yaml 后执行 server-side dry-run,再真正应用。前一步验证 API、字段和权限,后一步才改变集群:
kubectl --context k3d-tooling-lab create namespace tooling-lab
kubectl --context k3d-tooling-lab apply --server-side --dry-run=server \
-f k8s/dev/checkout-api.yaml
kubectl --context k3d-tooling-lab apply -f k8s/dev/checkout-api.yaml
kubectl --context k3d-tooling-lab -n tooling-lab rollout status \
deployment/checkout-api --timeout=90s成功时 rollout 完成,两个 Pod 的 IMAGE 指向 registry 内名称。若出现 ImagePullBackOff,describe pod 的 Event 会区分 DNS 失败、连接拒绝、镜像不存在和认证失败。先用 docker ps --filter name=k3d-tooling-registry 确认 registry 在运行,再用 curl http://127.0.0.1:5001/v2/ 验证宿主机入口;不要通过关闭 TLS 校验来修复企业 registry 的证书问题。
本地无认证 HTTP registry 只适用于隔离开发机。独立执行 k3d registry create 默认把镜像层留在 registry 容器里,删除 registry 就会一并丢失;若要跨集群复用缓存,可用 --volume <受控宿主机目录>:/var/lib/registry 持久化,但必须同时设置容量上限、清理责任和磁盘告警。团队共享 registry 应使用 TLS、短期或机器人凭证、最小 push/pull 权限和可审计的镜像保留策略。k3d 的 registry 配置支持 auth 与 tls.ca_file,但配置中出现明文密码时会同时进入本地文件、节点挂载和可能的 CI 日志;更稳妥的做法是由 CI 密钥库生成临时文件,任务结束立即销毁,并限制文件 ACL。
用配置文件固定拓扑,而不是固定秘密
命令适合探索,团队项目更适合提交一份 k3d Simple 配置。当前 v5 配置 API 是 k3d.io/v1alpha5,资源 kind 是 Simple;项目代码中的结构体常被称为 SimpleConfig,但它不是 YAML 的 kind。官方仍把配置文件能力标为 Alpha,字段可能变化;升级 k3d 时应先用临时集群验证配置仍被接受。下面的 k3d.yaml 表达一台 server、两台 agent、固定 API、Ingress 端口、节点标签和宿主机目录挂载:
apiVersion: k3d.io/v1alpha5
kind: Simple
metadata:
name: tooling-lab
servers: 1
agents: 2
image: rancher/k3s:v1.35.6-k3s1
kubeAPI:
host: 127.0.0.1
hostIP: 127.0.0.1
hostPort: "6550"
ports:
- port: 127.0.0.1:8080:80
nodeFilters:
- loadbalancer
volumes:
- volume: ./var/dev-data:/var/lib/tooling-data
nodeFilters:
- agent:0
options:
k3s:
nodeLabels:
- label: workload=app
nodeFilters:
- agent:0
- agent:1
registries:
use:
- k3d-tooling-registry.localhost:5001servers 决定控制面数量,agents 决定工作节点数量;它们不是业务副本数。kubeAPI.host 决定 kubeconfig 中的 API 主机名,hostIP 才限制 Docker 在宿主机哪张网卡监听,hostPort 固定入口端口;只写 host 不能替代监听地址约束。ports[].nodeFilters 决定 Docker 端口落在哪类容器,省略过滤器可能产生意外映射。volumes[].volume 左侧是宿主机路径,右侧是节点容器路径,不是 Pod 自动可见的路径;Pod 若要使用它,还需 hostPath 或相应存储配置。nodeLabels 比把 --node-label 塞进 extraArgs 更直接;确需使用 extraArgs 时,应像基础设施代码一样审查,尤其不能覆盖 k3d 保留的 K3S_URL、K3S_KUBECONFIG_OUTPUT 等设置。
创建前先确保挂载目录存在且不含真实业务数据:
mkdir -p var/dev-data
k3d cluster create --config k3d.yaml
kubectl --context k3d-tooling-lab get nodes --show-labels
docker inspect k3d-tooling-lab-agent-0 \
--format '{{range .Mounts}}{{println .Source "->" .Destination}}{{end}}'预期 agent 带有 workload=app 标签,inspect 输出中出现宿主机 var/dev-data 到 /var/lib/tooling-data 的绑定。Windows 上相对路径由当前 shell 和 Docker Desktop 文件共享共同解释;出现 Mounts denied 或空目录时,应检查磁盘共享、路径大小写和执行命令的位置。
可以用一个 Pod 验证卷的双向可见性:
apiVersion: v1
kind: Pod
metadata:
name: volume-probe
namespace: tooling-lab
spec:
nodeSelector:
kubernetes.io/hostname: k3d-tooling-lab-agent-0
containers:
- name: writer
image: busybox:1.36
command: ["sh", "-c", "date -u > /data/probe.txt; sleep 3600"]
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
hostPath:
path: /var/lib/tooling-data
type: Directory应用后,宿主机 var/dev-data/probe.txt 应出现 UTC 时间。反向实验是把 nodeSelector 改到 agent-1:该节点没有挂载宿主机目录,hostPath.type: Directory 会让 Pod 因目录不存在而启动失败,Event 给出 hostPath type check failed。这证明卷挂载带有节点亲和性,不是集群级共享存储。它适合缓存或单节点开发数据,不适合验证多节点持久卷、故障转移和一致性。
多 server 不等于生产高可用
k3d 可以创建三个 server 来验证调度、污点、亲和性和控制面成员变化:
k3d cluster create quorum-lab --servers 3 --agents 2 \
--image rancher/k3s:v1.35.6-k3s1 --subnet auto
kubectl --context k3d-quorum-lab get nodes奇数 server 让嵌入式 etcd 具备清晰的多数派;k3d 官方也建议多 server 采用 1、3、5 的数量。第一个 server 以 --cluster-init 初始化,后续 server 加入它。--subnet auto 用于降低容器重启后 IP 改变导致成员无法重新加入的概率,但该能力仍标注为实验性,升级时必须复测。
三个 server 都共享同一个 Docker daemon、宿主机内核、磁盘、电源和网络故障域。因此,停掉一个 server 容器只能验证部分 Kubernetes 行为,不能证明生产控制面的跨机容灾。还要预算资源:多 server 会增加 etcd、API Server、controller 和镜像副本,开发机至少观察 Docker 内存、磁盘和 CPU 压力。官方多 server 指南给出的起步建议是至少 2 核与 4 GiB;复杂工作负载应以实际基线为准。
一个可控故障实验是停止非初始化 server:
docker stop k3d-quorum-lab-server-2
kubectl --context k3d-quorum-lab get nodes
kubectl --context k3d-quorum-lab get --raw='/readyz?verbose'
docker start k3d-quorum-lab-server-2其余多数派健康时 API 应继续响应,停止节点会变为 NotReady;恢复后应重新加入。若同时停止两个 server,多数派丢失,写请求应失败或超时。实验结束必须启动容器或删除整个集群,不能把半失效拓扑留给后续测试。
端口、Service 与 Ingress 的三种入口
kubectl port-forward 是临时、按会话建立的调试隧道;k3d --port 是 Docker 层的长期映射;Service NodePort 是 Kubernetes 数据面能力。三者解决不同问题。
开发者只想看单个 Pod 时,优先使用绑定回环地址的 port-forward:
kubectl --context k3d-tooling-lab -n tooling-lab port-forward \
--address 127.0.0.1 service/checkout-api 18080:80需要整个团队项目稳定经过 Ingress 时,在建集群时映射 8080:80@loadbalancer,再提交 Ingress 规则。需要验证 NodePort 行为时,要把对应 NodePort 显式映射到某个 server 或 agent;创建 Service 后才发现未映射,可以用 k3d node edit ... --port-add,但配置文件应随即补齐,避免重建后丢失。
不要为了省事映射完整 30000-32767 端口段。它扩大宿主机暴露面,增加端口冲突,也让“哪个服务为何可访问”难以审计。所有固定映射默认绑定 127.0.0.1;需要局域网共享时,应经过防火墙、认证和明确 owner,而不是把 API、registry 或调试端口绑定 0.0.0.0。
清理必须覆盖集群之外的状态
删除 namespace 只清 Kubernetes 对象。k3d cluster delete 会删除目标集群的节点、serverlb、集群专用网络、k3d 管理的镜像导入卷和对应 kubeconfig 条目,但不会替你删除独立创建的 registry、bind mount 指向的宿主机目录,也不会清掉宿主机 Docker 镜像。先列出对象,再按所有权清理:
kubectl --context k3d-tooling-lab -n tooling-lab delete \
-f k8s/dev/checkout-api.yaml --ignore-not-found
k3d cluster delete tooling-lab quorum-lab
k3d registry delete tooling-registry.localhost
docker ps -a --filter label=app=k3d
docker volume ls --filter label=app=k3d
docker network ls --filter label=app=k3d最后三条预期不再出现这两个实验的容器、卷和网络。宿主机 var/dev-data 是否删除要由项目数据所有权决定;先检查解析后的绝对路径和内容,绝不能把变量为空的递归删除命令写进清理脚本。registry 删除后,本地标记为 localhost:5001/... 的镜像仍可能存在,可用 docker image ls 确认后按标签删除。
k3d 写入默认 kubeconfig 的 context 会随 cluster delete 清理;若创建时关闭了默认 kubeconfig 更新,或之后手工 merge 到其他 KUBECONFIG 文件,残留条目仍需执行 kubectl config get-contexts 检查。不要直接覆盖整个 kubeconfig;使用 k3d kubeconfig merge --overwrite 尤其危险,它会忽略既有内容,团队脚本不应采用。
CI 中何时选择 k3d
k3d 适合需要真实 API Server、k3s 默认组件、本地 registry 和多节点语义的集成测试。与直接启动若干 Docker 容器相比,它能验证 Deployment、Service、Ingress、RBAC、CRD 和控制器;与共享远程集群相比,它隔离清晰、任务结束即可删除。代价是需要特权或 Docker daemon、拉取 k3s 节点镜像、占用更多内存与磁盘,并承担嵌套容器网络差异。
CI 应固定 k3d、k3s 镜像和 kubectl 版本,把 cluster 名称带上流水线唯一 ID,给创建与测试设置超时,并把诊断采集放进失败清理之前。一个稳妥的顺序是:创建 registry,创建 cluster,导入或 push 镜像,应用清单,等待 rollout,执行测试;失败时采集 kubectl get all、Events、Pod describe、logs 和 k3d 容器日志;最后无条件删除 cluster 与 registry。
缓存 registry 数据能节省外网流量,却会引入磁盘增长、陈旧 tag 和供应链污染。缓存命中率、磁盘占用和清理时长应持续可见;镜像使用内容摘要或流水线唯一 tag,不用可变 latest 作为通过证据。涉及企业镜像的 CI 日志不能打印 registry 密码、Docker config、kubeconfig 或完整 Secret。
当测试目标依赖云负载均衡器、真实 CSI、多可用区、托管身份、生产 CNI 或节点内核差异时,k3d 通过只说明清单和控制器在这个轻量环境中工作。架构门禁应继续在与生产同构的测试集群验证。反过来,如果只需要单进程单元测试或几个依赖容器,启动完整 Kubernetes 会提高反馈时延,此时 Docker Compose 或 Testcontainers 更合适。
长期维护的判断依据
团队不必把每个 k3d 开关都写进模板,但必须能回答:k3d 与 k3s 版本由谁升级,镜像从哪里进入节点,端口为何暴露,持久数据由谁清理,失败证据保存多久,CI runner 是否允许 Docker 高权限,以及哪类验证必须升级到真实集群。
配置文件和项目清单进入代码审查,registry 凭证与企业 CA 留在受控密钥渠道;每次版本升级至少重跑镜像导入、registry pull、Ingress、卷、节点重启和彻底删除实验。k3d 的 配置文件、本地 registry、多 server 与 默认行为 会随版本演进,模板字段或实验性 IPAM 变化时,以固定版本的发布说明决定升级,而不是让开发机各自漂移。
