跳转至

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. 背景

集群为自建 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. 目标

变更完成后应达到的可验证状态:

  1. server 命名空间内 GitLab 全栈(webservice / sidekiq / gitaly / gitlab-shell / kas / registry / toolbox / exporter)全部 Pod Running 且 Ready,gitlab-migrations-* Job Completed。
  2. 依赖组件同命名空间就绪:CNPG Cluster gitlab-pg 状态 healthy;redis-0 Running(AOF + noeviction);minio-0 Running 且已有 6 个 bucket。
  3. Gateway gitlab-gw 为 PROGRAMMED=True;HTTPRoute 按 gitlab.rainchen.top / registry.rainchen.top / kas.rainchen.top 分流,/users/sign_in 返回 200,未知域名返回 404。
  4. Gitaly / PostgreSQL / Redis 的 PVC 绑定到 local-ssd 的 3 个 local PV,且 Pod 落在 k8s-node-01;MinIO 的 PVC 绑定 nfs-csi。
  5. 对象存储内存在 gitlab-artifacts、git-lfs、gitlab-uploads、gitlab-packages、gitlab-registry、gitlab-backups 六个 bucket。
  6. 不部署 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,导致全部 Pod FailedMount。上文三条均建在 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 没有 Succeeded condition,会误报超时(行为正确但判据错)。用 --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 同步,请勿在站点侧修改)