从 GitLab 14.0.12 迁移数据到 17.8.z

解决的问题

3.16 和 3.18 版本支持的 GitLab 版本滞后于官方版本。升级 GitLab 到最新官方版本需要超过 10 次升级才能完成。升级到 4.0 后,产品不再提供自动升级到 17.8.z 的功能。本方案介绍如何将数据从 14.0.12 迁移到 17.8.z。

鉴于版本跨度较大,升级采用数据迁移的方式。

术语

术语说明
all-in-one GitLab所有 GitLab 组件打包在一个容器或 Pod 中一起部署。用于快速版本升级。
platform-deployed GitLab由平台通过 Operator 部署和管理的 GitLab,组件分离部署。

流程概述

数据迁移路径

根据官方升级路径文档,数据迁移路径如下:

  • 14.0.12 -> 14.3.6
  • 14.3.6 -> 14.9.5
  • 14.9.5 -> 14.10.5
  • 14.10.5 -> 15.0.5
  • 15.0.5 -> 15.4.6
  • 15.4.6 -> 15.11.13
  • 15.11.13 -> 16.3.8
  • 16.3.8 -> 16.7.9
  • 16.7.9 -> 16.11.10
  • 16.11.10 -> 17.3.6
  • 17.3.6 → 17.5.5
  • 17.5.5 → 17.8.1

最后备份 17.8.1 的数据并导入到 platform-deployed 的 17.8.z 实例,完成数据迁移。

数据迁移流程

备份 platform-deployed GitLab 并恢复到 all-in-one 镜像部署的 GitLab,使用 all-in-one 镜像升级至 17.8.1,最后将数据备份恢复到 platform-deployed 17.8.z 实例。

  1. 备份 platform-deployed GitLab 14.0.12 实例。
  2. 将备份恢复到 all-in-one 部署的 GitLab 14.0.12。
  3. all-in-one 部署进行滚动升级至 GitLab 17.8.1。
  4. 备份升级后的 all-in-one GitLab 17.8.1 实例。
  5. all-in-one 17.8.1 的备份恢复到 platform-deployed GitLab 17.8.z 实例。
TIP

最新的 GitLab 实例和 operator 版本,请参考Release Note

迁移时长

迁移过程涉及数据库和仓库的备份/恢复操作:

  • 数据库越大,迁移时间越长。
  • 仓库数量越多或单个仓库越大,耗时显著增加。
  • 存储性能也影响效率,建议使用 topolvm 以获得更好性能。

测试环境:

  • 一个大仓库(约 600 MB),其他为小型初始仓库
  • GitLab 实例:6,700 个 issue,3,600 个 merge request
迁移步骤关键因素1,000 个仓库10,000 个仓库
备份 platform-deployed GitLab 14.0.12仓库大小和数量10 分钟30 分钟
恢复到 all-in-one GitLab 14.0.12仓库大小和数量20 分钟1 小时
滚动升级到 GitLab 17.8.1数据库大小1 小时 30 分1 小时 40 分
备份 all-in-one GitLab 17.8.1仓库大小和数量3 分钟10 分钟
恢复到 platform-deployed GitLab仓库大小和数量30 分钟1 小时

