使用 Velero 备份和恢复 GitLab

本指南演示如何使用 Velero 实现 GitLab 备份和恢复操作。Velero 是一款开源的云原生灾难恢复工具。

适用范围

本方案适用于运行 17.8 或更高版本 的 GitLab 实例。

TIP

如果你的 GitLab 实例已经配置了对象存储,建议优先使用 官方备份和恢复方案

如何判断是否启用了对象存储?

GitLab 数据由三个主要部分组成:PostgreSQL 数据库、Git 仓库以及已上传的制品(包括用户头像、评论附件等)。本方案仅提供 Git 仓库和已上传制品的备份支持。PostgreSQL 数据库需要根据数据库服务提供商的备份策略单独进行备份。

DANGER

本方案不支持使用 HostPath 存储部署的 GitLab 实例。

术语

术语定义
Source Instance备份前的 GitLab 实例
Target Instance恢复后的 GitLab 实例
Source Namespace源实例所在的 namespace
Target Namespace目标实例所在的 namespace
GitLab CR Resource描述 GitLab 实例部署配置的资源,由 operator 用于部署 GitLab 实例

前提条件

  1. 部署 MinIO 对象存储:当前备份和恢复方案依赖对象存储保存备份数据,因此需要预先部署 MinIO 实例。ACP 提供 MinIO Object Storage,可通过 快速创建 MinIO 实例 使用。
  2. 部署 Velero:Velero 是用于备份和恢复的工具。ACP 提供 Alauda Container Platform Data Backup for Velero,你可以在 Administrator 视图的 Marketplace -> Cluster Plugins 页面中搜索 Velero 并进行部署。
  3. 安装 mc 命令行工具:mc 是 MinIO 用于管理 MinIO 实例的命令行工具。安装说明请参考 MinIO 官方文档
  4. 安装 kubectl 命令行工具:kubectl 是 Kubernetes 用于管理 Kubernetes 集群的命令行工具。安装说明请参考 Kubernetes 官方文档

为便于后续操作,请先设置环境变量:

export MINIO_HOST=<MinIO instance access address> # Example: http://192.168.1.100:32008
export MINIO_ACCESS_KEY=<MinIO instance access key> # Example: minioadmin
export MINIO_SECRET_KEY=<MinIO instance secret key> # Example: minioadminpassword
export MINIO_ALIAS_NAME=<MinIO instance alias name> # Example: myminio

export VELERO_BACKUP_BUCKET=<Velero backup bucket name> # Example: backup
export VELERO_BACKUP_REPO_NAME=<Velero backup repo name> # Example: gitlab-backup-repo

export GITLAB_NAMESPACE=<namespace of the GitLab instance to be backed up>
export GITLAB_NAME=<name of the GitLab instance to be backed up>

执行以下命令配置 mc 命令行工具并测试连接:

mc alias set ${MINIO_ALIAS_NAME} ${MINIO_HOST} ${MINIO_ACCESS_KEY} ${MINIO_SECRET_KEY}
mc ping ${MINIO_ALIAS_NAME}

# Output:
#   1: http://192.168.131.56:32571:32571   min=98.86ms    max=98.86ms    average=98.86ms    errors=0   roundtrip=98.86ms
#   2: http://192.168.131.56:32571:32571   min=29.57ms    max=98.86ms    average=64.21ms    errors=0   roundtrip=29.57ms
#   3: http://192.168.131.56:32571:32571   min=29.57ms    max=98.86ms    average=52.77ms    errors=0   roundtrip=29.88ms

如果你可以成功 ping 通 MinIO 实例,说明 mc 已正确配置。

备份

准备工作

备份准备分为两个步骤:

  1. 创建 Bucket
  2. 配置 Velero 备份仓库

创建 Bucket

执行以下命令创建用于存储备份数据的 Bucket:

mc mb ${MINIO_ALIAS_NAME}/${VELERO_BACKUP_BUCKET}

# Output:
# Bucket created successfully `myminio/backup`.

配置 Velero 备份仓库

执行以下命令创建用于存储备份数据的 Velero 备份仓库:

