GitLab 在 K8s 集群部署 —— 变更 SOP¶
- 交付物目录:
gitlab-deploy-v2/ - 主题:在自建 k8s 集群(v1.36.4)内部署一套自用 GitLab,并交付为长期运行的服务
- 版本:GitLab 19.3.2 / Helm chart 10.3.2
- 变更时间:2026-09-17 03:19 – 04:00(首装);2026-09-18 00:48 – 01:13(命名空间统一到
server,属独立主题,见附录 E) - 配套文档:RUNBOOK.md(日常运行与排障)、INCIDENT-20260917-01-gitlab-部署与迁移期排障.md(变更期故障台账)、backup/README.md(备份台账)
目录¶
- 1. 背景
- 2. 目标
- 3. 前置条件
- 4. 影响范围
- 5. 分步操作
- 6. 验证
- 7. 回滚
- 8. 风险说明
- 附录 A:组件与版本清单
- 附录 B:存储与容量规划
- 附录 C:资源 requests/limits
- 附录 D:交付物清单
- 附录 E:相关变更与文档
1. 背景¶
集群为自建 k8s(1 master + 3 worker,Calico,Traefik,nfs-csi),此前没有 GitLab。需求是部署一套小规模自用 GitLab,覆盖 Git 仓库、CI、容器镜像仓库三件事。
本次变更的规模不是"装一个 chart",而是四件交付物的组合——原因来自 chart 10.x 的三条硬约束(均已实测确认):
| 约束 | 实测证据 | 后果 |
|---|---|---|
| chart 10.x 已移除内置 PostgreSQL / Redis / MinIO | helm template 直接报错 The chart provides no longer bundled object storage solution;Chart.yaml 中无 postgresql / redis / minio 子 chart |
必须自备 PG + Redis + S3 兼容对象存储 |
| Gitaly 仅支持本地块存储 | GitLab 官方明确 NFS/CephFS 等文件系统不受支持(NFS 支持自 15.0 EOL) | Gitaly 与 PostgreSQL 必须落节点本地盘,不能放 NFS |
| GitLab 19.x 默认 Gateway API + Envoy Gateway | 渲染结果含 Gateway/EnvoyProxy/TCPRoute(v1),chart 打包的 gateway-helm 不含 CRD |
必须先安装 Gateway API ≥ v1.6 CRDs;集群无 LB,需把网关 Service 从默认 LoadBalancer 改为 ClusterIP |
变更分两段:09-17 首装(落在 gitlab / gitlab-pg / gitlab-redis / gitlab-minio 四个命名空间),09-18 统一命名空间到 server(走重建路径,当时无业务数据;细节见 附录 E)。本文档第 5 章的操作命令统一按最终形态(server 单命名空间)书写,与新环境重建时的执行路径一致。
实现体说明:按用户明确要求,本次实现不以 Python 工程承载(与"复杂变更一律 Python 工程实现"的常规约定不同,此处以用户指令为准)。因此第 5 章即为唯一操作依据;配置以本目录
values-gitlab.yaml为准。
2. 目标¶
变更完成后应达到的可验证状态:
server命名空间内 GitLab 全栈(webservice / sidekiq / gitaly / gitlab-shell / kas / registry / toolbox / exporter)全部 PodRunning且Ready,gitlab-migrations-*JobCompleted。- 依赖组件同命名空间就绪:CNPG
Cluster gitlab-pg状态 healthy;redis-0Running(AOF +noeviction);minio-0Running 且已有 6 个 bucket。 Gateway gitlab-gw为PROGRAMMED=True;HTTPRoute 按gitlab.rainchen.top/registry.rainchen.top/kas.rainchen.top分流,/users/sign_in返回 200,未知域名返回 404。- Gitaly / PostgreSQL / Redis 的 PVC 绑定到
local-ssd的 3 个 local PV,且 Pod 落在k8s-node-01;MinIO 的 PVC 绑定nfs-csi。 - 对象存储内存在
gitlab-artifacts、git-lfs、gitlab-uploads、gitlab-packages、gitlab-registry、gitlab-backups六个 bucket。 - 不部署 Prometheus、cert-manager、gitlab-runner(本场景不需要,避免多出 8Gi PVC 与额外组件)。
不在本次范围:TLS 证书与对外暴露链路(由入口工程承担,见 附录 E)、CI Runner 注册、备份自动化(见 RUNBOOK 第 6 章)。
3. 前置条件¶
权限
| 对象 | 要求 | 实测 |
|---|---|---|
| k8s | 集群管理员 kubeconfig | ~/.kube/config 可用(server: 192.168.31.8:16443) |
| 宿主机 dell-R620(192.168.31.249/250,KVM 宿主) | sudo 免密 |
可用 |
| 各节点 | root 免密 SSH |
可用(如 root@192.168.31.13) |
工具版本
| 工具 | 实测 |
|---|---|
| kubectl | v1.36.4 |
| helm | v3.16.3(chart 建议 4.0+,实测 3.16.3 渲染与安装均正常) |
| virsh / qemu-img | 宿主机已装(libvirt 运行中,含 5 台 VM) |
资源与空间(执行前核对)
| 资源 | 要求 | 实测值 |
|---|---|---|
宿主机 SSD sda3(/,Samsung 860 EVO 250G) |
可用 ≥ 70G(新建 60G qcow2) | 可用 161G(变更前) |
k8s-node-01 数据盘 |
60G 供 Gitaly/PG/Redis 的 local PV | /mnt/ssd 60G |
nfs-csi(loop 设备 118G) |
MinIO 数据卷 50G | 可用 112G |
| worker 资源 | 本次 requests 约 2.5 核 / 7Gi | 3 × (8C/16G),变更前 requests 极低 |
外部依赖可达性(节点直连,实测)
registry.gitlab.com(401 需认证=可达)、charts.gitlab.io(200)、docker.io(302)、quay.io(401)、registry.k8s.io(401),延迟均 < 2s。镜像拉取实测带宽约 5MB/s(gitaly 478MB 镜像耗时 95s),首次安装需预留时间。
执行前检查
kubectl get nodes # 预期 4 节点 Ready
kubectl get sc # 预期 nfs-csi (default),变更后新增 local-ssd
helm version --short # 预期 v3.16.x 及以上
virsh list --all # 预期 k8s-node-01 running;注意 master-02/03 为旧集群残留(shut off)
sudo df -h / # 预期 SSD 可用 ≥ 100G
4. 影响范围¶
涉及主机与组件
| 主机 | 角色 | 变更内容 | 是否停服 |
|---|---|---|---|
| dell-R620(KVM 宿主) | 虚拟化宿主 | 在 SSD 上新建 60G qcow2 并热挂给 k8s-node-01 | 否(热插拔;不影响宿主上其余 4 台运行中的 VM) |
| k8s-node-01(192.168.31.13) | worker | 新增数据盘 vdb,格式化 xfs 并挂载 /mnt/ssd(写入 /etc/fstab) |
否(不重启节点) |
| k8s 集群 | — | 新增命名空间内全套对象、Gateway API CRDs、local-ssd StorageClass、3 个 local PV、CNPG operator |
否 |
影响面说明
- 集群当时无业务负载,属新建型变更,无需停服窗口;建议预留连续 1 小时(实测首装耗时约 40 分钟,其中镜像拉取占大头)。
- 宿主机 SSD 同时承载宿主系统盘与 VM 系统盘,新数据盘与其共享 IO;小规模负载下影响可忽略。
- 不触碰既有 Traefik(其占用 master 的 hostPort 80/443)与既有工作负载。
- 集群级新增 CRD:
gateway.networking.k8s.io/*、postgresql.cnpg.io/*(后者由 CNPG operator 安装,共 11 个)。
5. 分步操作¶
以下为与实现等价的操作序列,供人工复核与手工复现。参数以
values-gitlab.yaml为准;标注【高危】的步骤会创建/格式化/删除数据,执行前务必核对目标设备与路径。
阶段 0:为 k8s-node-01 增加 SSD 本地盘¶
# --- 0.1 宿主机(dell-R620)创建 qcow2(放在 SSD 上,非 /data 的 HDD)---
sudo install -d -m 0755 /kvm/ssd # 幂等:目录已存在不报错
sudo qemu-img create -f qcow2 /kvm/ssd/gitlab-data.qcow2 60G # 60G 稀疏盘
sudo chown libvirt-qemu:kvm /kvm/ssd/gitlab-data.qcow2 # 对齐现有 VM 磁盘属主
# --- 0.2 热挂给 k8s-node-01(【高危】:写入 VM 持久化配置,可用 detach-disk 回退)---
sudo virsh attach-disk k8s-node-01 /kvm/ssd/gitlab-data.qcow2 vdb \
--targetbus virtio --driver qemu --subdriver qcow2 --cache none --io native --persistent
sudo virsh domblklist k8s-node-01 # 预期出现 vdb → /kvm/ssd/gitlab-data.qcow2
# --- 0.3 节点内格式化并挂载(【高危】:会清空 vdb,先 lsblk 核对设备名与容量)---
ssh root@192.168.31.13
lsblk # 确认 vdb 60G、无分区、无文件系统
mkfs.xfs -f /dev/vdb # 【高危】格式化
mkdir -p /mnt/ssd
UUID=$(blkid -s UUID -o value /dev/vdb)
grep -q "$UUID" /etc/fstab || echo "UUID=$UUID /mnt/ssd xfs defaults,noatime 0 2" >> /etc/fstab
mount -a && df -hT /mnt/ssd # 预期 60G 已挂载
# --- 0.4 预建 PV 目录与属主(local PV 不建目录、不改权限,必须手工)---
mkdir -p /mnt/ssd/k8s/{gitaly,postgres,redis}
chown 1000:1000 /mnt/ssd/k8s/gitaly # Gitaly 容器 uid 1000(git)
chown 26:26 /mnt/ssd/k8s/postgres # CNPG 容器 uid 26(postgres)
chown 999:999 /mnt/ssd/k8s/redis # redis 官方镜像 uid 999
exit
阶段 1:安装 Gateway API CRDs¶
# TCPRoute 自 Gateway API v1.6 起转正为 v1,chart 10.3.2 渲染的就是 v1,故必须 ≥ v1.6
kubectl apply --server-side -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.6.1/standard-install.yaml
kubectl get crd | grep gateway.networking.k8s.io # 预期含 gateways/httproutes/tcproutes/backendtlspolicies
阶段 2:创建 local-ssd StorageClass 与 3 个 local PV¶
kubectl apply -f - <<'YAML'
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-ssd
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer # local 卷必须等 Pod 调度后再绑定
reclaimPolicy: Retain # 保留:避免删 PVC 连带丢数据
---
apiVersion: v1
kind: PersistentVolume
metadata: {name: pv-gitaly-ssd}
spec:
capacity: {storage: 20Gi}
accessModes: [ReadWriteOnce]
persistentVolumeReclaimPolicy: Retain
storageClassName: local-ssd
local: {path: /mnt/ssd/k8s/gitaly}
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- {key: kubernetes.io/hostname, operator: In, values: ["k8s-node-01"]}
---
apiVersion: v1
kind: PersistentVolume
metadata: {name: pv-postgres-ssd}
spec:
capacity: {storage: 5Gi}
accessModes: [ReadWriteOnce]
persistentVolumeReclaimPolicy: Retain
storageClassName: local-ssd
local: {path: /mnt/ssd/k8s/postgres}
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- {key: kubernetes.io/hostname, operator: In, values: ["k8s-node-01"]}
---
apiVersion: v1
kind: PersistentVolume
metadata: {name: pv-redis-ssd}
spec:
capacity: {storage: 5Gi}
accessModes: [ReadWriteOnce]
persistentVolumeReclaimPolicy: Retain
storageClassName: local-ssd
local: {path: /mnt/ssd/k8s/redis}
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- {key: kubernetes.io/hostname, operator: In, values: ["k8s-node-01"]}
YAML
kubectl get pv # 预期 3 个 PV 状态 Available
阶段 3:准备命名空间与随机凭据¶
# 命名空间(本环境最终统一为 server;首装时曾用 gitlab/gitlab-pg/gitlab-redis/gitlab-minio 四个)
kubectl create namespace server --dry-run=client -o yaml | kubectl apply -f -
# 口令一律随机生成,不写入文档、不硬编码;生成后请自行保存
export PG_PASS=$(openssl rand -base64 24 | tr -d '/+=' | cut -c1-24)
export REDIS_PASS=$(openssl rand -base64 24 | tr -d '/+=' | cut -c1-24)
export MINIO_USER="gitlab-minio"
export MINIO_PASS=$(openssl rand -base64 24 | tr -d '/+=' | cut -c1-24)
kubectl -n server create secret generic gitlab-pg-app \
--from-literal=username=gitlab --from-literal=password="$PG_PASS"
kubectl -n server create secret generic gitlab-redis \
--from-literal=password="$REDIS_PASS"
kubectl -n server create secret generic minio-root-credentials \
--from-literal=root-user="$MINIO_USER" --from-literal=root-password="$MINIO_PASS"
【实测踩坑】chart 只在 release 所在命名空间查找数据库/Redis 凭据。首装时把
gitlab-pg-app/gitlab-redis建在各自命名空间(PG/Redis 所在 ns)而 release 在gitlab,导致全部 PodFailedMount。上文三条均建在server(与 release 同 ns),故不会踩到;若将来把依赖与 release 分属不同命名空间,必须在 release 的 ns 再建一份同名 Secret。
阶段 4:部署 MinIO(集群内单实例,S3 兼容)¶
# 采用固定镜像 tag 的单副本 StatefulSet(MinIO 官方社区 helm chart 已停止维护,不依赖它)
kubectl apply -f - <<'YAML'
apiVersion: v1
kind: PersistentVolumeClaim
metadata: {name: minio-data, namespace: server}
spec:
accessModes: [ReadWriteOnce]
storageClassName: nfs-csi # 对象数据放 NFS(只有 Gitaly/PG 必须本地盘)
resources: {requests: {storage: 50Gi}}
---
apiVersion: v1
kind: Service
metadata: {name: minio, namespace: server}
spec:
selector: {app: minio}
ports:
- {name: s3, port: 9000, targetPort: 9000}
- {name: console, port: 9001, targetPort: 9001}
---
apiVersion: apps/v1
kind: StatefulSet
metadata: {name: minio, namespace: server}
spec:
serviceName: minio
replicas: 1
selector: {matchLabels: {app: minio}}
template:
metadata: {labels: {app: minio}}
spec:
containers:
- name: minio
image: quay.io/minio/minio:RELEASE.2025-04-22T22-12-26Z # 固定 tag(已校验存在)
args: ["server", "/data", "--console-address", ":9001"]
env:
- {name: MINIO_ROOT_USER, valueFrom: {secretKeyRef: {name: minio-root-credentials, key: root-user}}}
- {name: MINIO_ROOT_PASSWORD, valueFrom: {secretKeyRef: {name: minio-root-credentials, key: root-password}}}
ports: [{containerPort: 9000}, {containerPort: 9001}]
volumeMounts: [{name: data, mountPath: /data}]
readinessProbe: {httpGet: {path: /minio/health/ready, port: 9000}, initialDelaySeconds: 10}
resources: {requests: {cpu: 200m, memory: 512Mi}, limits: {cpu: "1", memory: 1Gi}}
volumes:
- name: data
persistentVolumeClaim: {claimName: minio-data}
YAML
kubectl -n server rollout status statefulset/minio --timeout=240s
4.1 建业务 bucket(GitLab 需要 5 个)
kubectl apply -f - <<'YAML'
apiVersion: v1
kind: Pod
metadata: {name: minio-init-buckets, namespace: server}
spec:
restartPolicy: Never
containers:
- name: mc
image: quay.io/minio/mc:RELEASE.2025-04-16T18-13-26Z
env:
- {name: MINIO_ROOT_USER, valueFrom: {secretKeyRef: {name: minio-root-credentials, key: root-user}}}
- {name: MINIO_ROOT_PASSWORD, valueFrom: {secretKeyRef: {name: minio-root-credentials, key: root-password}}}
command:
- sh
- -c
- |
set -e
mc alias set m http://minio.server.svc.cluster.local:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD"
for b in gitlab-artifacts git-lfs gitlab-uploads gitlab-packages gitlab-registry gitlab-backups; do
mc mb --ignore-existing "m/$b"
done
mc ls m
YAML
kubectl -n server wait --for=jsonpath='{.status.phase}'=Succeeded pod/minio-init-buckets --timeout=180s
kubectl -n server logs minio-init-buckets # 预期列出 6 个 bucket
kubectl -n server delete pod minio-init-buckets
建 bucket 的一次性 Pod 不要用
kubectl wait --for=condition=Succeeded:Pod 没有Succeededcondition,会误报超时(行为正确但判据错)。用--for=jsonpath='{.status.phase}'=Succeeded。
4.2 生成三个存储 Secret
MINIO_USER=$(kubectl -n server get secret minio-root-credentials -o jsonpath='{.data.root-user}' | base64 -d)
MINIO_PASS=$(kubectl -n server get secret minio-root-credentials -o jsonpath='{.data.root-password}' | base64 -d)
# ① GitLab Rails 侧(consolidated 形式,key=connection)
cat > /tmp/object-storage.yaml <<EOF
provider: AWS
aws_access_key_id: ${MINIO_USER}
aws_secret_access_key: ${MINIO_PASS}
region: us-east-1
endpoint: http://minio.server.svc.cluster.local:9000
path_style: true
EOF
kubectl -n server create secret generic gitlab-object-storage --from-file=connection=/tmp/object-storage.yaml
rm -f /tmp/object-storage.yaml
# ② Registry 侧(key=config)
cat > /tmp/registry-storage.yaml <<EOF
s3:
bucket: gitlab-registry
accesskey: ${MINIO_USER}
secretkey: ${MINIO_PASS}
regionendpoint: http://minio.server.svc.cluster.local:9000
region: us-east-1
v4auth: true
pathstyle: true
rootdirectory: /
EOF
kubectl -n server create secret generic registry-storage --from-file=config=/tmp/registry-storage.yaml
rm -f /tmp/registry-storage.yaml
# ③ 备份用 s3cmd 配置(key=config)——缺失会导致 toolbox CrashLoop,见下方踩坑说明
cat > /tmp/s3cfg <<EOF
[default]
access_key = ${MINIO_USER}
secret_key = ${MINIO_PASS}
host_base = minio.server.svc.cluster.local:9000
host_bucket = minio.server.svc.cluster.local:9000
bucket_location = us-east-1
use_https = False
signature_v2 = False
EOF
kubectl -n server create secret generic gitlab-backup-s3cfg --from-file=config=/tmp/s3cfg
rm -f /tmp/s3cfg
【实测踩坑】chart 的
backups.objectStorage.backend默认是s3,toolbox 容器启动命令会执行cp -v -r -L /etc/gitlab/.s3cfg $HOME/.s3cfg;该文件由上述 ③ 的 Secret 提供,缺失时容器以 exit 1 退出并 CrashLoop。values-gitlab.yaml中已通过gitlab.toolbox.backups.objectStorage.config.secret: gitlab-backup-s3cfg指定。
阶段 5:部署 PostgreSQL(CloudNativePG 单实例)¶
helm repo add cnpg https://cloudnative-pg.github.io/charts && helm repo update
helm -n cnpg-system upgrade --install cnpg cnpg/cloudnative-pg --create-namespace
kubectl apply -f - <<'YAML'
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata: {name: gitlab-pg, namespace: server}
spec:
instances: 1
imageName: ghcr.io/cloudnative-pg/postgresql:17 # GitLab 19.x 仅支持 PG 17.x
storage:
size: 5Gi
storageClass: local-ssd
resources: {requests: {cpu: 200m, memory: 512Mi}, limits: {cpu: "1", memory: 1Gi}}
postgresql:
parameters:
max_locks_per_transaction: "256" # 见下方踩坑说明,必须设置
bootstrap:
initdb:
database: gitlabhq_production
owner: gitlab
secret: {name: gitlab-pg-app}
postInitApplicationSQL: # GitLab 必需扩展(需超级用户创建)
- CREATE EXTENSION IF NOT EXISTS pg_trgm;
- CREATE EXTENSION IF NOT EXISTS btree_gist;
YAML
kubectl -n server wait --for=condition=Ready cluster/gitlab-pg --timeout=420s
kubectl -n server get svc | grep gitlab-pg # 预期出现 gitlab-pg-rw / -ro / -r
【实测踩坑】GitLab 的
structure.sql在单个事务内创建上千个对象,PG 默认max_locks_per_transaction=64会报ERROR: out of shared memory (HINT: increase "max_locks_per_transaction"),migrations Job 反复失败。设为256后一次通过(改该参数会触发 PG 滚动重启)。 另:ghcr.io/cloudnative-pg/postgresql上没有17.6-*这类 tag(实测 404,17.5-19存在),因此使用主版本 tag:17,跟随最新 17.x 补丁。
阶段 6:部署 Redis(单实例 + AOF)¶
kubectl apply -f - <<'YAML'
apiVersion: v1
kind: Service
metadata: {name: redis, namespace: server}
spec:
selector: {app: redis}
ports: [{name: redis, port: 6379, targetPort: 6379}]
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata: {name: redis-data, namespace: server}
spec:
accessModes: [ReadWriteOnce]
storageClassName: local-ssd
resources: {requests: {storage: 5Gi}}
---
apiVersion: apps/v1
kind: StatefulSet
metadata: {name: redis, namespace: server}
spec:
serviceName: redis
replicas: 1
selector: {matchLabels: {app: redis}}
template:
metadata: {labels: {app: redis}}
spec:
containers:
- name: redis
image: redis:7.4-alpine
command:
- sh
- -c
- exec redis-server --appendonly yes --maxmemory 400mb --maxmemory-policy noeviction --requirepass "$REDIS_PASSWORD"
env:
- {name: REDIS_PASSWORD, valueFrom: {secretKeyRef: {name: gitlab-redis, key: password}}}
ports: [{containerPort: 6379}]
volumeMounts: [{name: data, mountPath: /data}]
readinessProbe:
exec: {command: ["sh","-c","redis-cli -a \"$REDIS_PASSWORD\" ping | grep -q PONG"]}
initialDelaySeconds: 10
resources: {requests: {cpu: 100m, memory: 256Mi}, limits: {cpu: "1", memory: 768Mi}}
volumes:
- name: data
persistentVolumeClaim: {claimName: redis-data}
YAML
kubectl -n server rollout status statefulset/redis --timeout=240s
kubectl -n server exec redis-0 -- sh -c 'redis-cli -a "$REDIS_PASSWORD" ping' # 预期 PONG
GitLab 要求 Redis 淘汰策略为
noeviction(allkeys-lru会导致 CI 异常)。
阶段 7:部署 GitLab chart¶
cd /home/rain/Documents/k8s_ins/gitlab-deploy-v2
# 部署前本地渲染复核(不改集群):确认无 Prometheus/cert-manager 资源、网关为 ClusterIP
helm template gitlab ./gitlab -n server -f values-gitlab.yaml > /tmp/render-check.yaml
grep -cE '^kind: (Prometheus|Alertmanager|ServiceMonitor|PodMonitor|Certificate|ClusterIssuer)' /tmp/render-check.yaml # 预期 0
helm upgrade --install gitlab ./gitlab -n server -f values-gitlab.yaml --timeout 900s
kubectl -n server get pod -w # 等到全部 Running/Completed;首装实测约 15 分钟(镜像拉取为主)
values-gitlab.yaml 固化的关键决策:hosts.domain=rainchen.top、hosts.https=false(GitLab 自身 HTTP,TLS 由入口链路终止)、Gateway 协议 HTTP、Envoy Service ClusterIP、escapeSlashesAction: KeepUnchanged(否则 API 的 %2F 路径失效)、关闭 cert-manager / Prometheus / gitlab-runner / nginx-ingress、Gitaly 用 local-ssd 20Gi、toolbox 指定 gitlab-backup-s3cfg。
6. 验证¶
# 1) 工作负载就绪
kubectl -n server get deploy,sts,job
# 预期:webservice / sidekiq / gitaly / gitlab-shell / kas / registry / toolbox / exporter 均 Ready;migrations Job Completed
# 2) 数据库迁移是否成功(唯一改数据库结构的一步)
kubectl -n server get job | grep migrations # 预期 COMPLETIONS 1/1
kubectl -n server logs job/$(kubectl -n server get job -o name | grep migrations | head -1 | cut -d/ -f2) | tail -20
# 3) 依赖与存储落点
kubectl -n server get cluster gitlab-pg # 预期 Cluster in healthy state,READY 1
kubectl -n server get pvc # 预期 gitlab-pg-1/redis-data/repo-data-gitlab-gitaly-0 绑 local-ssd;minio-data 绑 nfs-csi
kubectl get pv | grep ssd # 预期 3 个 local PV 全部 Bound
# 4) 网关与路由
kubectl -n server get gateway gitlab-gw # 预期 PROGRAMMED=True
kubectl -n server get svc | grep envoy # 数据面 Service:envoy-server-gitlab-gw-<hash>,暴露 80/22
# 【注意】不要 port-forward 到控制平面 Service envoy-gateway(18000/19001 管理端口,连不上)
SVC=$(kubectl -n server get svc -o name | grep -i envoy | grep -v 'envoy-gateway$' | head -1 | cut -d/ -f2)
kubectl -n server port-forward svc/$SVC 8080:80
# 另开终端:
curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: gitlab.rainchen.top' http://127.0.0.1:8080/users/sign_in # 200
curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: registry.rainchen.top' http://127.0.0.1:8080/v2/ # 401(需认证,正常)
curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: unknown.rainchen.top' http://127.0.0.1:8080/ # 404(域名分流生效)
# 5) 对象存储
kubectl -n server exec deploy/gitlab-toolbox -- ls -l /etc/gitlab/.s3cfg # 预期存在(toolbox 能启动即已通过)
# 6) 业务抽检(人工)
# 登录 → 建项目 → push 一次 → 跑一条最小 CI(验证 Gitaly + Sidekiq + artifacts)→ registry 推一个镜像
6.1 本次实测结果(2026-09-17 首装 / 2026-09-18 迁移后复核)¶
| 检查项 | 实测 |
|---|---|
| migrations | gitlab-migrations-* Completed |
| 工作负载 | webservice、gitaly、gitlab-shell、kas、registry×2、toolbox、exporter、envoy 数据面全部 Ready |
| Gateway | gitlab-gw PROGRAMMED=True;HTTPRoute 按 gitlab/registry/kas.rainchen.top 分流 + TCPRoute(SSH) |
| 页面与路由 | /users/sign_in 200、/ 302 → 登录页、未知域名 404、registry /v2/ 401 且 WWW-Authenticate 正确 |
| 依赖 | CNPG gitlab-pg healthy(PG 17.11,扩展 pg_trgm/btree_gist 已建);redis-0(AOF + noeviction);minio-0(6 bucket) |
| 存储 | 3 个 local PV 全部 Bound,Pod 均在 k8s-node-01;/mnt/ssd 用量 2.3G/60G(gitaly 79M / postgres 809M / redis 129M) |
| 幂等 | 相同参数二次 helm upgrade → 全部 Pod AGE 未变、无重建、零变更 |
6.2 变更期发现的问题¶
四个问题的根因、证据与处置见 INCIDENT-20260917-01-gitlab-部署与迁移期排障.md,此处不复述。
7. 回滚¶
备份与留档
- 本次变更的备份台账:
gitlab-deploy-v2/backup/README.md(逐条登记备份文件、原路径、产生时间、用途、可删时机)。 - 变更前状态快照(命名空间迁移阶段产生):
ns_migration/backup/(含 values、Secret、CNPG Cluster、PV/PVC 快照)。
回滚触发判据(任一满足即回滚,不临场判断)
gitlab-migrations-*Job 失败或 30 分钟内未 Completed;- webservice / gitaly 超过 15 分钟未 Ready;
- CNPG Cluster 非 healthy,或日志出现数据库连接拒绝;
- 验证阶段登录页无法返回 200。
回滚步骤(按逆序)
# 1) 卸载 GitLab(PVC 不随 uninstall 删除,数据仍在)
helm -n server uninstall gitlab
# 2) 卸载依赖组件
helm -n cnpg-system uninstall cnpg
kubectl -n server delete statefulset redis --cascade=orphan
kubectl -n server delete statefulset minio --cascade=orphan
kubectl -n server delete pvc redis-data minio-data # 【高危】删除即丢数据(MinIO 的 PV 是 Delete 策略)
# 3) 【高危】清理 local PV 与节点数据(仅在确认不再使用时执行;永久删除)
kubectl -n server delete pvc gitlab-pg-1 repo-data-gitlab-gitaly-0
kubectl delete pv pv-gitaly-ssd pv-postgres-ssd pv-redis-ssd
kubectl delete sc local-ssd
ssh root@192.168.31.13 'umount /mnt/ssd && sed -i "/\/mnt\/ssd/d" /etc/fstab'
# 4) 宿主机摘除数据盘(【高危】:先确认数据已备份)
sudo virsh detach-disk k8s-node-01 vdb --persistent
sudo rm -f /kvm/ssd/gitlab-data.qcow2
回滚后验证:kubectl -n server get all | grep gitlab 无残留;kubectl get pv | grep ssd 无遗留;节点 lsblk 无 vdb;宿主机 df -h / 恢复原可用空间。
注意:回滚会丢失 09-17 之后写入 GitLab 的任何数据(仓库、CI 记录、镜像)。执行前先用 RUNBOOK 第 6 章的方式做一次应用级备份。
8. 风险说明¶
| 风险 | 等级 | 说明 | 缓解措施 |
|---|---|---|---|
| MinIO 上游已停止发布社区版预编译二进制,官方社区 helm chart 归档 | 中 | 当前固定镜像仍可用,但无安全更新 | 已固定 tag;评估 Garage / SeaweedFS / Ceph RGW 作为替代后再迁移(对象数据可 mc mirror) |
MinIO 数据卷是 nfs-csi 动态卷(Delete 策略) |
高 | 误删 PVC 即丢对象数据(artifacts/LFS/uploads/packages/registry) | 回滚脚本中对删除 PVC 标注【高危】;建议改为静态 PV 或定期备份 bucket |
| 对象存储/共享数据放在 loop 设备(118G,可用 112G) | 中 | 容量上限低、IO 性能弱于真实磁盘 | 当前用量仅 212M;增长后迁移到 /data(1.7T)新导出目录 |
| Gitaly 单副本 | 中 | 节点维护/重启期间 Git 操作中断(Gitaly 不支持多副本无 HA) | 属已知取舍;维护安排在无 CI 任务时段 |
自签证书(chart 生成了 gitlab-wildcard-tls) |
低 | 仅内部链路使用;对外 TLS 由入口链路终止 | 保持 hosts.https=false;如需内网 HTTPS,用自建 CA 或接入现有证书 |
| chart 默认会装 Prometheus 与 gitlab-runner | 中 | 漏关会多出 8Gi PVC 与额外组件 | values-gitlab.yaml 已显式关闭;第 5 章渲染复核项已含该检查 |
| 凭据跨命名空间引用 | 中 | chart 只在 release ns 找 DB/Redis Secret;分 ns 部署时易漏 | 第 5 章阶段 3 的踩坑说明;重建时保持依赖与 release 同 ns |
PG max_locks_per_transaction 不足 |
中 | 默认值会让 migrations 反复失败,且报错信息指向不明显 | 已在 Cluster 中固化 256;见阶段 5 说明 |
| 未接入监控告警 | 中 | 集群虽已有 kube-prometheus-stack,但 GitLab 未暴露 ServiceMonitor/PodMonitor,缺少告警 | 见 RUNBOOK 第 7 章"建议接入"一节的清单 |
| 幂等性 | 低 | 本变更无自动化实现体,重复执行需人工判断 | 各步骤均可重复执行:create 失败即已存在、helm upgrade --install 幂等、apply 幂等;二次执行零变更已实测 |
附录 A:组件与版本清单¶
| 组件 | 版本 | 命名空间 | 备注 |
|---|---|---|---|
| GitLab chart | 10.3.2(GitLab 19.3.2) | server | 本目录 gitlab/ 为离线解包源码,安装无需联网拉 chart |
| Gateway API CRDs | v1.6.1(standard 通道) | — | TCPRoute v1 自 v1.6 起;chart 渲染的即 v1 |
| Envoy Gateway | gateway-helm 1.9.0(chart 内置) | server | 数据面 Service:envoy-server-gitlab-gw-<hash>(80/22) |
| CloudNativePG | operator 1.30.0(chart 0.29.0) | cnpg-system | 11 个 CRD |
| PostgreSQL | 17.11(镜像 ghcr.io/cloudnative-pg/postgresql:17) |
server | 扩展 pg_trgm、btree_gist;max_locks_per_transaction=256 |
| Redis | 7.4-alpine | server | AOF + noeviction + maxmemory 400mb |
| MinIO | RELEASE.2025-04-22T22-12-26Z | server | 单副本 StatefulSet;6 个 bucket |
| 未安装 | Prometheus、cert-manager、gitlab-runner、nginx-ingress、zoekt、OpenBao、AI Gateway | — | 均为本场景不需要的默认项,已在 values 中显式关闭 |
附录 B:存储与容量规划¶
| 用途 | 容量 | 介质 | StorageClass | 落点 | 实测用量 |
|---|---|---|---|---|---|
| Gitaly(Git 仓库) | 20Gi | 节点 SSD | local-ssd | k8s-node-01 /mnt/ssd/k8s/gitaly |
79M |
| PostgreSQL | 5Gi | 节点 SSD | local-ssd | k8s-node-01 /mnt/ssd/k8s/postgres |
809M |
| Redis | 5Gi | 节点 SSD | local-ssd | k8s-node-01 /mnt/ssd/k8s/redis |
129M |
| MinIO(artifacts/LFS/uploads/packages/registry) | 50Gi | NFS(loop,118G) | nfs-csi | 192.168.31.249:/srv/nfs/k8s |
704K |
| 宿主机新增数据盘 | 60Gi | SSD 上的 qcow2 | — | /kvm/ssd/gitlab-data.qcow2 |
2.1G |
说明:Gitaly 与 PostgreSQL 必须本地盘(官方要求);对象数据放 NFS(可迁移、对延迟不敏感)。容量按小规模自用设定:nfs-csi 支持在线扩容,本地盘需换更大 qcow2 或新增 PV。
附录 C:资源 requests/limits¶
| 组件 | requests | limits |
|---|---|---|
| webservice | 500m / 2Gi | 2 / 3Gi |
| sidekiq | 500m / 1.5Gi | 2 / 3Gi |
| gitaly | 300m / 1Gi | 2 / 2Gi |
| toolbox | 100m / 300Mi | 1 / 1Gi |
| gitlab-shell / kas / registry / exporter | 各 100m 量级(chart 默认) | 各 1 核量级 |
| envoy-gateway(含数据面) | 约 200m / 500Mi | 1 / 1Gi |
| PostgreSQL(CNPG) | 200m / 512Mi | 1 / 1Gi |
| Redis | 100m / 256Mi | 1 / 768Mi |
| MinIO | 200m / 512Mi | 1 / 1Gi |
合计 requests 约 2.5 核 / 7Gi,3 个 worker(24C/48G)余量充足。webservice / sidekiq / shell / kas 配置了 minReplicas/maxReplicas,由 HPA 管理副本,依赖集群 metrics-server。
附录 D:交付物清单¶
| 文件/目录 | 说明 |
|---|---|
gitlab-deploy-v2/values-gitlab.yaml |
部署参数(不含任何口令,凭据一律引用 Secret) |
gitlab-deploy-v2/SOP.md |
本文档 |
gitlab-deploy-v2/RUNBOOK.md |
服务日常运行与排障手册 |
gitlab-deploy-v2/INCIDENT-20260917-01-gitlab-部署与迁移期排障.md |
变更期故障台账 |
gitlab-deploy-v2/backup/ |
备份与台账(见 backup/README.md) |
gitlab-deploy-v2/gitlab/、gitlab-10.3.2.tgz |
chart 源码与离线包 |
gitlab-deploy-v2/rendered-*.yaml |
渲染产物(复核用,非部署输入) |
ns_migration/ |
命名空间统一变更的方案、执行记录与备份 |
附录 E:相关变更与文档¶
| 事项 | 主题目录 | 说明 |
|---|---|---|
命名空间统一到 server |
ns_migration/ |
2026-09-18 执行(重建路径,当时无业务数据);旧数据目录在 k8s-node-01 上以 .bak.20260918005523 后缀保留,确认无用后可删。文档:GitLab迁移到server-方案与风险.md |
| 对外暴露链路(公网入口、TLS、隧道) | ingress_tunnel_deploy/ |
GitLab 公网地址为 https://gitlab.rainchen.top,TLS 与链路细节见该主题的 RUNBOOK.md |
| 集群自身部署 | k8s_136_cluster_deploy/ |
节点、CNI、基础组件 |
| NFS 存储 | nfs-storage/、nfs-mount-tuning/ |
nfs-csi / nfs-csi-hdd 的供给与调优 |
修订记录:2026-09-19 按更新后的
writing-ops-change-sop规范重写(补目录、补齐验收实测与本变更相关的备份台账联动、与 RUNBOOK 分工去重)。
来源:
/home/rain/Documents/k8s_ins/gitlab-deploy-v2/SOP.md| 最后更新:2026-09-19 16:51(由build/sync.py同步,请勿在站点侧修改)