前置条件

  1. 在执行主机安装 kubectl (>=1.30.0) 和 yq (>=4.45.0)。

    kubectl version --client
    yq --version
    
    # 输出示例:
    # Client Version: v1.30.0
    # Kustomize Version: v5.4.2
    # yq (https://github.com/mikefarah/yq/) version v4.45.4
  2. 配置环境变量:

    # 替换为旧 GitLab CR 的名称
    export GITLAB_NAME=<old gitlab name>
    export GITLAB_NAMESPACE=<gitlab deploy namespace>
  3. 准备升级所需的 PVC,必须在旧 GitLab 实例的同一命名空间中创建:

    TIP

    估算原实例所需存储大小的简单方法是将数据库和 Gitaly 组件使用的 PVC 大小相加。

    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      name: backup-pvc
      namespace: ${GITLAB_NAMESPACE}
    spec:
      accessModes:
        - ReadWriteMany
      resources:
        requests:
          storage: 40Gi
      storageClassName: ceph
    ---
    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      name: upgrade-pvc
      namespace: ${GITLAB_NAMESPACE}
    spec:
      accessModes:
        - ReadWriteMany
      resources:
        requests:
          storage: 20Gi
      storageClassName: ceph
      volumeMode: Filesystem
    1. backup-pvc:用于存储备份包,建议容量为原实例大小的两倍。
    2. 替换为集群中的存储类名称。
    3. upgrade-pvc:用于滚动升级过程中存储 GitLab 数据,建议容量为原实例总存储大小加上数据库大小的三倍。
    4. 替换为集群中的存储类名称。
  4. 准备升级所需镜像。下载附件中的各版本镜像,并推送到 GitLab 部署集群可拉取的镜像仓库。镜像下载链接:

    中国区
    其他区域
    # amd 镜像
    https://cloud.alauda.cn/attachments/knowledge/242090509/gitlab-ce-14-0-12-amd.tgz
    https://cloud.alauda.cn/attachments/knowledge/242090509/gitlab-ce-14-3-6-amd.tgz
    https://cloud.alauda.cn/attachments/knowledge/242090509/gitlab-ce-14-9-5-amd.tgz
    https://cloud.alauda.cn/attachments/knowledge/242090509/gitlab-ce-14-10-5-amd.tgz
    https://cloud.alauda.cn/attachments/knowledge/242090509/gitlab-ce-15-0-5-amd.tgz
    https://cloud.alauda.cn/attachments/knowledge/242090509/gitlab-ce-15-4-6-amd.tgz
    https://cloud.alauda.cn/attachments/knowledge/242090509/gitlab-ce-15-11-13-amd.tgz
    https://cloud.alauda.cn/attachments/knowledge/242090509/gitlab-ce-16-3-8-amd.tgz
    https://cloud.alauda.cn/attachments/knowledge/242090509/gitlab-ce-16-7-9-amd.tgz
    https://cloud.alauda.cn/attachments/knowledge/242090509/gitlab-ce-16-11-10-amd.tgz
    https://cloud.alauda.cn/attachments/knowledge/242090509/gitlab-ce-17-3-6-amd.tgz
    https://cloud.alauda.cn/attachments/knowledge/242090509/gitlab-ce-17-5-5-amd.tgz
    https://cloud.alauda.cn/attachments/knowledge/242090509/gitlab-ce-17-8-1-amd.tgz
    
    # arm 镜像
    https://cloud.alauda.cn/attachments/knowledge/242090509/gitlab-ce-14-0-12-arm.tgz
    https://cloud.alauda.cn/attachments/knowledge/242090509/gitlab-ce-14-3-6-arm.tgz
    https://cloud.alauda.cn/attachments/knowledge/242090509/gitlab-ce-14-9-5-arm.tgz
    https://cloud.alauda.cn/attachments/knowledge/242090509/gitlab-ce-14-10-5-arm.tgz
    https://cloud.alauda.cn/attachments/knowledge/242090509/gitlab-ce-15-0-5-arm.tgz
    https://cloud.alauda.cn/attachments/knowledge/242090509/gitlab-ce-15-4-6-arm.tgz
    https://cloud.alauda.cn/attachments/knowledge/242090509/gitlab-ce-15-11-13-arm.tgz
    https://cloud.alauda.cn/attachments/knowledge/242090509/gitlab-ce-16-3-8-arm.tgz
    https://cloud.alauda.cn/attachments/knowledge/242090509/gitlab-ce-16-7-9-arm.tgz
    https://cloud.alauda.cn/attachments/knowledge/242090509/gitlab-ce-16-11-10-arm.tgz
    https://cloud.alauda.cn/attachments/knowledge/242090509/gitlab-ce-17-3-6-arm.tgz
    https://cloud.alauda.cn/attachments/knowledge/242090509/gitlab-ce-17-5-5-arm.tgz
    https://cloud.alauda.cn/attachments/knowledge/242090509/gitlab-ce-17-8-1-arm.tgz
  5. 准备升级所需脚本。

    执行以下命令生成四个脚本:

  • check_gitlab.sh:检查 GitLab pod 是否就绪。
  • finalize_migrations.sql:完成迁移任务。
  • monitor_gitlab.sh:监控 GitLab pod,检查数据迁移是否完成。
  • rolling_upgrade.sh:逐步滚动升级 GitLab 实例。
命令
生成的脚本
cat << 'EOF' > check_gitlab.sh
#!/bin/bash

gitlab_pod=$1
port=${2:-30855}

log() {
    echo "$(date '+%Y-%m-%d %H:%M:%S') - $1"
}

log "Starting monitoring script for GitLab pod: ${gitlab_pod} on port: ${port}"

while true; do
  curl_result=$(kubectl -n $GITLAB_NAMESPACE exec ${gitlab_pod} -- curl -s localhost:${port} | grep "You are being")
  if [[ -z "${curl_result}" ]]; then
      log "HTTP not ready. Retrying in 10 seconds..."
      sleep 10
      continue
  fi

  log "GitLab is ready"
  break
done
EOF

cat << 'EOF' > finalize_migrations.sql
select concat(
  'gitlab-rake gitlab:background_migrations:finalize[',
  job_class_name, ',',
  table_name, ',',
  column_name, $$,'$$,
  REPLACE(cast(job_arguments as text), ',', $$\,$$),
  $$']$$
)
from batched_background_migrations WHERE status NOT IN(3, 6);
EOF

cat << 'EOF' > monitor_gitlab.sh
#!/bin/bash

gitlab_pod=$1
port=${2:-30855}

log() {
    echo "$(date '+%Y-%m-%d %H:%M:%S') - $1"
}

log "Starting monitoring script for GitLab pod: ${gitlab_pod} on port: ${port}"