gitlab_backup_repo_credentials_secret_name="gitlab-backup-repo-credentials"
minio_gitlab_backup_bucket="${VELERO_BACKUP_BUCKET:-backup}"
minio_gitlab_backup_bucket_directory="gitlab"

kubectl apply -f - <<EOF
apiVersion: v1
stringData:
  cloud: |
    [default]
    aws_access_key_id = ${MINIO_ACCESS_KEY}
    aws_secret_access_key = ${MINIO_SECRET_KEY}
kind: Secret
metadata:
  labels:
    component: velero
    cpaas.io/backup-storage-location-repo: ${VELERO_BACKUP_REPO_NAME}
  name: ${gitlab_backup_repo_credentials_secret_name}
  namespace: cpaas-system
type: Opaque
---
apiVersion: velero.io/v1
kind: BackupStorageLocation
metadata:
  name: ${VELERO_BACKUP_REPO_NAME}
  namespace: cpaas-system
spec:
  config:
    checksumAlgorithm: ""
    insecureSkipTLSVerify: "true"
    region: minio
    s3ForcePathStyle: "true"
    s3Url: ${MINIO_HOST}
  credential:
    key: cloud
    name: ${gitlab_backup_repo_credentials_secret_name}
  objectStorage:
    bucket: ${minio_gitlab_backup_bucket}
    prefix: ${minio_gitlab_backup_bucket_directory}
  provider: aws
EOF

# Output
# secret/gitlab-backup-repo-credentials created
# backupstoragelocationrepo.ait.velero.io/gitlab-backup-repo created

执行备份

手动备份包含六个步骤:

  1. 为备份资源打标签
  2. 停止 GitLab 服务
  3. 创建备份 pod
  4. 创建备份计划
  5. 执行备份
  6. 恢复 GitLab 服务

为备份资源打标签

执行以下脚本:

if [ -z "${GITLAB_NAMESPACE}" ] || [ -z "${GITLAB_NAME}" ]; then
    cat <<EOF
    Please set the correct namespace and instance name:
    export GITLAB_NAME=<original GitLab instance name>
    export GITLAB_NAMESPACE=<original namespace name>
EOF
    return
fi

kubectl label gitlabofficial -n ${GITLAB_NAMESPACE} ${GITLAB_NAME} release=${GITLAB_NAME} --overwrite

secrets_to_label=()

root_password_secret_name=$(kubectl get gitlabofficial -n ${GITLAB_NAMESPACE} ${GITLAB_NAME} -o jsonpath='{.spec.helmValues.global.initialRootPassword.secret}')
secrets_to_label+=(${root_password_secret_name})

rails_secret_name=$(kubectl get gitlabofficial -n ${GITLAB_NAMESPACE} ${GITLAB_NAME} -o jsonpath='{.spec.helmValues.global.railsSecrets.secret}')
if [ -n "${rails_secret_name}" ]; then
    echo "Found rails secret: ${rails_secret_name}"
    secrets_to_label+=(${rails_secret_name})
fi

pg_connect_secret_name=$(kubectl get gitlabofficial -n ${GITLAB_NAMESPACE} ${GITLAB_NAME} -o jsonpath='{.spec.helmValues.global.psql.password.secret}')
secrets_to_label+=(${pg_connect_secret_name})

praefect_db_secret_name=$(kubectl get gitlabofficial -n ${GITLAB_NAMESPACE} ${GITLAB_NAME} -o jsonpath='{.spec.helmValues.global.praefect.dbSecret.secret}')
if [ -n "${praefect_db_secret_name}" ]; then
    echo "Found praefect database secret: ${praefect_db_secret_name}"
    secrets_to_label+=(${praefect_db_secret_name})
fi

ingress_secret_name=$(kubectl get gitlabofficial -n ${GITLAB_NAMESPACE} ${GITLAB_NAME} -o jsonpath='{.spec.helmValues.global.ingress.tls.secretName}')
if [ -n "${ingress_secret_name}" ]; then
    echo "Found ingress secret: ${ingress_secret_name}"
    secrets_to_label+=(${ingress_secret_name})
fi

