前面的 Kubernetes 基座、Harbor 私有仓库和 Headlamp 可视化入口已经完成之后,这次实验的重点就不再是把 Pod 拉起来,而是把代码仓库、应用同步和集群运行真正串成一条闭环。目标很明确:把 YAML 放进 Gitea,交给 Argo CD 监控和同步,让集群自己把 Deployment、Service 和 Pod 拉起来。

这套环境仍然沿用之前已经跑稳的 kubeadm 双节点集群。控制平面是 k8s-master-01,工作节点是 k8s-worker-01,镜像统一从 harbor.yxwa.info 获取。Gitea 和 Argo CD 都作为业务工作负载部署在 Kubernetes 里,后续要做的就是让它们分别扮演好Git 仓库和GitOps 控制器这两个角色。(如果其中所需要的镜像全部来自我自己搭建的镜像仓库,所以请根据自己所能拉到的镜像源自行更改)

Gitea 这一层没有直接使用临时容器,而是先把持久化存储补上。如果不先规划 PVC,Pod 一旦重建,仓库数据和用户信息就会一起消失。这里使用了最直接的一套静态本地卷方案,在 gitea 命名空间下给 Gitea 和 PostgreSQL 分别准备 PV/PVC,存储路径落在工作节点本地目录 /data/k8s/gitea 和 /data/k8s/postgres。YAML 如下:

cat > /root/gitea-storage.yaml <<'EOF'
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-gitea
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-manual
hostPath:
path: /data/k8s/gitea
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-postgres
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-manual
hostPath:
path: /data/k8s/postgres
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-gitea
namespace: gitea
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-manual
resources:
requests:
storage: 10Gi
volumeName: pv-gitea
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-postgres
namespace: gitea
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-manual
resources:
requests:
storage: 10Gi
volumeName: pv-postgres
EOF

w然后直接创建命名空间并应用:

export KUBECONFIG=/etc/kubernetes/admin.conf
kubectl create namespace gitea
kubectl apply -f /root/gitea-storage.yaml
kubectl get pv
kubectl get pvc -n gitea

这一步的是在 Kubernetes 里先把状态数据和 Pod 生命周期绑定。这样后面无论 Gitea 还是数据库做滚动更新、重建 Pod,数据都还能留在卷里。

数据库没有用 SQLite,而是单独起了一个 PostgreSQL。 GitOps 这条链一旦跑起来,Gitea 就不再只是一个轻量演示仓库,而会变成后续 YAML、仓库权限和项目管理的中心,单独放数据库会更稳,也更接近后面的真实使用方式。数据库的 Secret、Deployment 和 Service 可以直接按下面这套写入:

cat > /root/postgres-gitea.yaml <<'EOF'
apiVersion: v1
kind: Secret
metadata:
name: postgres-secret
namespace: gitea
type: Opaque
stringData:
POSTGRES_DB: gitea
POSTGRES_USER: gitea
POSTGRES_PASSWORD: Gitea@123456
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
namespace: gitea
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
nodeSelector:
kubernetes.io/hostname: k8s-worker-01
containers:
- name: postgres
image: harbor.yxwa.info/library/postgres:16
imagePullPolicy: IfNotPresent
ports:
- containerPort: 5432
envFrom:
- secretRef:
name: postgres-secret
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
volumes:
- name: postgres-data
persistentVolumeClaim:
claimName: pvc-postgres
---
apiVersion: v1
kind: Service
metadata:
name: postgres
namespace: gitea
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
EOF

应用完成后检查状态:

kubectl apply -f /root/postgres-gitea.yaml
kubectl get pods -n gitea -o wide
kubectl get svc -n gitea

数据库镜像最终没有直接用公网 postgres:16,而是和前面 Kubernetes 核心镜像、Flannel、Headlamp、Argo CD 一样,先同步进 Harbor,再改成 harbor.yxwa.info/library/postgres:16。这一步的经验和前面完全一致:内网环境里,真正决定成败的往往不是 YAML 本身,而是镜像链路能不能收口到内网仓库。

Gitea 本体部署完成后,第一次打开安装页面时,数据库参数实际上已经可以直接填固定值了:数据库类型是 PostgreSQL,数据库主机是 postgres:5432,用户和数据库名都是 gitea,密码对应前面 Secret 里的 Gitea@123456。这里真正需要注意的是 Gitea 的服务地址。

因为当前阶段还是通过node的端口暴露访问,没有挂到正式的 80/443 入口上,所以 ROOT_URL 不能提前写成域名,而要按当前真实访问方式填写成 http://git.yxwa.info:31000/。否则页面虽然能打开,但后续的跳转、Webhook、OAuth 以及仓库克隆链接都会不一致。

