学 Kubernetes 最头疼的不是概念,是没有一个能随便折腾的练习环境。买云上的托管 K8s,一个月几百块起步,还怕手抖忘了删集群被扣费。用 Minikube 倒是免费,但跑 CI/CD 流水线的时候总差点意思。
这篇文章给你一个零成本方案:Kind 本地起集群,GitHub Actions 跑流水线,从 git push 到 Pod 运行,全链路打通。不花一分钱,不依赖任何外部服务。
整体架构
三个组件,各管一摊:
| 组件 | 职责 | 成本 |
|---|
| Kind | Docker 里跑 K8s 集群,本地验证用 | 免费 |
| GitHub Actions | 云端 CI/CD,构建镜像、跑测试、部署验证 | 公共仓库 2000 分钟/月免费 |
| Docker | 容器运行时,构建镜像的基础 | 免费 |
流程一句话总结:本地写代码 → git push → GitHub Actions 自动触发 → 构建 Docker 镜像 → 在 Actions 里起一个 Kind 集群 → 部署并验证 → main 分支的镜像推到 GHCR。
环境准备
你需要的东西:一台 4GB 以上内存的电脑、Docker、kubectl、Kind、Git。
装 kubectl:
1
2
3
4
| # Linux
curl -LO "https://dl.k8s.io/release/v1.31.0/bin/linux/amd64/kubectl"
chmod +x kubectl && sudo mv kubectl /usr/local/bin/
kubectl version --client
|
装 Kind:
1
2
3
| curl -Lo kind https://kind.sigs.k8s.io/dl/v0.24.0/kind-linux-amd64
chmod +x kind && sudo mv kind /usr/local/bin/
kind version
|
本地起一个集群验证安装没问题:
1
2
3
| kind create cluster --name cicd-lab
kubectl cluster-info
kubectl get nodes
|
看到节点 Ready 就说明环境 OK。先删掉,后面用 Actions 里的:
1
| kind delete cluster --name cicd-lab
|
写应用和 K8s 清单
用一个极简 Python HTTP 服务做演示,不引任何框架,纯标准库:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| # src/app.py
from http.server import HTTPServer, BaseHTTPRequestHandler
import json
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps({
"status": "ok",
"message": "Hello from K8s CI/CD Lab!",
"version": "1.0.0"
}).encode())
if __name__ == '__main__':
server = HTTPServer(('0.0.0.0', 8080), Handler)
print("Server running on port 8080...")
server.serve_forever()
|
Dockerfile 能多小就多小:
1
2
3
4
5
| FROM python:3.12-slim
WORKDIR /app
COPY src/ .
EXPOSE 8080
CMD ["python", "app.py"]
|
K8s 的 Deployment 加上健康检查,这是生产环境的习惯:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| # k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: cicd-demo
labels:
app: cicd-demo
spec:
replicas: 2
selector:
matchLabels:
app: cicd-demo
template:
metadata:
labels:
app: cicd-demo
spec:
containers:
- name: app
image: ghcr.io/OWNER/cicd-demo:TAG
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 3
periodSeconds: 5
livenessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
resources:
requests:
memory: "64Mi"
cpu: "100m"
limits:
memory: "128Mi"
cpu: "250m"
|
Service 用 ClusterIP 就够了,Actions 里用 port-forward 验证:
1
2
3
4
5
6
7
8
9
10
11
12
| # k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
name: cicd-demo-svc
spec:
selector:
app: cicd-demo
ports:
- port: 80
targetPort: 8080
type: ClusterIP
|
注意 Deployment 里的 OWNER 和 TAG 是占位符,Actions 流水线会在运行时用 sed 替换。
GitHub Actions 流水线
项目结构长这样:
1
2
3
4
5
6
7
8
9
10
| k8s-cicd-lab/
├── src/
│ └── app.py
├── Dockerfile
├── k8s/
│ ├── deployment.yaml
│ └── service.yaml
└── .github/
└── workflows/
└── ci-cd.yml
|
流水线分三个阶段,拆开讲:
阶段一:构建和检查。 检出代码,做语法检查,构建镜像,验证镜像大小不超过 200MB。这个阶段跑得快,有问题直接拦住。
阶段二:K8s 部署测试。 用 helm/kind-action 在 Actions runner 里起一个 Kind 集群,把刚才构建的镜像 kind load 进去(注意不是 push 到远端,是直接灌进 Kind),替换 manifest 里的占位符,apply 部署,rollout status 等就绪,port-forward 加 curl 做健康检查。如果这步挂了,自动导出 Pod 日志方便排查。
阶段三:推送镜像。 只在 main 分支触发,用 GITHUB_TOKEN 登录 GHCR,推镜像。PR 不推,避免污染仓库。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
| name: K8s CI/CD Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
env:
IMAGE_NAME: ${{ github.repository_owner }}/cicd-demo
IMAGE_TAG: ${{ github.sha }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: 语法检查
run: python3 -m py_compile src/app.py
- name: 构建镜像
run: docker build -t ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} .
deploy-test:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: 启动 Kind
uses: helm/kind-action@v1.10.0
with:
cluster_name: k8s-cicd
- name: 构建并加载镜像
run: |
docker build -t ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} .
kind load docker-image ${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} \
--name k8s-cicd
- name: 替换占位符并部署
run: |
sed -i "s|OWNER|${{ github.repository_owner }}|g" k8s/deployment.yaml
sed -i "s|TAG|${{ env.IMAGE_TAG }}|g" k8s/deployment.yaml
kubectl apply -f k8s/
- name: 等待就绪
run: kubectl rollout status deployment/cicd-demo --timeout=120s
- name: 健康检查
run: |
kubectl port-forward svc/cicd-demo-svc 8080:80 &
sleep 3
curl -sf http://localhost:8080 | python3 -m json.tool
push-image:
needs: deploy-test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: 构建并推送
run: |
IMAGE=ghcr.io/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}
docker build -t $IMAGE .
docker push $IMAGE
|
关键点解释:kind load docker-image 是把本地构建好的镜像直接灌进 Kind 节点的 Docker 里,省去了 push/pull 的环节,CI 里用这招特别快。needs: build 保证阶段串行。permissions 那块必须显式声明 packages: write,不然 GITHUB_TOKEN 没权限推 GHCR。
跑通全流程
把上面的文件全部提交推到 GitHub:
1
2
3
| git add -A
git commit -m "init: K8s CI/CD lab"
git push origin main
|
去仓库的 Actions tab 看流水线跑。正常情况下三个阶段全绿,最后 GHCR 里多了一个镜像。
之后改代码、提交、推送,流水线自动跑。迭代流程就三步:改代码 → push → 看 Actions 结果。本地想快速验证的话:
1
2
3
| docker build -t test-app .
docker run -p 8080:8080 test-app
curl http://localhost:8080
|
不用每次都等 CI,本地先跑通了再推。
踩坑速查
| 问题 | 原因 | 解决 |
|---|
ImagePullBackOff | Kind 集群里没有这个镜像 | 用 kind load docker-image 灌进去 |
Actions 里 GITHUB_TOKEN 推镜像 403 | 权限不够 | 仓库 Settings → Actions → Workflow permissions 改成 Read and write |
| YAML 解析报错 | 缩进用了 Tab 或者对齐不对 | YAML 严格只认空格,用 yamllint 检查一下 |
| Kind 集群创建卡住 | 上次没删干净 | kind delete cluster --name xxx 然后重试 |
port-forward 超时 | Pod 还没 Ready | 先 kubectl rollout status 等就绪再转发 |
kubectl 连不上集群 | kubeconfig 的 context 不对 | kubectl config use-context kind-集群名 |
下一步
跑通这个流水线之后,可以往几个方向扩展:
- Helm 打包:把 K8s 清单改成 Helm Chart,支持 values 覆盖,多环境复用。
- ArgoCD:在 Kind 里装一个 ArgoCD,把部署从 push 模式改成 GitOps pull 模式。
- 多环境隔离:dev / staging / prod 用不同 namespace,同一套 manifest 不同 values。
- 监控和日志:Prometheus + Grafana 看指标,EFK 收日志,Kind 里跑这些也不吃太多资源。
这套方案最大的好处是零心理负担。集群随便建随便删,流水线跑挂了不用心疼钱。先把基础流程跑通,再往上加东西,比直接在生产环境里摸黑强太多了。