redis_connect_secret_name=$(kubectl get gitlabofficial -n ${GITLAB_NAMESPACE} ${GITLAB_NAME} -o jsonpath='{.spec.helmValues.global.redis.auth.secret}')
if [ -n "${redis_connect_secret_name}" ]; then
    echo "Found redis auth secret: ${redis_connect_secret_name}"
    secrets_to_label+=(${redis_connect_secret_name})
fi

redis_sentinel_connect_secret_name=$(kubectl get gitlabofficial -n ${GITLAB_NAMESPACE} ${GITLAB_NAME} -o jsonpath='{.spec.helmValues.global.redis.sentinelAuth.secret}')
if [ -n "${redis_sentinel_connect_secret_name}" ]; then
    echo "Found redis sentinel connect secret: ${redis_sentinel_connect_secret_name}"
    secrets_to_label+=(${redis_sentinel_connect_secret_name})
fi

object_store_secret_name=$(kubectl get gitlabofficial -n ${GITLAB_NAMESPACE} ${GITLAB_NAME} -o jsonpath='{.spec.helmValues.global.appConfig.object_store.connection.secret}')
if [ -n "${object_store_secret_name}" ]; then
    echo "Found object storage connect secret: ${object_store_secret_name}"
    kubectl label secret ${object_store_secret_name} -n ${GITLAB_NAMESPACE} release=${GITLAB_NAME}
fi

for secret_name in "${secrets_to_label[@]}"; do
  echo "Add label to secret: ${secret_name}"
  kubectl label secret ${secret_name} -n ${GITLAB_NAMESPACE} release=${GITLAB_NAME} --overwrite
done

kubectl get pvc -n ${GITLAB_NAMESPACE} --no-headers | grep ${GITLAB_NAME} | awk '{print $1}' | xargs -I {} kubectl label pvc {} release=${GITLAB_NAME} -n ${GITLAB_NAMESPACE}

kubectl get secret -n ${GITLAB_NAMESPACE} --no-headers | grep ${GITLAB_NAME} | awk '{print $1}' | xargs -I {} kubectl label secret {} release=${GITLAB_NAME} -n ${GITLAB_NAMESPACE}

# Output:
# secret/sh.helm.release.v1.xxx-gitlab.v9 not labeled
WARNING

如果 Redis Secret 由 GitLab 实例外部管理,例如由 Alauda Cache Service for Redis OSS 管理,则外部 controller 可能会移除该脚本添加的备份标签。在这种情况下,Velero 将不会备份 Redis Secret。请将外部管理的 Redis Secret 视为外部依赖:先手动备份它们,并在启动恢复后的 GitLab 实例之前将其恢复到目标 namespace 中。

如果还需要备份其他资源,请参考 为备份资源添加额外标签 章节。

停止 GitLab 服务

备份期间必须停止所有 GitLab 组件:

kubectl annotate statefulset -n ${GITLAB_NAMESPACE} -l release=${GITLAB_NAME} skip-sync="true"
kubectl scale statefulset -n ${GITLAB_NAMESPACE} -l release=${GITLAB_NAME} --replicas=0

kubectl annotate deployment -n ${GITLAB_NAMESPACE} -l release=${GITLAB_NAME} skip-sync="true"
kubectl scale deployment -n ${GITLAB_NAMESPACE} -l release=${GITLAB_NAME} --replicas=0

kubectl delete pod -n ${GITLAB_NAMESPACE} -l release=${GITLAB_NAME} --ignore-not-found

# Output:
# statefulset.apps/xxxx-gitlab-gitaly annotated
# deployment.apps/xxxx-gitlab-webservice-default annotated
# deployment.apps/xxxx-gitlab-toolbox scaled
# deployment.apps/xxxx-gitlab-webservice-default scaled

创建备份 Pod

此步骤会创建一个挂载 GitLab PVC 的 pod,以便 Velero 完成 PVC 数据备份。

image=''

if [ -z "${IMAGE}" ]; then
  image=$(kubectl get deployment -n ${GITLAB_NAMESPACE} -l release=${GITLAB_NAME},app=gitlab-shell -o jsonpath='{range .items[0].spec.template.spec.containers[]}{.image}{"\n"}{end}' | head -n1)
fi

PVC_NAMES=($(kubectl get pvc -n ${GITLAB_NAMESPACE} -l release=${GITLAB_NAME} -o jsonpath='{range .items[*]}{.metadata.name}{" "}{end}'))