记得设置管理员账户

Gitea 的 SSH 这一步还额外踩中了一个比较典型的点:容器默认以非 root 方式运行,而 SSH 监听 22 端口在容器里属于特权端口,启动时会直接报 listen tcp :22: bind: permission denied。

实际处理方式也很干脆,把容器内部的 SSH 监听改成 2222,对外 NodePort 仍然保留 32022,这样既绕开了特权端口限制,也不影响外部用户的 SSH 克隆方式。数据库初始化成功以后,Gitea 日志里可以看到 ORM 初始化和 PostgreSQL 连接都已经跑通。

Argo CD 沿用的仍然是前面已经验证过的路径:官方 install.yaml 先在外部环境下载,再把所需镜像同步进 Harbor,最后在集群内应用安装。

安装完成后

Argo CD 的访问入口被改成了 NodePort,argocd-server 通过 30000/30080 对外暴露。初始管理员密码并不是明文展示,而是存放在 argocd-initial-admin-secret 里,需要先从 Secret 里取出来:

kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d && echo

如果默认密码和登录实际不一致,或者后面想主动重置密码,最稳的方式是直接使用 Argo CD 自带的 bcrypt 生成功能,再 patch 到 argocd-secret 里,最后重启 argocd-server:

HASH=$(kubectl exec -n argocd deploy/argocd-server -- argocd account bcrypt --password '你的密码' | tr -d '\r')
echo "$HASH"kubectl -n argocd patch secret argocd-secret \
-p "{\"stringData\":{
\"admin.password\":\"$HASH\",
\"admin.passwordMtime\":\"$(date +%FT%T%Z)\"
}}"kubectl rollout restart deployment/argocd-server -n argocd
kubectl get pods -n argocd -w

等 Gitea 和 Argo CD 都能稳定访问后,真正的 GitOps 演示才算开始。

先在 Gitea 里创建了一个最小仓库 admin/git-ops-yxwa,用来存放两份最基础的 Kubernetes 资源,一个 Deployment,一个 Service。仓库本身不需要复杂模板,也不需要许可证或 CI,先初始化一个 README,保证它有初始提交即可。接着在仓库根目录里直接创建 nginx-deploy.yaml 和 nginx-svc.yaml。Deployment 里用的镜像也不是公网 nginx:alpine,而是已经同步进 Harbor 的 harbor.yxwa.info/library/nginx:alpine:

apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-demo
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: nginx-demo
template:
metadata:
labels:
app: nginx-demo
spec:
containers:
- name: nginx
image: harbor.yxwa.info/library/nginx:alpine
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080

Service 则保持最基础的 NodePort 暴露方式:

apiVersion: v1
kind: Service
metadata:
name: nginx-demo
namespace: default
spec:
type: NodePort
selector:
app: nginx-demo
ports:
- port: 80
targetPort: 8080
nodePort: 30081

这样处理的目的很清楚:先用一套最小资源验证 Git 仓库、Argo CD 和 Kubernetes 之间的同步链路,确认“改 YAML -> 同步 -> 资源创建”这条路线是通的,而不是一开始就把问题复杂化。


Argo CD 里新建应用时,配置本身也不复杂。应用名设为 gitops-nginx,项目仍然用 default,同步策略先选手动。仓库地址直接指向 Gitea 里的 Git 仓库,Revision 使用 main,Path 因为 YAML 文件就在仓库根目录,所以填 .,目标集群仍然是默认的 https://kubernetes.default.svc,命名空间则落在 default。应用创建出来后,第一次通常会显示 OutOfSync,这不是故障,只说明 Argo CD 已经发现了仓库里的目标状态,但还没真正下发到集群。手工点击同步应用之后,这个应用就会被 Argo CD 正式接管。


同步完成之后,GitOps 这条链就真正闭合了。Argo CD 中 gitops-nginx 的状态会从不同步进入健康同步,而在集群内执行:

kubectl get deploy,po,svc | grep nginx-demo
kubectl describe deployment nginx-demo

可以直接看到 Deployment、Pod 和 Service 都已经被创建出来,并且 Pod 已经正常进入 Running 状态。Deployment 里显示的镜像地址也明确指向了 Harbor 中的 harbor.yxwa.info/library/nginx:alpine。


至此,我已经把镜像管理、代码仓库、应用同步和运行平台拼成了一套可用的链路。

此作者没有提供个人介绍。
最后更新于 2026-05-12