Skaffold:开发循环、文件同步与单次部署
同一个容器项目在开发机上需要反复更新,在流水线中则需要构建一次、保存结果并部署。Skaffold 用 skaffold.yaml 描述两种流程共同使用的镜像、清单和部署方式;执行哪条命令,决定它是持续运行还是完成一轮后退出。
从配置到运行中的应用
build.artifacts 声明要构建什么,manifests 指定从哪里取得清单,deploy 指定如何提交。构建结束后,清单中匹配 artifact 名称的 image 被替换为本次构建结果。
| 命令 | 主要行为 | 适合的任务 |
|---|---|---|
| dev | 首次完整执行,随后观察文件并更新;中断时默认清理部署对象 | 本地持续开发 |
| run | 构建、执行已配置测试、渲染、部署并检查状态,然后退出 | 单次验证或简单流水线 |
| build | 构建镜像,可输出构建结果文件 | 固定可复用的制品 |
| render | 输出最终清单,可使用预先构建的镜像 | 查看待部署资源 |
| deploy | 使用构建结果部署,也支持其他镜像输入方式 | 构建与部署分离 |
| delete | 按项目配置删除部署资源 | 收尾或异常退出后的清理 |
开发工作流先建立完整应用,后续再根据变更选择同步文件、重建镜像或重新部署。没有配置测试时,不会凭空生成业务测试。
Skaffold 可以调用 Docker、Jib、Buildpacks、Helm、Kustomize 等工具。选择已有项目正在使用的构建器与渲染器即可,不需要为了使用 Skaffold 把全部清单改成 Helm Chart。字段与集成方式见配置参考。
在 kind 中完成第一次开发循环
安装与执行身份
使用 Linux amd64、Bash、Docker、kubectl、jq 和已经准备好的 kind 测试集群,context 为 kind-tooling-dev。以能够访问 Docker daemon 的普通用户操作,不使用生产 kubeconfig。构建阶段由 Docker 执行;应用容器固定使用 10001:10001。
固定 Skaffold v2.16.1,下载摘要取自官方发行资产。在新目录安装,摘要失败就停止,不执行下载文件。
set -euo pipefail
umask 077
skaffold_lab_dir=$(mktemp -d /tmp/skaffold-lab.XXXXXX)
cd "$skaffold_lab_dir"
mkdir -p bin web
curl -q --fail --show-error --location --max-time 600 \
https://github.com/GoogleContainerTools/skaffold/releases/download/v2.16.1/skaffold-linux-amd64 \
-o bin/skaffold
printf '%s bin/skaffold\n' \
'1cbeea85aa14ba603dbc2bbdfa7bfde5644d7988beed0fdc0fd1c67298d4cf67' \
| sha256sum -c -
chmod 0755 bin/skaffold
./bin/skaffold version
docker version
kubectl --context kind-tooling-dev cluster-info
kubectl --context kind-tooling-dev create namespace skaffold-lab版本应为 v2.16.1。namespace 已存在时,确认是否属于旧实验,不直接接管。所有项目文件保存在当前目录;后续终端也回到这个目录执行。
五份完整项目文件
实验目录
├── bin/skaffold
├── .dockerignore
├── Dockerfile
├── skaffold.yaml
├── k8s.yaml
└── web/index.htmlweb/index.html 提供可识别的版本内容。
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><title>Skaffold lab</title></head>
<body><h1>skaffold-v1</h1></body></html>Dockerfile 将文件放入可写的 /www。BusyBox 同时提供 httpd 与同步时需要的 tar;换用极简镜像时,需要重新确认这些工具。
FROM busybox:1.37.0
RUN mkdir -p /www && chown 10001:10001 /www
COPY --chown=10001:10001 web/ /www/
USER 10001:10001
EXPOSE 8080
CMD ["httpd", "-f", "-p", "8080", "-h", "/www"].dockerignore 排除工具和后续生成文件。
.git
bin
artifacts.json
rendered.yaml
effective*.yaml
*.logk8s.yaml 使用一个副本和专属 Service。readinessProbe 检查应用 HTTP 路径。
apiVersion: apps/v1
kind: Deployment
metadata:
name: skaffold-web
namespace: skaffold-lab
spec:
replicas: 1
selector:
matchLabels: {app: skaffold-web}
template:
metadata:
labels: {app: skaffold-web}
spec:
securityContext:
runAsUser: 10001
runAsGroup: 10001
runAsNonRoot: true
containers:
- name: web
image: local/skaffold-web
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
readinessProbe:
httpGet: {path: /, port: 8080}
periodSeconds: 2
resources:
requests: {cpu: 10m, memory: 16Mi}
limits: {cpu: 100m, memory: 64Mi}
---
apiVersion: v1
kind: Service
metadata:
name: skaffold-web
namespace: skaffold-lab
spec:
selector: {app: skaffold-web}
ports:
- port: 8080
targetPort: 8080skaffold.yaml 连接上述内容。image 名称必须与清单一致;push:false 适用于这里的本地集群,不能直接拿去部署远程节点。
apiVersion: skaffold/v4beta13
kind: Config
metadata:
name: skaffold-web
build:
local:
push: false
useDockerCLI: true
useBuildkit: true
concurrency: 2
tagPolicy:
sha256: {}
artifacts:
- image: local/skaffold-web
context: .
docker:
dockerfile: Dockerfile
sync:
manual:
- src: 'web/**/*.html'
dest: /www
strip: 'web/'
manifests:
rawYaml:
- k8s.yaml
deploy:
kubectl: {}
statusCheckDeadlineSeconds: 180
portForward:
- resourceType: service
resourceName: skaffold-web
namespace: skaffold-lab
port: 8080
localPort: 18083
address: 127.0.0.1
profiles:
- name: serial-build
patches:
- op: add
path: /build/local/concurrency
value: 1kind 使用节点内 containerd,宿主 Docker 中构建成功的镜像还需要进入节点。Skaffold 能识别 kind context 并加载镜像;imagePullPolicy:Always 会尝试访问仓库,不适合这条没有推送的实验路线。识别规则见本地集群文档。
运行、访问与修改
先展开配置,再启动开发循环:
./bin/skaffold diagnose --profile-auto-activation=false --yaml-only > effective.yaml
./bin/skaffold dev --kube-context kind-tooling-dev \
--namespace skaffold-lab --port-forward=user --no-prune=true等待构建、镜像加载与部署稳定,看到 Watching for changes 后再修改文件;应用已经可访问时,文件观察可能还没有初始化完成。日志应列出 Service 的本机转发地址。localPort 指定期望端口;如果 18083 已被占用,Skaffold 可以选择其他空闲端口,必须使用日志里的实际地址。监听地址显式限制为回环;具体行为见端口转发文档。
在另一个终端检查。下面假定日志确认使用 18083:
curl -q --noproxy '*' --fail-with-body --max-time 10 \
http://127.0.0.1:18083/
kubectl --context kind-tooling-dev -n skaffold-lab get pods \
-l app=skaffold-web \
-o custom-columns=NAME:.metadata.name,UID:.metadata.uid,IMAGE:.spec.containers[0].image响应应包含 skaffold-v1。记录 Pod UID 和镜像引用,把 web/index.html 改成 skaffold-v2 并保存,再请求一次。匹配同步规则后,预期响应变成 v2,而 Pod UID 与镜像保持不变;如果同时发生重启或其他发布,需先排除这些变化。
接着在 Dockerfile 的 FROM 后增加 LABEL lab.rebuild="v2"。这次应触发完整构建和部署,新 Pod 使用新镜像,响应仍为 v2。由此确认完整镜像也包含最新页面。
将快速更新接到测试与发布
同步规则如何决定文件位置
manual 的 src 相对于 artifact context。web/index.html 匹配后,strip 去掉 web/,余下 index.html 放进 /www;没有 strip 就可能变成 /www/web/index.html,应用仍读取旧的 /www/index.html。
Skaffold 将匹配文件打成 tar,发送到目标容器并解包,所以容器需要 tar 和目标目录写权限。infer 根据 Dockerfile 的 COPY/ADD 推导目的地;auto 主要用于 Jib、Buildpacks 等支持自动同步的构建器。三者的选择条件见文件同步说明。
文件进入容器后,进程还必须读取它。静态 httpd 每次请求读取页面,编译型语言则可能需要编译与重启。只同步 Java 源文件不会改变 JVM 已加载的类。依赖、构建指令或不能安全原地更新的内容应走重建;不要把源代码同步当作生产镜像更新。
Profile 覆盖与最终配置
前面的 serial-build profile 将本地构建并发设为 1。用同一版本 CLI 展开基础配置和覆盖配置:
./bin/skaffold diagnose --profile-auto-activation=false --yaml-only > effective.yaml
./bin/skaffold diagnose --profile-auto-activation=false \
-p serial-build --yaml-only > effective-serial.yaml
diff -u effective.yaml effective-serial.yaml || test "$?" -eq 1差异应落在 build.local.concurrency,不应改变 namespace、image 或 Service。这里 diff 返回 1 表示存在差异,其他非零退出仍是错误。单服务示例不会明显提速;多 artifact 构建时,该参数用于控制资源争用。
profile 可通过命令显式选择,也可按 context、环境变量或命令自动激活。同一 activation 条目里的条件要同时满足,不同条目之间满足任意一个即可。配置会叠加或按 JSON Patch 修改,不能假定整个对象被替换。规则见Profiles 文档。
增加镜像级检查
在 Linux 项目的 skaffold.yaml 顶层增加以下 test。Skaffold 为自定义命令提供本次镜像的 IMAGE 环境变量;命令在本机执行,再由 docker run 检查镜像内容。
test:
- image: local/skaffold-web
custom:
- command: 'sh test-image.sh'
timeoutSeconds: 30将 test-image.sh 保存到项目根目录,由 sh 读取。环境变量展开和带空格参数的引用由脚本处理,不假定任意 command 字符串都经过 Shell。
#!/bin/sh
set -eu
: "${IMAGE:?Skaffold must supply IMAGE}"
docker run --rm --network none "$IMAGE" grep -q skaffold-v /www/index.html它检查镜像是否包含页面标记,不能替代部署后的 HTTP 请求。把 grep 的预期改成不存在的字符串,下一次完整测试应失败,部署阶段被阻止;改回后再执行。不要使用 --skip-tests 绕过失败。运行时机和依赖配置见自定义测试文档。
已有 Maven、Jib 或 Buildpacks 工程可复用现有测试命令,不必再建立一套测试框架。自定义命令、构建脚本和 Helm hooks 都以实际执行身份运行,导入其他项目配置前应检查这些入口。
构建一次,复用结果
先停止 dev 并完成下一节的应用清理,保留 namespace,再回到同一目录运行一次完整发布:
./bin/skaffold run --kube-context kind-tooling-dev \
--namespace skaffold-lab --port-forward=offrun 完成后退出,工作负载保留。可以用 kubectl 建立临时转发,再用 curl 核对实际响应。更细的流水线可拆为构建、测试、渲染和部署:
./bin/skaffold build --kube-context kind-tooling-dev \
--namespace skaffold-lab --file-output artifacts.json
./bin/skaffold test --profile-auto-activation=false --build-artifacts artifacts.json
./bin/skaffold render --offline --profile-auto-activation=false \
--build-artifacts artifacts.json \
--digest-source=tag --output rendered.yaml
kubectl --context kind-tooling-dev -n skaffold-lab \
apply --dry-run=server --validate=strict -f rendered.yaml
skaffold_image=$(jq -er \
'.builds[] | select(.imageName == "local/skaffold-web") | .tag' artifacts.json)
kind load docker-image "$skaffold_image" --name tooling-dev
./bin/skaffold deploy --kube-context kind-tooling-dev \
--namespace skaffold-lab --build-artifacts artifacts.jsonartifacts.json 保存 imageName 与实际 tag 的对应关系;rendered.yaml 应引用同一构建结果。render 使用 --offline,不连接 Kubernetes API,namespace 保留清单里显式设置的 skaffold-lab;--digest-source=tag 避免把本地未推送的镜像拿到远程仓库解析。参数见 CLI 参考,阶段拆分见持续交付说明。
单独 build 的镜像只在宿主 Docker 中。这里按 artifacts.json 找到本实验唯一镜像,显式导入 kind,再部署;不能假定分离执行的 deploy 会复现 dev 进程中的镜像导入状态。deploy 会依据当前配置重新渲染。若审批的是已经保存的 rendered.yaml,应部署该文件,不能一边改模板、一边认为旧审批仍然有效。远程 CI 还需推送到节点可访问的仓库,保存仓库摘要,并配置构建者推送身份与节点拉取身份;本地 Docker image ID 不等于仓库 manifest 摘要。
恢复旧应用需要旧镜像仍然存在,以及对应配置或渲染文件。重新部署旧资源不能撤销数据库迁移、消息发送等副作用,这些操作必须使用各自的恢复方案。
处理失败、调试与退出
用失败阶段缩小检查范围
| 现象 | 先检查 | 下一步 |
|---|---|---|
| 配置解析失败 | apiVersion、字段和缩进 | 使用固定 CLI diagnose,不连接错误目标 |
| Docker 构建失败 | Dockerfile 指令和依赖下载 | 修复后完整构建,不先修改 Pod |
| 镜像找不到 | artifact 名称、节点导入、拉取策略 | 本地确认 kind load,远程确认仓库与权限 |
| sync 成功但页面没变 | strip、目标文件、实际请求端口 | 确认文件位置与进程加载方式 |
| Service 转发端口变化 | Skaffold 转发日志、本机占用 | 改用实际端口,或释放自己占用的端口再启动 |
| Pod 未稳定 | Events、探针、日志和资源配置 | 找到具体失败原因后重试部署 |
在 Dockerfile 加入 RUN false 可以观察构建失败。恢复该行后保存,等待新构建成功。旧 Pod 可能仍在提供响应,不能仅以页面可访问判断修复;还要比较新镜像、Pod 就绪状态和响应内容。
文件监听在网络盘或虚拟化文件共享上不可靠时,可选择 --trigger=polling;它定期扫描文件,会增加扫描成本。调试日志使用 -v debug,分享前检查是否带出 registry 地址、凭证或敏感构建参数。
debug 会对支持的运行时调整容器启动方式并建立调试连接。它需要真正的语言进程,不能用这个静态页面演示 Java 断点。按官方调试指南对已有 Java 等项目选择运行时配置,在隔离 namespace 使用,结束后检查调试端口与工作负载状态。
退出和明确清理
dev 默认在 Ctrl-C 后删除部署资源;run 与 deploy 退出后保留资源。dev 被强制终止时,清理可能没有执行。对当前示例可在原目录显式运行:
./bin/skaffold delete --kube-context kind-tooling-dev \
--namespace skaffold-lab
kubectl --context kind-tooling-dev -n skaffold-lab \
wait --for=delete pod -l app=skaffold-web --timeout=120s
kubectl --context kind-tooling-dev -n skaffold-lab get deployment,pod,service等待完成后,这组应用对象应不存在。namespace 由前文手动建立,不在项目清单中,因此仍保留,可以继续单次发布实验。所有实验结束后再删除它:
kubectl --context kind-tooling-dev delete namespace skaffold-lab \
--wait=true --timeout=120s镜像、registry 数据和应用产生的外部数据各有生命周期。这里显式使用 --no-prune=true,保留本地镜像以便比较;不要用全局 docker system prune 代替项目清理。资源清理、镜像缓存和信号行为见官方清理说明。
升级时保留旧 CLI 与配置,用相同源码比较 diagnose、构建结果、最终清单和两条更新路径。先迁移测试项目,再统一开发机和 CI 版本。缓存命中只说明复用了已有结果,完整无缓存构建仍应用于检查未声明依赖,且不需要删除其他项目的缓存。