VOLUME_MOUNTS=""
VOLUMES=""
INDEX=0
for pvc in ${PVC_NAMES[@]}; do
  VOLUME_MOUNTS="${VOLUME_MOUNTS}
        - name: data-${INDEX}
          mountPath: /mnt/data-${INDEX}"
  VOLUMES="${VOLUMES}
    - name: data-${INDEX}
      persistentVolumeClaim:
        claimName: ${pvc}"
  INDEX=$((INDEX+1))
done

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: ${GITLAB_NAME}-backup-pod
  namespace: ${GITLAB_NAMESPACE}
  labels:
    release: ${GITLAB_NAME}
spec:
  containers:
    - name: backup
      image: ${image}
      command: ["/bin/sh", "-c", "sleep 86400"]
      resources:
        limits:
          cpu: 1
          memory: 1Gi
      volumeMounts:${VOLUME_MOUNTS}
  volumes:${VOLUMES}
  restartPolicy: Never
EOF

kubectl wait --for=condition=ready pod -n ${GITLAB_NAMESPACE} ${GITLAB_NAME}-backup-pod

# Output
# pod/xxxx-gitlab-backup-pod created
# pod/xxxx-gitlab-backup-pod condition met

创建备份卷策略

卷策略用于指定 PVC 的备份方式。默认策略是使用 fs-backup 方式备份 PVC。

export BACKUP_POLICY_NAME=${BACKUP_POLICY_NAME:-gitlab-backup}
export VELERO_BACKUP_REPO_NAME=${VELERO_BACKUP_REPO_NAME:-backup}

kubectl apply -f - <<EOF
apiVersion: v1
kind: ConfigMap
metadata:
  name: ${BACKUP_POLICY_NAME}-volume-policy
  namespace: cpaas-system
data:
  resourcepolicies: |
    version: v1
    volumePolicies:
      - conditions:
          csi: {}
        action:
          type: fs-backup
      - conditions:
          csi: {}
        action:
          type: fs-backup
EOF

# Output
# configmap/gitlab-backup-volume-policy created

如果 PVC 支持快照,可以使用快照来备份 PVC,以加快备份速度。你需要修改 volume policy configmap,指定某个 storage class 使用快照进行备份。例如,如果 ceph storage class 支持快照备份,则需要添加以下配置(新增条件应放在最前面以获得更高优先级):

如何判断 PVC 是否支持快照?

WARNING

如果你使用 pvc 快照备份,恢复时不能更改 storage class。请根据实际情况调整 storage class。

apiVersion: v1
kind: ConfigMap
metadata:
  name: ${BACKUP_POLICY_NAME}-volume-policy
  namespace: cpaas-system
data:
  resourcepolicies: |
    version: v1
    volumePolicies:
+      - conditions:
+          storageClass:
+            - ceph
+        action:
+          type: snapshot
      - conditions:
          csi: {}
        action:
          type: fs-backup
      - conditions:
          csi: {}
        action:
          type: fs-backup
WARNING

如果 velero 未启用 CSI snapshot,备份时会出现以下错误:

Skip action velero.io/csi-pvc-backupper for resource persistentvolumeclaims:xxx, because the CSI feature is not enabled.

要修复此问题,需要在 velero deployment 中添加 --features=EnableCSI 参数。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: velero
spec:
  template:
    spec:
      containers:
      - args:
        - server
        - --uploader-type=restic
+        - --features=EnableCSI
        - --namespace=cpaas-system
        command:
        - /velero
        name: velero

执行备份

执行以下命令创建备份计划并触发备份任务:

export BACKUP_POLICY_NAME=${BACKUP_POLICY_NAME:-gitlab-backup}
export VELERO_BACKUP_REPO_NAME=${VELERO_BACKUP_REPO_NAME:-backup}

kubectl apply -f - <<EOF
apiVersion: velero.io/v1
kind: Schedule
metadata:
  name: ${BACKUP_POLICY_NAME}
  namespace: cpaas-system