while true; do
  curl_result=$(kubectl -n $GITLAB_NAMESPACE exec ${gitlab_pod} -- curl -s localhost:${port} | grep "You are being")
  if [[ -z "${curl_result}" ]]; then
      log "HTTP check not ready. Retrying in 10 seconds..."
      sleep 10
      continue
  fi

  migration_result=$(kubectl -n $GITLAB_NAMESPACE exec ${gitlab_pod} -- gitlab-psql -c "SELECT job_class_name, table_name,  column_name, job_arguments FROM batched_background_migrations WHERE status NOT IN(3, 6);" | grep "(0 rows)")

  if [[ -z "${migration_result}" ]]; then
      log "Database migration is running. Retrying in 10 seconds..."
      sleep 10
      continue
  fi

  log "GitLab is ready, all migrations are done"
  break
done
EOF

cat << 'EOF' > rolling_upgrade.sh
#!/bin/bash

image=$(kubectl -n $GITLAB_NAMESPACE get deployment gitlab -o jsonpath='{.spec.template.spec.containers[?(@.name=="gitlab")].image} ')

versions=(
  14.0.12-ce.0
  14.3.6-ce.0
  14.9.5-ce.0
  14.10.5-ce.0
  15.0.5-ce.0
  15.4.6-ce.0
  15.11.13-ce.0
  16.3.8-ce.0
  16.7.9-ce.0
  16.11.10-ce.0
  17.3.6-ce.0
  17.5.5-ce.0
  17.8.1-ce.0
)

STATE_FILE="upgrade_state.log"
touch "$STATE_FILE"

is_done() {
  grep -Fxq "$1" "$STATE_FILE"
}

mark_done() {
  echo "$1" >> "$STATE_FILE"
}

for version in "${versions[@]}"; do
  if is_done "$version"; then
    echo "Version ${version} already processed, skipping..."
    continue
  fi

  echo "Upgrading to ${version}..."
  new_image="${image%:*}:${version}"
  kubectl -n $GITLAB_NAMESPACE set image deployment/gitlab gitlab=$new_image

  echo "Waiting for the GitLab pod to start..."
  sleep 10
  kubectl -n $GITLAB_NAMESPACE wait deploy gitlab --for condition=available --timeout=3000s
  sleep 10
  kubectl -n $GITLAB_NAMESPACE wait pod -l deploy=gitlab --for=condition=Ready --timeout=3000s

  new_pod_name=$(kubectl -n $GITLAB_NAMESPACE get po -l deploy=gitlab --sort-by=.metadata.creationTimestamp  --field-selector=status.phase=Running -o jsonpath='{.items[-1].metadata.name}')
  echo "Waiting for GitLab to be ready..."
  bash check_gitlab.sh $new_pod_name

  echo "Waiting for migrations to finish..."
  kubectl -n $GITLAB_NAMESPACE cp finalize_migrations.sql $new_pod_name:/tmp/finalize_migrations.sql
  for i in {1..10}; do
    echo "Running migration tasks (attempt $i/10)..."
    if kubectl -n $GITLAB_NAMESPACE exec -ti $new_pod_name -- bash -c "gitlab-psql -t -A -f /tmp/finalize_migrations.sql > /tmp/run_migration_tasks.sh && xargs -d \"\n\" -P 3 -I {} bash -c \"{}\" < /tmp/run_migration_tasks.sh"; then
      echo "Migration tasks completed successfully"
      break
    fi
    sleep 30
  done
  bash monitor_gitlab.sh $new_pod_name
  echo "Upgraded to ${version} successfully"
  mark_done "$version"
done

echo "All upgrades completed successfully!"
EOF