spec:
  schedule: '@every 876000h'
  template:
    defaultVolumesToFsBackup: true
    hooks: {}
    includedNamespaces:
    - ${GITLAB_NAMESPACE}
    includedResources:
    - '*'
    resourcePolicy:
      kind: configmap
      name: ${BACKUP_POLICY_NAME}-volume-policy
    orLabelSelectors:
    - matchLabels:
        release: ${GITLAB_NAME}
    storageLocation: ${VELERO_BACKUP_REPO_NAME}
    ttl: 720h0m0s
EOF

kubectl create -f - <<EOF
apiVersion: velero.io/v1
kind: Backup
metadata:
  labels:
    velero.io/schedule-name: ${BACKUP_POLICY_NAME}
    velero.io/storage-location: ${VELERO_BACKUP_REPO_NAME}
  generateName: ${BACKUP_POLICY_NAME}-
  namespace: cpaas-system
spec:
  csiSnapshotTimeout: 10m0s
  defaultVolumesToFsBackup: true
  includedNamespaces:
    - ${GITLAB_NAMESPACE}
  includedResources:
    - "*"
  itemOperationTimeout: 4h0m0s
  resourcePolicy:
    kind: configmap
    name: ${BACKUP_POLICY_NAME}-volume-policy
  orLabelSelectors:
    - matchLabels:
        release: ${GITLAB_NAME}
  snapshotMoveData: false
  storageLocation: ${VELERO_BACKUP_REPO_NAME}
  ttl: 720h0m0s
EOF

# Output
# schedule.velero.io/gitlab-backup created
# backup.velero.io/gitlab-backup-r6hht created

查看备份日志:

kubectl logs -f -n cpaas-system -l app.kubernetes.io/instance=velero | grep gitlab-backup

# Output
# time="2025-07-01T07:29:54Z" level=info msg="PodVolumeBackup completed" controller=PodVolumeBackup logSource="pkg/controller/pod_volume_backup_controller.go:244" pvb=gitlab-backup-xxx-q7prc

检查任务进度。如果状态为 Completed,则表示备份成功。

kubectl get backup -n cpaas-system -o jsonpath="{range .items[*]}{.metadata.name}{'\t'}{.status.phase}{'\t'}{.status.startTimestamp}{'\n'}{end}"

# Output
# Completed

恢复 GitLab 服务

删除备份 pod 并恢复所有 GitLab 组件。

kubectl delete pod -n ${GITLAB_NAMESPACE} ${GITLAB_NAME}-backup-pod

kubectl annotate statefulset -n ${GITLAB_NAMESPACE} -l release=${GITLAB_NAME} skip-sync-
kubectl scale statefulset -n ${GITLAB_NAMESPACE} -l release=${GITLAB_NAME} --replicas=1

kubectl annotate deployment -n ${GITLAB_NAMESPACE} -l release=${GITLAB_NAME} skip-sync-
kubectl scale deployment -n ${GITLAB_NAMESPACE} -l release=${GITLAB_NAME} --replicas=1

所有组件恢复后,访问 GitLab 实例以确保其正常运行。

恢复

准备工作

恢复准备分为五个步骤:

  1. 恢复 PostgreSQL 数据库(可选)
  2. 准备 Redis Secret(可选)
  3. 选择要恢复的备份
  4. 确定恢复目标 namespace
  5. 卸载 Gitlab CE Operator

恢复 PostgreSQL 数据库(可选)

如果新实例仍将使用相同的 PostgreSQL 实例和数据库,则无需恢复 PostgreSQL 数据库。

如果新实例将使用与旧实例不同的 PostgreSQL 实例或数据库,则需要先恢复 PostgreSQL 数据库。请参考 PostgreSQL 服务提供商的备份和恢复文档。

准备 Redis Secret(可选)

如果新实例仍将使用相同的 Redis 实例和 Secret 名称,请确保目标 namespace 中存在 Redis Secret。当 Redis Secret 由 GitLab 实例外部管理时,例如由 Alauda Cache Service for Redis OSS 管理,外部 controller 可能会移除在“为备份资源打标签”步骤中添加的备份标签。在这种情况下,Velero 不会备份 Redis Secret,你需要手动备份,并将其恢复或复制到目标 namespace。

如果新实例将使用与旧实例不同的 Redis 实例或 Secret 名称,请先在目标 namespace 中为新实例创建 Redis Secret。然后你将在 修改 GitLab CR 资源 步骤中更新 GitLab CR,以使用新的 Redis 连接信息。

选择要恢复的备份数据

执行以下命令查看成功备份记录列表,并根据开始时间选择所需的备份。

kubectl get backup -n cpaas-system -o jsonpath="{range .items[*]}{.metadata.name}{'\t'}{.status.phase}{'\t'}{.status.startTimestamp}{'\n'}{end}" | grep Completed

# Output:
# gitlab-backup-xxx     Completed       2025-07-01T07:29:19Z

设置 BACKUP_NAME 环境变量:

export BACKUP_NAME=<selected backup record name>

确定目标 namespace

建议将实例恢复到一个新的 namespace。设置以下环境变量:

export NEW_GITLAB_NAMESPACE=<new namespace name>
kubectl create namespace ${NEW_GITLAB_NAMESPACE}

卸载 Gitlab CE Operator

执行以下命令卸载 GitLab CE Operator:

kubectl get subscription --all-namespaces | grep gitlab-ce-operator | awk '{print "kubectl delete subscription "$2" -n "$1}' | sh

# Output:
# subscription.operators.coreos.com "gitlab-ce-operator" deleted
为什么要卸载 GitLab operator?

在恢复过程中,Velero 需要启动备份 pod 以恢复 PVC 中的数据,这会花费一些时间。如果在恢复期间不卸载 operator,可能会出现以下问题:

  1. operator 可能会根据 GitLab CR 重新创建工作负载资源,导致已恢复的 pod 重启或被重新创建,最终造成恢复中断或失败。
  2. 某些已恢复的资源可能会发生冲突,例如 operator 根据已恢复的 GitLab CR 创建的 ingress 资源可能会与旧实例的 ingress 资源冲突。
卸载 GitLab CE Operator 的影响

如果卸载了 operator,对 GitLab CR 资源的修改将不会生效,例如调整资源或存储大小。

卸载 operator 不会导致现有实例异常。

恢复操作

恢复操作包含五个步骤:

  1. 创建恢复配置文件
  2. 创建恢复任务
  3. 清理资源
  4. 修改 GitLab CR 资源
  5. 部署 Gitlab CE Operator

创建恢复配置文件

请仔细阅读 YAML 中的注释,并根据实际情况进行修改(例如更改 storage class),然后创建修改后的 YAML。

# If you need to change storage class, set the following two environment variables
OLD_STORAGECLASS_NAME='' # Original storage class name
NEW_STORAGECLASS_NAME='' # New storage class name

if [ -n "${OLD_STORAGECLASS_NAME}" ] && [ -n "${NEW_STORAGECLASS_NAME}" ] && [ ${NEW_STORAGECLASS_NAME} != ${OLD_STORAGECLASS_NAME} ]; then
kubectl apply -f - <<EOF
apiVersion: v1
data:
  resourcemodifier: |
    version: v1
    resourceModifierRules:
      - conditions:
          groupResource: persistentvolumeclaims
          resourceNameRegex: .*
          namespaces:
            - "*"
        patches: &a1
          - operation: test
            path: /spec/storageClassName
            value: ${OLD_STORAGECLASS_NAME}
          - operation: replace
            path: /spec/storageClassName
            value: ${NEW_STORAGECLASS_NAME}
      - conditions:
          groupResource: persistentvolume
          resourceNameRegex: .*
          namespaces:
            - "*"
        patches: *a1
      - conditions:
          groupResource: persistentvolumeclaims
          resourceNameRegex: .*
          namespaces:
            - "*"
        patches:
          - operation: test
            path: "/metadata/annotations/meta.helm.sh~1release-namespace"
            value: ${GITLAB_NAMESPACE}
          - operation: replace
            path: "/metadata/annotations/meta.helm.sh~1release-namespace"
            value: ${NEW_GITLAB_NAMESPACE}
kind: ConfigMap
metadata:
  labels:
    component: velero
  name: gitlab-restore-modifier
  namespace: cpaas-system
EOF

else