备份 platform-deployed GitLab 14.0.12

  1. 在 GitLab 14 上启用 task-runner 组件以执行备份命令

    Platform < 3.18.0 的命令
    Platform 3.18.0+ 的命令
    输出
    # 根据 gitaly 镜像生成 task-runner 的镜像仓库和标签
    image=$(kubectl get pod ${GITLAB_NAME}-gitaly-0 -n $GITLAB_NAMESPACE -o jsonpath='{.spec.containers[0].image}')
    tag=${image##*:}
    registry=${image%%/*}
    
    echo "registry: ${registry}"
    echo "tag: ${tag}"
    
    kubectl patch gitlabofficials.operator.devops.alauda.io "${GITLAB_NAME}" \
      -n "$GITLAB_NAMESPACE" \
      --type=merge \
      -p "$(cat <<EOF
    {
      "spec": {
        "helmValues": {
          "gitlab": {
            "task-runner": {
              "enabled": "true",
              "image": {
                "repository": "${registry}/devops/gitlab-org/build/cng/gitlab-task-runner-ce",
                "tag": "${tag}"
              },
              "resources": {
                "limits": {
                  "cpu": "2",
                  "memory": "4G"
                }
              }
            }
          }
        }
      }
    }
    EOF
    )"
    
    # 等待 task-runner 部署可用
    echo "Waiting for the task-runner deployment to be available..."
    sleep 60
    
    # 执行命令并确认 ${GITLAB_NAME}-task-runner 部署存在
    kubectl wait deploy ${GITLAB_NAME}-task-runner -n $GITLAB_NAMESPACE --for condition=available --timeout=3000s
    kubectl get deploy -n $GITLAB_NAMESPACE ${GITLAB_NAME}-task-runner
  2. 设置 GitLab 14 为只读模式

    命令
    输出
    kubectl patch deploy ${GITLAB_NAME}-webservice-default -n $GITLAB_NAMESPACE --type='merge' -p='{"metadata":{"annotations":{"skip-sync":"true"}}}'
    kubectl patch deploy ${GITLAB_NAME}-sidekiq-all-in-1-v1 -n $GITLAB_NAMESPACE --type='merge' -p='{"metadata":{"annotations":{"skip-sync":"true"}}}'
    kubectl scale deploy ${GITLAB_NAME}-webservice-default -n $GITLAB_NAMESPACE --replicas 0
    kubectl scale deploy ${GITLAB_NAME}-sidekiq-all-in-1-v1 -n $GITLAB_NAMESPACE --replicas 0
  3. 为 GitLab 14 task-runner 补丁备份 PVC,PVC 名称应为 backup-pvc

    命令
    输出
    kubectl patch deploy ${GITLAB_NAME}-task-runner -n $GITLAB_NAMESPACE --type='json' -p='
    [
      {
        "op": "add",
        "path": "/metadata/annotations/skip-sync",
        "value": "true"
      },
      {
        "op": "replace",
        "path": "/spec/template/spec/volumes/1",
        "value": {"name": "task-runner-tmp", "persistentVolumeClaim": {"claimName": "backup-pvc"}}
      }
    ]'
    
    echo "Waiting for the task-runner deployment to be available..."
    sleep 60
    
    # 执行命令并确认 ${GITLAB_NAME}-task-runner 部署存在
    kubectl wait deploy ${GITLAB_NAME}-task-runner -n $GITLAB_NAMESPACE --for condition=available --timeout=3000s
    kubectl get deploy -n $GITLAB_NAMESPACE ${GITLAB_NAME}-task-runner
  4. 备份 GitLab 14 数据

    命令
    输出
    pod_name=$(kubectl get po -n $GITLAB_NAMESPACE -l app=task-runner,release=${GITLAB_NAME} -o jsonpath='{.items[0].metadata.name}')
    kubectl exec -ti -n "$GITLAB_NAMESPACE" $pod_name -- gitlab-rake gitlab:backup:create SKIP=repositories
    
    # 给备份文件添加权限
    kubectl exec -ti -n "$GITLAB_NAMESPACE" $pod_name -- bash -c '
    chmod 777 /srv/gitlab/tmp/backups/*_gitlab_backup.tar
    ls -lh /srv/gitlab/tmp/backups
    '

    通过以下命令备份 rails-secret,文件保存为当前目录下的 gitlab14-rails-secret.yaml

    kubectl get secrets -n ${GITLAB_NAMESPACE} ${GITLAB_NAME}-rails-secret -o jsonpath="{.data['secrets\.yml']}" | base64 --decode | yq -o yaml > gitlab14-rails-secret.yaml
  5. 备份完成后移除 task-runner 组件。

    命令
    输出
    kubectl patch gitlabofficials.operator.devops.alauda.io ${GITLAB_NAME} -n $GITLAB_NAMESPACE --type='merge' -p='
    {
      "spec": {
        "helmValues": {
          "gitlab": {
            "task-runner": null
          }
        }
      }
    }'
    
    echo "Waiting for the task-runner deployment to be removed..."
    sleep 30
    
    kubectl get po -n "$GITLAB_NAMESPACE" -l app=task-runner,release=${GITLAB_NAME}

恢复备份数据到 all-in-one 部署的 14.0.12

  1. 使用 all-in-one 镜像部署 GitLab 14.0.12。

    首先设置 NODE_IP 环境变量,通过该 IP 和 NodePort 端口访问 all-in-one GitLab 实例。

    export NODE_IP=<node_ip>
    export GITLAB_IMAGE=<registry-host>/gitlab/gitlab-ce:14.0.12-ce.0

    创建 all-in-one GitLab 实例。

    命令
    输出
    kubectl apply -f - <<EOF
    apiVersion: v1
    kind: Service
    metadata:
      name: gitlab-http-nodeport
      namespace: ${GITLAB_NAMESPACE}
    spec:
      ports:
        - appProtocol: tcp
          name: web
          port: 30855
          protocol: TCP
          targetPort: 30855
          nodePort: 30855
      selector:
        deploy: gitlab
      type: NodePort
    ---
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: gitlab
      namespace: ${GITLAB_NAMESPACE}
    spec:
      progressDeadlineSeconds: 600
      replicas: 1
      revisionHistoryLimit: 10
      selector:
        matchLabels:
          deploy: gitlab
      strategy:
        rollingUpdate:
          maxSurge: 1
          maxUnavailable: 1
        type: RollingUpdate
      template:
        metadata:
          labels:
            deploy: gitlab
        spec:
          affinity: {}
          containers:
            - env:
                # all-in-one gitlab 的外部访问地址,需替换为集群节点的 IP 和 nodeport 端口
                - name: GITLAB_OMNIBUS_CONFIG
                  value: external_url 'http://${NODE_IP}:30855'
              image: ${GITLAB_IMAGE} # 可替换为可拉取的镜像
              imagePullPolicy: IfNotPresent
              name: gitlab
              ports:
                - containerPort: 30855
                  name: http
                  protocol: TCP
                - containerPort: 2424
                  name: ssh
                  protocol: TCP
              resources:
                limits:
                  cpu: "8"
                  memory: 8Gi
                requests:
                  cpu: 4
                  memory: "4Gi"
              securityContext:
                privileged: true
              terminationMessagePath: /dev/termination-log
              terminationMessagePolicy: File
              volumeMounts:
                - mountPath: /dev/shm
                  name: dshm
                - mountPath: /var/opt/gitlab/backups
                  name: backup
                  subPath: backups
                - mountPath: /etc/gitlab
                  name: gitlab-upgrade-data
                  subPath: config
                - mountPath: /var/log/gitlab
                  name: gitlab-upgrade-data
                  subPath: logs
                - mountPath: /var/opt/gitlab
                  name: gitlab-upgrade-data
                  subPath: data
          dnsPolicy: ClusterFirst
          restartPolicy: Always
          schedulerName: default-scheduler
          securityContext: {}
          terminationGracePeriodSeconds: 30
          volumes:
            - name: dshm
              emptyDir:
                medium: Memory
                sizeLimit: 512Mi
            - name: backup
              persistentVolumeClaim:
                claimName: backup-pvc
            - name: gitlab-upgrade-data
              persistentVolumeClaim:
                claimName: upgrade-pvc
    EOF

    检查 all-in-one GitLab 实例是否启动成功。

    命令
    输出
    pod_name=$(kubectl get po -l deploy=gitlab -n $GITLAB_NAMESPACE -o jsonpath='{.items[0].metadata.name}')
    bash check_gitlab.sh $pod_name
  2. 修改 all-in-one GitLab 的 rails secret 内容。将 secret_key_baseotp_key_basedb_key_baseopenid_connect_signing_keyci_jwt_signing_key 字段替换为备份的 GitLab 14 rails secret 中对应字段。

    命令
    输出
    # 复制 gitlab-secrets.json 到本地
    pod_name=$(kubectl get po -l deploy=gitlab -n $GITLAB_NAMESPACE -o jsonpath='{.items[0].metadata.name}')
    kubectl -n $GITLAB_NAMESPACE cp $pod_name:/etc/gitlab/gitlab-secrets.json all-in-one-gitlab-secrets.json
    
    # 替换为 gitlab 14 rails-secret 的数据备份
    KEYS=(secret_key_base otp_key_base db_key_base openid_connect_signing_key ci_jwt_signing_key)
    for key in "${KEYS[@]}"; do
      echo "Replace ${key} ..."
      export KEY_VALUE=$(yq eval -r --unwrapScalar=false ".production.${key}" gitlab14-rails-secret.yaml)
      yq eval ".gitlab_rails.${key} = env(KEY_VALUE)" all-in-one-gitlab-secrets.json -i
    done
    
    # 复制 gitlab-secrets.json 回 pod
    kubectl -n $GITLAB_NAMESPACE cp all-in-one-gitlab-secrets.json $pod_name:/etc/gitlab/gitlab-secrets.json

    禁用不必要的组件。

    cat <<EOF >> gitlab.rb
    prometheus['enable'] = false
    gitlab_kas['enable'] = false
    redis_exporter['enable'] = false
    gitlab_exporter['enable'] = false
    postgres_exporter['enable'] = false
    sidekiq['enable'] = false
    EOF
    
    # 复制 gitlab.rb 到 pod
    kubectl -n $GITLAB_NAMESPACE cp gitlab.rb $pod_name:/etc/gitlab/gitlab.rb
  3. 同步 Gitaly 数据到 All-in-One GitLab

    Tip

    此步骤必须在 all-in-one GitLab pod 所在节点执行。

    首先获取挂载路径,分别获取原 GitLab Gitaly 组件 PVC(名为 repo-data-${GITLAB_NAME}-gitaly-0)和 all-in-one GitLab 使用的升级 PVC(名为 upgrade-pvc)在节点上的挂载路径。

    使用 rsync 命令高效复制 Gitaly 数据从原 PVC 到 all-in-one GitLab 实例的 upgrade-pvc

    Tip

    使用 rsync 替代 scp,速度更快且效率更高。

    获取原实例存储类型

    persistence_type=$(kubectl get GitlabOfficial.operator.devops.alauda.io -n $GITLAB_NAMESPACE $GITLAB_NAME -o jsonpath='{.spec.persistence.type}')
    echo "gitlab persistence type: $persistence_type"
    LocalPath 部署
    PVC 或 StorageClass 部署
    origin_gitaly_local_mount_path=$(kubectl get GitlabOfficial.operator.devops.alauda.io -n $GITLAB_NAMESPACE $GITLAB_NAME -o jsonpath='{.spec.persistence.location.path}')/gitlab
    origin_gitaly_node_ip=<origin_gitaly_node_ip>
    all_in_one_upgrade_pvc_mount_path=<all_in_one_gitaly_mount_path>
    
    # 在 all-in-one pod 所在节点执行
    rsync -avh --chown=998:998 ${origin_gitaly_node_ip}:${origin_gitaly_local_mount_path}/ ${all_in_one_upgrade_pvc_mount_path}/data/git-data/repositories/
    
    # 检查仓库数据所有权
    ls -alh ${all_in_one_upgrade_pvc_mount_path}/data/git-data/repositories

    重启 pod 并等待启动。

    命令
    输出
    # 重启 pod
    kubectl delete po -l deploy=gitlab -n $GITLAB_NAMESPACE
    
    echo "Waiting for the pod to start..."
    sleep 10
    
    pod_name=$(kubectl get po -l deploy=gitlab -n $GITLAB_NAMESPACE -o jsonpath='{.items[0].metadata.name}')
    bash check_gitlab.sh $pod_name
  4. 恢复备份数据。

    TIP

    恢复过程中可能出现数据库相关错误,例如:

    • ERROR: role "xxx" does not exist
    • ERROR: function xxx does not exist
    • ERROR: permission denied to create extension "pg_stat_statements"
    • ERROR: could not open extension control file

    这些错误可忽略,详情见 (https://gitlab.com/gitlab-org/gitlab/-/issues/266988)。

    命令
    输出
    # 设置 gitlab 为只读
    pod_name=$(kubectl get po -l deploy=gitlab -n $GITLAB_NAMESPACE -o jsonpath='{.items[0].metadata.name}')
    kubectl -n $GITLAB_NAMESPACE exec -ti $pod_name -- gitlab-ctl stop puma
    kubectl -n $GITLAB_NAMESPACE exec -ti $pod_name -- gitlab-ctl stop sidekiq
    
    # 确认 puma 和 sidekiq 已停止
    kubectl -n $GITLAB_NAMESPACE exec -ti $pod_name -- gitlab-ctl status
    
    # 执行恢复
    backup_id=$(kubectl -n $GITLAB_NAMESPACE exec -ti $pod_name -- bash -c 'ls /var/opt/gitlab/backups/*14.0.12_gitlab_backup.tar | head -n1 | sed "s|.*/\([0-9_]*_14\.0\.12\)_gitlab_backup\.tar|\1|"')
    echo "Backup ID: ${backup_id}"
    
    echo "此步骤会多次提示是否继续,请每次选择“yes”以继续。"
    kubectl -n $GITLAB_NAMESPACE exec -ti $pod_name -- bash -c 'gitlab-backup restore BACKUP=${backup_id}'

    重启 pod 并等待启动,然后执行 gitlab-rake gitlab:check SANITIZE=true 检查 GitLab 完整性。

    命令
    输出
    # 重启 pod
    kubectl delete po -l deploy=gitlab -n $GITLAB_NAMESPACE
    
    # 等待 pod 启动
    sleep 10
    pod_name=$(kubectl get po -l deploy=gitlab -n $GITLAB_NAMESPACE -o jsonpath='{.items[0].metadata.name}')
    bash check_gitlab.sh $pod_name
    
    kubectl -n $GITLAB_NAMESPACE exec -ti $pod_name -- gitlab-rake gitlab:check SANITIZE=true

    登录 GitLab 检查实例恢复是否成功(root 用户密码与原实例相同),并执行以下命令获取 all-in-one 实例的访问地址。

    命令
    输出
    kubectl -n $GITLAB_NAMESPACE get pod -l deploy=gitlab -o yaml | grep external_url

滚动升级 all-in-one 部署的 gitlab 到 17.8.1

执行 rolling_upgrade.sh 脚本,按照升级路径逐步升级 all-in-one 实例到 17.8.1 版本。

bash -x rolling_upgrade.sh
Tip

滚动升级过程中数据库可能会自动重启,脚本可能出现如下错误,这是正常现象,脚本会自动重试并继续执行。

connections on Unix domain socket "/var/opt/gitlab/postgresql/.s.PGSQL.5432"

脚本执行完成后,进入 GitLab Web UI 检查实例版本是否为 17.8.1,并验证数据完整性,包括代码仓库、用户、issue、merge request 等。

备份 17.8.1 all-in-one 实例数据

停止 all-in-one 实例服务并备份数据,备份文件保存在 /var/opt/gitlab/backups/ 目录。

命令
输出
pod_name=$(kubectl get po -l deploy=gitlab -n $GITLAB_NAMESPACE -o jsonpath='{.items[0].metadata.name}')
kubectl -n $GITLAB_NAMESPACE exec -ti $pod_name -- bash -c '
gitlab-ctl stop puma
gitlab-ctl stop sidekiq
gitlab-ctl status
gitlab-rake gitlab:backup:create
'

停止 all-in-one 实例服务。

命令
输出
kubectl scale deploy gitlab -n $GITLAB_NAMESPACE --replicas 0

all-in-one 备份数据恢复到 platform-deployed 17.8.z

新 gitlab 实例的命名空间

新 gitlab 实例的命名空间必须与旧实例命名空间相同。

  1. 参考GitLab 部署指南使用 operator 部署新的 GitLab 17.8.z 实例。

    Tip
    1. 新部署的实例必须与旧实例在同一命名空间
    2. 新实例的访问方式不得与旧实例冲突,如域名、nodeport 等

    新实例部署完成后,设置新实例名称的环境变量:

    export NEW_GITLAB_NAME=<name of the new GitLab instance>
  2. 为新 GitLab 实例启用 toolbox 组件。

    kubectl patch gitlabofficial.operator.alaudadevops.io ${NEW_GITLAB_NAME} -n $GITLAB_NAMESPACE --type='merge' -p='
    {
      "spec": {
        "helmValues": {
          "gitlab": {
            "toolbox": {
              "enabled": true,
              "resources": {
                "limits": {
                  "cpu": 2,
                  "memory": "4G"
                }
              }
            }
          }
        }
      }
    }'

    等待实例完成重新部署。

    命令
    输出
    kubectl get po -l app=toolbox,release=${NEW_GITLAB_NAME} -n $GITLAB_NAMESPACE
  3. 将备份数据恢复到 operator 部署的 GitLab 17.8.z

    TIP

    恢复过程中可能出现数据库相关错误,例如:

    • ERROR: role "xxx" does not exist
    • ERROR: function xxx does not exist
    • ERROR: must be owner of extension

    这些错误可忽略,详情见 (https://gitlab.com/gitlab-org/gitlab/-/issues/266988)。

    关闭新 17.8.z gitlab 实例的外部访问,并补丁 toolbox 组件挂载备份 pvc。

    命令
    输出
    kubectl patch deploy ${NEW_GITLAB_NAME}-webservice-default -n $GITLAB_NAMESPACE --type='merge' -p='{"metadata":{"annotations":{"skip-sync":"true"}}}'
    kubectl patch deploy ${NEW_GITLAB_NAME}-sidekiq-all-in-1-v2 -n $GITLAB_NAMESPACE --type='merge' -p='{"metadata":{"annotations":{"skip-sync":"true"}}}'
    kubectl scale deploy ${NEW_GITLAB_NAME}-webservice-default -n $GITLAB_NAMESPACE --replicas 0
    kubectl scale deploy ${NEW_GITLAB_NAME}-sidekiq-all-in-1-v2 -n $GITLAB_NAMESPACE --replicas 0
    
    # 修改 toolbox,挂载 backup pvc 到 toolbox
    kubectl patch deploy ${NEW_GITLAB_NAME}-toolbox -n $GITLAB_NAMESPACE -p='{"metadata":{"annotations":{"skip-sync":"true"}}}'
    kubectl patch deploy ${NEW_GITLAB_NAME}-toolbox -n $GITLAB_NAMESPACE --type='json' -p='[{"op": "replace", "path": "/spec/template/spec/volumes/1", "value": {"name": "toolbox-tmp", "persistentVolumeClaim": {"claimName": "backup-pvc"}}}]'

    等待 toolbox pod 就绪,然后将备份数据恢复到新 GitLab 实例。

    命令
    输出
    toolbox_pod_name=$(kubectl get po -n $GITLAB_NAMESPACE -l release=$NEW_GITLAB_NAME,app=toolbox -o jsonpath='{.items[0].metadata.name}')
    
    # 备份旧备份文件
    kubectl -n $GITLAB_NAMESPACE exec -ti $toolbox_pod_name -- bash -c '
    mkdir -p /srv/gitlab/tmp/backup_tarballs
    mv /srv/gitlab/tmp/backups/*gitlab_backup.tar /srv/gitlab/tmp/backup_tarballs || true
    '
    
    backup_file=$(kubectl -n $GITLAB_NAMESPACE exec -ti $toolbox_pod_name -- bash -c "ls /srv/gitlab/tmp/backup_tarballs/*17.8.1_gitlab_backup.tar | head -n1 | tr -d '\r\n'")
    echo "Backup file of 17.8.1: ${backup_file}"
    
    kubectl -n $GITLAB_NAMESPACE exec -ti $toolbox_pod_name -- backup-utility --restore --skip-restore-prompt -f "file://${backup_file}"
  4. 替换新 GitLab 实例的 rails-secret。

    命令
    输出
    kubectl get secrets -n ${GITLAB_NAMESPACE} ${NEW_GITLAB_NAME}-rails-secret -o jsonpath="{.data['secrets\.yml']}" | base64 --decode | yq -o yaml > gitlab17-rails-secret.yaml
    yq eval-all 'select(fileIndex == 0) * select(fileIndex == 1)' gitlab17-rails-secret.yaml gitlab14-rails-secret.yaml > final-rails-secret.yml
    kubectl create secret generic rails-secret -n ${GITLAB_NAMESPACE} --from-file=secrets.yml=final-rails-secret.yml
    
    kubectl patch gitlabofficials.operator.alaudadevops.io ${NEW_GITLAB_NAME} -n $GITLAB_NAMESPACE --type='merge' -p='
    {
      "spec": {
        "helmValues": {
          "global": {
            "railsSecrets": {
              "secret": "rails-secret"
            }
          }
        }
      }
    }'
    
    kubectl patch deploy ${NEW_GITLAB_NAME}-webservice-default -n $GITLAB_NAMESPACE --type='json' -p='[{"op":"remove","path":"/metadata/annotations/skip-sync"}]'
    kubectl patch deploy ${NEW_GITLAB_NAME}-sidekiq-all-in-1-v2 -n $GITLAB_NAMESPACE --type='json' -p='[{"op":"remove","path":"/metadata/annotations/skip-sync"}]'
    kubectl patch deploy ${NEW_GITLAB_NAME}-toolbox -n $GITLAB_NAMESPACE --type='json' -p='[{"op":"remove","path":"/metadata/annotations/skip-sync"}]'
    
    kubectl delete pod -lrelease=${NEW_GITLAB_NAME} -n $GITLAB_NAMESPACE
    kubectl scale deploy ${NEW_GITLAB_NAME}-webservice-default -n $GITLAB_NAMESPACE --replicas 1
    kubectl scale deploy ${NEW_GITLAB_NAME}-sidekiq-all-in-1-v2 -n $GITLAB_NAMESPACE --replicas 1

    operator 会自动重新部署新 GitLab 实例,等待实例完成重新部署。

  5. 验证升级后数据是否正常。

    • 进入 GitLab UI 的管理员视图,检查仓库和用户数据是否正常。
    • 选择部分仓库,验证代码、分支和 merge request 是否正常。
  6. 清理未使用资源:

    • 如有需要,删除旧 GitLab 实例和旧 operator。
    • 如有需要,删除 all-in-one GitLab 实例。

FAQ

恢复上传数据

迁移的数据不包含头像和附件,导致新 GitLab 实例无法显示这些内容。解决方法是将 GitLab 17.8.z 的 uploads PVC 替换为 14.0.12 使用的 PVC。

设置 uploads pvc 名称的环境变量:

export UPLOADS_PVC_NAME=<name of the uploads pvc>

补丁新 GitLab 实例使用 uploads PVC:

kubectl patch gitlabofficials.operator.devops.alaudadevops.io ${NEW_GITLAB_NAME} -n $GITLAB_NAMESPACE --type='json' -p='
{
  "spec": {
    "helmValues": {
      "global": {
        "uploads": {
          "persistence": {
            "enabled": true,
            "existingClaim": "${UPLOADS_PVC_NAME}"
          }
        }
      }
    }
  }
}'

如果旧 GitLab 实例使用 HostPath 存储,必须手动将旧实例 uploads 目录的数据复制到新实例 webservice 组件的 /srv/gitlab/public/uploads 路径。

如何查找节点上 PVC 的挂载路径

按照以下步骤查找 Kubernetes 节点上 PersistentVolumeClaim (PVC) 的挂载路径:

  1. 获取 Pod 所在节点 IP

    kubectl -n $GITLAB_NAMESPACE get pod <pod-name> -o jsonpath='{.status.hostIP}'
    
    # 输出示例:
    # 192.168.xxx.xxx
  2. 获取绑定到 PVC 的卷名称

    kubectl get pvc -n $GITLAB_NAMESPACE <pvc-name> -o jsonpath='{.spec.volumeName}'
    
    # 输出示例:
    # pvc-359bb8d2-841e-406c-a5e7-e48bc701f610
  3. 查找节点上 PVC 的挂载路径

    登录节点(使用步骤 1 的节点 IP),执行:

    df -h | grep <pv-name>
    
    # 输出示例:
    # 192.168.xx.xx:6789:/volumes/csi/csi-vol-4661bd66-a68f-42f6-b995-53bd5c3f79a8/ede3ffa1-0f77-4f44-b0e6-41ec7384484f   10G  216M  9.8G   3% /var/lib/kubelet/pods/7772096f-9b81-4698-8369-34689e36fc3a/   volumes/kubernetes.io~csi/pvc-359bb8d2-841e-406c-a5e7-e48bc701f610/mount

    最后一列显示的路径,例如:

    /var/lib/kubelet/pods/7772096f-9b81-4698-8369-34689e36fc3a/volumes/kubernetes.io~csi/pvc-359bb8d2-841e-406c-a5e7-e48bc701f610/mount

    即为该 PVC 在节点上的挂载目录。