kubectl apply -f - <<EOF
apiVersion: v1
data:
  resourcemodifier: |
    version: v1
    resourceModifierRules:
      - conditions:
          groupResource: persistentvolumeclaims
          resourceNameRegex: .*
          namespaces:
            - "*"
        patches:
          - operation: test
            path: "/metadata/annotations/meta.helm.sh~1release-namespace"
            value: ${GITLAB_NAMESPACE}
          - operation: replace
            path: "/metadata/annotations/meta.helm.sh~1release-namespace"
            value: ${NEW_GITLAB_NAMESPACE}
kind: ConfigMap
metadata:
  labels:
    component: velero
  name: gitlab-restore-modifier
  namespace: cpaas-system
EOF
fi

创建恢复任务

执行以下命令创建恢复任务:

kubectl create -f - <<EOF
apiVersion: velero.io/v1
kind: Restore
metadata:
  generateName: ${GITLAB_NAME}-restore-
  namespace: cpaas-system
spec:
  backupName: ${BACKUP_NAME}
  hooks: {}
  includedNamespaces:
    - ${GITLAB_NAMESPACE}
  includedResources:
    - persistentvolumeclaims
    - persistentvolumes
    - secrets
    - pods
    - gitlabofficials.operator.alaudadevops.io
    - volumesnapshots
    - volumesnapshotcontents
  itemOperationTimeout: 10h0m0s
  namespaceMapping:
    ${GITLAB_NAMESPACE}: ${NEW_GITLAB_NAMESPACE}
  resourceModifier:
    kind: configmap
    name: gitlab-restore-modifier
EOF

查看恢复日志:

kubectl logs -f -n cpaas-system -l app.kubernetes.io/instance=velero | grep gitlab-restore

# Output
# time="2025-07-01T08:01:41Z" level=info msg="Async fs restore data path completed" PVR=xxxx-gitlab-restore-mv6l5-sc4pk controller=PodVolumeRestore logSource="pkg/controller/pod_volume_restore_controller.go:275" pvr=xxxx-gitlab-restore-mv6l5-sc4pk
# time="2025-07-01T08:01:41Z" level=info msg="Restore completed" controller=PodVolumeRestore logSource="pkg/controller/pod_volume_restore_controller.go:327" pvr=xxxx-gitlab-restore-mv6l5-sc4pk

检查任务进度。如果状态为 Completed,则表示恢复成功。

kubectl get restore -n cpaas-system -o jsonpath="{range .items[*]}{.metadata.name}{'\t'}{.status.phase}{'\t'}{.status.startTimestamp}{'\n'}{end}"


# Output
# xxx-gitlab-restore-xxx    InProgress      2025-07-01T10:18:17Z

清理资源

请确保恢复操作已完成后再继续!

执行以下命令完成资源清理。

kubectl get pod -n ${NEW_GITLAB_NAMESPACE} | grep ${GITLAB_NAME} | awk '{print $1}' | xargs kubectl delete pod -n ${NEW_GITLAB_NAMESPACE}

kubectl get secret -n ${NEW_GITLAB_NAMESPACE} | grep sh.helm.release.v1.${GITLAB_NAME} | awk '{print $1}' | xargs kubectl delete secret -n ${NEW_GITLAB_NAMESPACE}

修改 GitLab CR 资源

kubectl edit gitlabofficial ${GITLAB_NAME} -n ${NEW_GITLAB_NAMESPACE}

新实例的 CR 资源可能需要进行以下修改。请根据实际情况进行调整。

  1. 域名
    • 适用场景:原实例通过域名方式部署
    • 修改原因:旧实例和新实例创建的 ingress 资源中如果域名相同,会发生冲突,导致新实例的 ingress 资源创建失败
    • 修改建议
      • 推荐)将原实例的域名改为临时域名,保持新实例不变
      • 保持原实例域名不变,将新实例的域名改为新的域名
    • 修改方法:请参考 配置实例网络访问 修改实例访问域名。
  2. NodePort
    • 适用场景:原实例通过 NodePort 方式部署
    • 修改原因:旧实例和新实例创建的 service 资源中如果 NodePort 相同,会发生冲突,导致新实例的 service 资源创建失败
    • 修改建议
      • 推荐)将原实例的 NodePort 改为临时端口,保持新实例不变
      • 保持原实例的 NodePort 不变,将新实例的 NodePort 改为新的端口
    • 修改方法:请参考 配置实例网络访问 修改实例访问配置。
  3. Storage Class
    • 适用场景:原实例使用 storage class 部署,且新旧实例之间的 storage class 发生变化(例如原实例使用 NFS,新实例使用 Ceph)
    • 修改原因:如果不修改,operator 仍会使用旧的 storage class 创建 PVC,这会与已恢复的 PVC 发生冲突
    • 修改建议:将 storage class 修改为正确的值
    • 修改方法:请参考 配置实例存储 修改实例存储配置。
  4. PostgreSQL 连接信息
    • 适用场景:新实例和旧实例计划使用不同的 PostgreSQL 实例或数据库
    • 修改原因:如果不修改,新实例仍会连接到旧的 PostgreSQL 实例或数据库
    • 修改建议:将 PostgreSQL 连接信息修改为正确的值
    • 修改方法:请参考 配置 PostgreSQL 访问凭证配置 修改 PostgreSQL 访问凭证配置。
  5. Redis 连接信息
    • 适用场景:新实例和旧实例计划使用不同的 Redis 实例或 Secret 名称
    • 修改原因:如果不修改,新实例仍会连接到旧的 Redis 实例或使用旧的 Redis Secret 名称
    • 修改建议:将 Redis 连接信息修改为正确的值
    • 修改方法:请参考 配置 Redis 访问凭证配置 修改 Redis 访问凭证配置。

部署 Gitlab CE Operator

进入 Administrator 视图,前往 Marketplace -> OperatorHub 页面,重新部署 Alauda Build of Gitlab operator。

部署 Operator 后,operator 会根据 GitLab CR 部署新实例。你可以在实例详情页查看实例部署进度。

等待实例状态恢复正常后,登录 GitLab 检查数据是否已成功恢复。检查项包括但不限于:

  • Groups
  • Repositories
  • Users
  • Merge Requests

Q & A

为备份添加额外资源标签

此步骤为可选步骤。如果你在同一个 namespace 中还有其他希望一起备份的资源,可以为相应资源添加 release 标签。标签值应为原始 GitLab 实例名称,例如:

metadata:
  labels:
    release: gitlab-g6zd4

如何判断 PVC 是否支持快照?

要判断某个 PVC 是否支持快照,需要检查其底层 StorageClass 是否支持快照功能。如果集群中存在一个 driver 与该 StorageClass 的 provisioner 匹配的 VolumeSnapshotClass,则该 PVC 支持快照。换句话说,只要存在这样的 VolumeSnapshotClass,该 StorageClass(以及对应的 PVC)就支持快照功能。

示例配置:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: ceph
provisioner: rook-ceph.cephfs.csi.ceph.com
parameters:
  clusterID: rook-ceph
  csi.storage.k8s.io/controller-expand-secret-name: rook-csi-cephfs-provisioner
  csi.storage.k8s.io/controller-expand-secret-namespace: rook-ceph
  csi.storage.k8s.io/node-stage-secret-name: rook-csi-cephfs-node
  csi.storage.k8s.io/node-stage-secret-namespace: rook-ceph
  csi.storage.k8s.io/provisioner-secret-name: rook-csi-cephfs-provisioner
  csi.storage.k8s.io/provisioner-secret-namespace: rook-ceph
  fsName: cephfs
  pool: cephfs-data0
reclaimPolicy: Delete
volumeBindingMode: Immediate
allowVolumeExpansion: true
---
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
  name: csi-cephfs-snapshotclass
deletionPolicy: Delete
driver: rook-ceph.cephfs.csi.ceph.com
parameters:
  clusterID: rook-ceph
  csi.storage.k8s.io/snapshotter-secret-name: rook-csi-cephfs-provisioner
  csi.storage.k8s.io/snapshotter-secret-namespace: rook-ceph

关键点:

  • StorageClass 的 provisioner 必须与 VolumeSnapshotClass 的 driver 完全一致
  • 这两个资源都必须存在于集群中
  • CSI driver 必须支持快照操作