在 Azure Kubernetes 服务 (AKS) 上从树内存储类迁移到 CSI 驱动程序

从版本 1.21 开始,Azure Kubernetes 服务 (AKS) 引入了容器存储接口 (CSI) 驱动程序的实现。 采用 CSI 并将其作为标准时,应迁移或升级使用树内永久性卷 (PV) 的现有有状态工作负载,以使用 CSI 驱动程序。

为了使此过程尽可能简单,并确保不会丢失数据,本文提供了不同的迁移选项。 这些选项包括有助于确保从树内磁盘顺利迁移到 Azure 磁盘和 Azure 文件 CSI 驱动程序的脚本。

开始之前

  • Azure CLI 版本 2.37.0 或更高版本。 可通过运行 az --version 查找版本,运行 az upgrade 升级版本。 如果需要进行安装或升级,请参阅安装 Azure CLI
  • Kubectl 和群集管理员有权创建、获取、列出、删除对 PVC 或 PV、卷快照或卷快照内容的访问权限。 对于已启用 Microsoft Entra RBAC 的群集,你是 Azure Kubernetes 服务 RBAC 群集管理员角色的成员。

迁移磁盘卷

注意

标签 failure-domain.beta.kubernetes.io/zonefailure-domain.beta.kubernetes.io/region 在 AKS 1.24 中已被弃用,在 1.28 中已被删除。 如果现有永久性卷仍在使用与这两个标签匹配的 nodeAffinity,则需要将其更改为新的永久性卷设置中的 topology.kubernetes.io/zonetopology.kubernetes.io/region 标签。

使用两个迁移选项支持从树内迁移到 CSI:

  • 创建静态卷
  • 创建动态卷

创建静态卷

使用此选项,可以通过将 claimRef 静态分配给稍后要创建的新 PVC 来创建 PV,并为 PersistentVolumeClaim 指定 volumeName

Static volume workflow diagram.

此方法的优点包括:

  • 此工作流很简单,而且可以自动化。
  • 无需使用树内存储类清理原始配置。
  • 风险低,因为仅执行 Kubernetes PV/PVC 的逻辑删除,不会删除实际的物理数据。
  • 无额外费用,因为不需要创建额外的 Azure 对象,如磁盘、快照等。

以下是要评估的重要注意事项:

  • 从原始动态样式卷转换到静态卷时需要为所有选项手动构造和管理 PV 对象。
  • 根据新的 PVC 对象重新部署新应用程序时潜在的应用程序停机时间。

迁移

  1. 运行以下命令,将现有 PV ReclaimPolicy 从“删除”更新为“保留”:

    kubectl patch pv pvName -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'
    

    将“pvName”替换为所选 PersistentVolume 的名称。 或者,如果要更新多个 PV 的 reclaimPolicy,请创建一个名为 patchReclaimPVs.sh 的文件,并复制到以下代码中。

    #!/bin/bash
    # Patch the Persistent Volume in case ReclaimPolicy is Delete
    NAMESPACE=$1
    i=1
    for PVC in $(kubectl get pvc -n $NAMESPACE | awk '{ print $1}'); do
      # Ignore first record as it contains header
      if [ $i -eq 1 ]; then
        i=$((i + 1))
      else
        PV="$(kubectl get pvc $PVC -n $NAMESPACE -o jsonpath='{.spec.volumeName}')"
        RECLAIMPOLICY="$(kubectl get pv $PV -n $NAMESPACE -o jsonpath='{.spec.persistentVolumeReclaimPolicy}')"
        echo "Reclaim Policy for Persistent Volume $PV is $RECLAIMPOLICY"
        if [[ $RECLAIMPOLICY == "Delete" ]]; then
          echo "Updating ReclaimPolicy for $pv to Retain"
          kubectl patch pv $PV -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'
        fi
      fi
    done
    

    使用 namespace 参数执行脚本以指定群集命名空间 ./PatchReclaimPolicy.sh <namespace>

  2. 运行以下命令,获取命名空间中按 creationTimestamp 排序的所有 PVC 的列表。 使用 --namespace 参数和实际群集命名空间设置命名空间。

    kubectl get pvc -n <namespace> --sort-by=.metadata.creationTimestamp -o custom-columns=NAME:.metadata.name,CreationTime:.metadata.creationTimestamp,StorageClass:.spec.storageClassName,Size:.spec.resources.requests.storage
    

    如果你有大量需要迁移的 PV,并且想要一次迁移几个 PV,则此步骤非常有用。 运行此命令可以确定在给定时间范围内创建了哪些 PVC。 运行 CreatePV.sh 脚本时,其中两个参数是开始时间和结束时间,使你能够仅在该时间段内迁移 PVC。

  3. 创建一个名为 CreatePV.sh 的文件并将以下 YAML 复制到其中。 此脚本执行以下任务:

    • 为存储类 storageClassName 的命名空间中的所有 PersistentVolume 创建名为 existing-pv-csi 的新 PersistentVolume。
    • 将新 PVC 名称配置为 existing-pvc-csi
    • 使用指定的 PV 名称创建新的 PVC。
    #!/bin/bash
    #kubectl get pvc -n <namespace> --sort-by=.metadata.creationTimestamp -o custom-columns=NAME:.metadata.name,CreationTime:.metadata.creationTimestamp,StorageClass:.spec.storageClassName,Size:.spec.resources.requests.storage
    # TimeFormat 2022-04-20T13:19:56Z
    NAMESPACE=$1
    FILENAME=$(date +%Y%m%d%H%M)-$NAMESPACE
    EXISTING_STORAGE_CLASS=$2
    STORAGE_CLASS_NEW=$3
    STARTTIMESTAMP=$4
    ENDTIMESTAMP=$5
    i=1
    for PVC in $(kubectl get pvc -n $NAMESPACE | awk '{ print $1}'); do
      # Ignore first record as it contains header
      if [ $i -eq 1 ]; then
        i=$((i + 1))
      else
        PVC_CREATION_TIME=$(kubectl get pvc  $PVC -n $NAMESPACE -o jsonpath='{.metadata.creationTimestamp}')
        if [[ $PVC_CREATION_TIME >= $STARTTIMESTAMP ]]; then
          if [[ $ENDTIMESTAMP > $PVC_CREATION_TIME ]]; then
            PV="$(kubectl get pvc $PVC -n $NAMESPACE -o jsonpath='{.spec.volumeName}')"
            RECLAIM_POLICY="$(kubectl get pv $PV -n $NAMESPACE -o jsonpath='{.spec.persistentVolumeReclaimPolicy}')"
            STORAGECLASS="$(kubectl get pv $PV -n $NAMESPACE -o jsonpath='{.spec.storageClassName}')"
            echo $PVC
            RECLAIM_POLICY="$(kubectl get pv $PV -n $NAMESPACE -o jsonpath='{.spec.persistentVolumeReclaimPolicy}')"
            if [[ $RECLAIM_POLICY == "Retain" ]]; then
              if [[ $STORAGECLASS == $EXISTING_STORAGE_CLASS ]]; then
                STORAGE_SIZE="$(kubectl get pv $PV -n $NAMESPACE -o jsonpath='{.spec.capacity.storage}')"
                SKU_NAME="$(kubectl get storageClass $STORAGE_CLASS_NEW -o jsonpath='{.parameters.skuname}')"
                DISK_URI="$(kubectl get pv $PV -n $NAMESPACE -o jsonpath='{.spec.azureDisk.diskURI}')"
                PERSISTENT_VOLUME_RECLAIM_POLICY="$(kubectl get pv $PV -n $NAMESPACE -o jsonpath='{.spec.persistentVolumeReclaimPolicy}')"
    
                cat >$PVC-csi.yaml <<EOF
        apiVersion: v1
        kind: PersistentVolume
        metadata:
          annotations:
            pv.kubernetes.io/provisioned-by: disk.csi.azure.com
          name: $PV-csi
        spec:
          accessModes:
          - ReadWriteOnce
          capacity:
            storage: $STORAGE_SIZE
          claimRef:
            apiVersion: v1
            kind: PersistentVolumeClaim
            name: $PVC-csi
            namespace: $NAMESPACE
          csi:
            driver: disk.csi.azure.com
            volumeAttributes:
              csi.storage.k8s.io/pv/name: $PV-csi
              csi.storage.k8s.io/pvc/name: $PVC-csi
              csi.storage.k8s.io/pvc/namespace: $NAMESPACE
              requestedsizegib: "$STORAGE_SIZE"
              skuname: $SKU_NAME
            volumeHandle: $DISK_URI
          persistentVolumeReclaimPolicy: $PERSISTENT_VOLUME_RECLAIM_POLICY
          storageClassName: $STORAGE_CLASS_NEW
    ---
    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      name: $PVC-csi
      namespace: $NAMESPACE
    spec:
      accessModes:
        - ReadWriteOnce
      storageClassName: $STORAGE_CLASS_NEW
      resources:
        requests:
          storage: $STORAGE_SIZE
      volumeName: $PV-csi
    EOF
                kubectl apply -f $PVC-csi.yaml
                LINE="PVC:$PVC,PV:$PV,StorageClassTarget:$STORAGE_CLASS_NEW"
                printf '%s\n' "$LINE" >>$FILENAME
              fi
            fi
          fi
        fi
      fi
    done
    
  4. 若要为命名空间中的所有 PersistentVolumes 创建新的 PersistentVolume,请使用以下参数执行脚本 CreatePV.sh:

    • namespace - 群集命名空间
    • sourceStorageClass - 基于树内存储驱动程序的 StorageClass
    • targetCSIStorageClass - 基于 CSI 存储驱动程序的 StorageClass,可以是将预配器设置为 disk.csi.azure.com 或 file.csi.azure.com 的默认存储类之一。 或者,可以创建自定义存储类,只要它设置为这两个预配器之一。
    • startTimeStamp - 以 yyyy-mm-ddthh:mm:ssz 格式提供 PVC 创建时间之前的开始时间
    • endTimeStamp - 提供格式为 yyyy-mm-ddthh:mm:ssz 的结束时间。
    ./CreatePV.sh <namespace> <sourceIntreeStorageClass> <targetCSIStorageClass> <startTimestamp> <endTimestamp>
    
  5. 更新应用程序以使用新 PVC。

创建动态卷

使用此选项,可以根据永久性卷声明动态创建永久性卷。

Dynamic volume workflow diagram.

此方法的优点包括:

  • 风险较低,因为所有新对象都是在保留包含快照的其他副本的同时创建的。

  • 无需单独构造 PV 并在 PVC 清单中添加卷名称。

以下是要评估的重要注意事项:

  • 虽然这种方法的风险较低,但它确实会创建多个对象,这会增加存储成本。

  • 在创建新卷期间,应用程序不可用。

  • 应谨慎执行删除步骤。 临时资源锁可以应用于资源组,直到迁移完成并成功验证应用程序。

  • 根据快照创建新磁盘时执行数据验证。

迁移

在继续之前,请验证以下事项:

  • 对于数据在写入磁盘之前先写入内存的特定工作负载,应停止应用程序并允许将内存中的数据刷新到磁盘。

  • VolumeSnapshot 类应存在,如以下示例 YAML 所示:

    apiVersion: snapshot.storage.k8s.io/v1
    kind: VolumeSnapshotClass
    metadata:
      name: custom-disk-snapshot-sc
    driver: disk.csi.azure.com
    deletionPolicy: Delete
    parameters:
      incremental: "false"
    
  1. 运行以下命令,获取特定命名空间中按 creationTimestamp 排序的所有 PVC 的列表。 使用 --namespace 参数和实际群集命名空间设置命名空间。

    kubectl get pvc --namespace <namespace> --sort-by=.metadata.creationTimestamp -o custom-columns=NAME:.metadata.name,CreationTime:.metadata.creationTimestamp,StorageClass:.spec.storageClassName,Size:.spec.resources.requests.storage
    

    如果你有大量需要迁移的 PV,并且想要一次迁移几个 PV,则此步骤非常有用。 运行此命令可以确定在给定时间范围内创建了哪些 PVC。 运行 MigrateCSI.sh 脚本时,其中两个参数是开始时间和结束时间,使你能够仅在该时间段内迁移 PVC。

  2. 创建一个名为 MigrateCSI.sh 的文件并将以下 YAML 复制到其中。 此脚本执行以下任务:

    • 使用 Azure CLI 创建完整磁盘快照
    • 创建 VolumesnapshotContent
    • 创建 VolumeSnapshot
    • VolumeSnapshot 创建一个新的 PVC
    • 创建一个文件名为 <namespace>-timestamp 的新文件,其中包含需要清理的所有旧资源的列表。
    #!/bin/bash
    #kubectl get pvc -n <namespace> --sort-by=.metadata.creationTimestamp -o custom-columns=NAME:.metadata.name,CreationTime:.metadata.creationTimestamp,StorageClass:.spec.storageClassName,Size:.spec.resources.requests.storage
    # TimeFormat 2022-04-20T13:19:56Z
    NAMESPACE=$1
    FILENAME=$NAMESPACE-$(date +%Y%m%d%H%M)
    EXISTING_STORAGE_CLASS=$2
    STORAGE_CLASS_NEW=$3
    VOLUME_STORAGE_CLASS=$4
    START_TIME_STAMP=$5
    END_TIME_STAMP=$6
    i=1
    for PVC in $(kubectl get pvc -n $NAMESPACE | awk '{ print $1}'); do
      # Ignore first record as it contains header
      if [ $i -eq 1 ]; then
        i=$((i + 1))
      else
        PVC_CREATION_TIME=$(kubectl get pvc $PVC -n $NAMESPACE -o jsonpath='{.metadata.creationTimestamp}')
        if [[ $PVC_CREATION_TIME > $START_TIME_STAMP ]]; then
          if [[ $END_TIME_STAMP > $PVC_CREATION_TIME ]]; then
            PV="$(kubectl get pvc $PVC -n $NAMESPACE -o jsonpath='{.spec.volumeName}')"
            RECLAIM_POLICY="$(kubectl get pv $PV -n $NAMESPACE -o jsonpath='{.spec.persistentVolumeReclaimPolicy}')"
            STORAGE_CLASS="$(kubectl get pv $PV -n $NAMESPACE -o jsonpath='{.spec.storageClassName}')"
            echo $PVC
            RECLAIM_POLICY="$(kubectl get pv $PV -n $NAMESPACE -o jsonpath='{.spec.persistentVolumeReclaimPolicy}')"
            if [[ $STORAGE_CLASS == $EXISTING_STORAGE_CLASS ]]; then
              STORAGE_SIZE="$(kubectl get pv $PV -n $NAMESPACE -o jsonpath='{.spec.capacity.storage}')"
              SKU_NAME="$(kubectl get storageClass $STORAGE_CLASS_NEW -o jsonpath='{.parameters.skuname}')"
              DISK_URI="$(kubectl get pv $PV -n $NAMESPACE -o jsonpath='{.spec.azureDisk.diskURI}')"
              TARGET_RESOURCE_GROUP="$(cut -d'/' -f5 <<<"$DISK_URI")"
              echo $DISK_URI
              SUBSCRIPTION_ID="$(echo $DISK_URI | grep -o 'subscriptions/[^/]*' | sed 's#subscriptions/##g')"
              echo $TARGET_RESOURCE_GROUP
              PERSISTENT_VOLUME_RECLAIM_POLICY="$(kubectl get pv $PV -n $NAMESPACE -o jsonpath='{.spec.persistentVolumeReclaimPolicy}')"
              az snapshot create --resource-group $TARGET_RESOURCE_GROUP --name $PVC-$FILENAME --source "$DISK_URI" --subscription ${SUBSCRIPTION_ID}
              SNAPSHOT_PATH=$(az snapshot list --resource-group $TARGET_RESOURCE_GROUP --query "[?name == '$PVC-$FILENAME'].id | [0]" --subscription ${SUBSCRIPTION_ID})
              SNAPSHOT_HANDLE=$(echo "$SNAPSHOT_PATH" | tr -d '"')
              echo $SNAPSHOT_HANDLE
              sleep 10
              # Create Restore File
              cat <<EOF >$PVC-csi.yml
        apiVersion: snapshot.storage.k8s.io/v1
        kind: VolumeSnapshotContent
        metadata:
          name: $PVC-$FILENAME
        spec:
          deletionPolicy: 'Delete'
          driver: 'disk.csi.azure.com'
          volumeSnapshotClassName: $VOLUME_STORAGE_CLASS
          source:
            snapshotHandle: $SNAPSHOT_HANDLE
          volumeSnapshotRef:
            apiVersion: snapshot.storage.k8s.io/v1
            kind: VolumeSnapshot
            name: $PVC-$FILENAME
            namespace: $1
    ---
        apiVersion: snapshot.storage.k8s.io/v1
        kind: VolumeSnapshot
        metadata:
          name: $PVC-$FILENAME
          namespace: $1
        spec:
          volumeSnapshotClassName: $VOLUME_STORAGE_CLASS
          source:
            volumeSnapshotContentName: $PVC-$FILENAME
    ---
        apiVersion: v1
        kind: PersistentVolumeClaim
        metadata:
          name: csi-$PVC
          namespace: $1
        spec:
          accessModes:
          - ReadWriteOnce
          storageClassName: $STORAGE_CLASS_NEW
          resources:
            requests:
              storage: $STORAGE_SIZE
          dataSource:
            name: $PVC-$FILENAME
            kind: VolumeSnapshot
            apiGroup: snapshot.storage.k8s.io
    
    EOF
              kubectl create -f $PVC-csi.yml
              LINE="OLDPVC:$PVC,OLDPV:$PV,VolumeSnapshotContent:volumeSnapshotContent-$FILENAME,VolumeSnapshot:volumesnapshot$FILENAME,OLDdisk:$DISK_URI"
              printf '%s\n' "$LINE" >>$FILENAME
            fi
          fi
        fi
      fi
    done
    
  3. 若要迁移磁盘卷,请使用以下参数执行脚本 MigrateToCSI.sh:

    • namespace - 群集命名空间
    • sourceStorageClass - 基于树内存储驱动程序的 StorageClass
    • targetCSIStorageClass - 基于 CSI 存储驱动程序的 StorageClass
    • volumeSnapshotClass - 卷快照类的名称。 例如 custom-disk-snapshot-sc
    • startTimeStamp - 提供格式为 yyyy-mm-ddthh:mm:ssz 的开始时间。
    • endTimeStamp - 提供格式为 yyyy-mm-ddthh:mm:ssz 的结束时间。
    ./MigrateToCSI.sh <namespace> <sourceStorageClass> <TargetCSIstorageClass> <VolumeSnapshotClass> <startTimestamp> <endTimestamp>
    
  4. 更新应用程序以使用新 PVC。

  5. 手动删除较旧的资源,包括树内 PVC/PV、VolumeSnapshot 和 VolumeSnapshotContent。 否则,维护树内 PVC/PC 和快照对象会产生更多成本。

迁移文件共享卷

通过创建静态卷,可以从树内迁移到 CSI:

  • 无需使用树内存储类清理原始配置。
  • 风险低,因为仅执行 Kubernetes PV/PVC 的逻辑删除,不会删除实际的物理数据。
  • 无额外费用,因为不需要创建额外的 Azure 对象,如文件共享等。

迁移

  1. 运行以下命令,将现有 PV ReclaimPolicy 从“删除”更新为“保留”:

    kubectl patch pv pvName -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'
    

    将“pvName”替换为所选 PersistentVolume 的名称。 或者,如果要更新多个 PV 的 reclaimPolicy,请创建一个名为 patchReclaimPVs.sh 的文件,并复制到以下代码中。

    #!/bin/bash
    # Patch the Persistent Volume in case ReclaimPolicy is Delete
    namespace=$1
    i=1
    for pvc in $(kubectl get pvc -n $namespace | awk '{ print $1}'); do
      # Ignore first record as it contains header
      if [ $i -eq 1 ]; then
        i=$((i + 1))
      else
        pv="$(kubectl get pvc $pvc -n $namespace -o jsonpath='{.spec.volumeName}')"
        reclaimPolicy="$(kubectl get pv $pv -n $namespace -o jsonpath='{.spec.persistentVolumeReclaimPolicy}')"
        echo "Reclaim Policy for Persistent Volume $pv is $reclaimPolicy"
        if [[ $reclaimPolicy == "Delete" ]]; then
          echo "Updating ReclaimPolicy for $pv to Retain"
          kubectl patch pv $pv -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'
        fi
      fi
    done
    

    使用 namespace 参数执行脚本以指定群集命名空间 ./PatchReclaimPolicy.sh <namespace>

  2. 创建一个新的存储类并将预配器设置为 file.csi.azure.com,或者将其中一个默认 StorageClasses 与 CSI 文件预配器配合使用。

  3. 通过运行以下命令,从现有 PersistentVolumes 中获取 secretNameshareName

    kubectl describe pv pvName
    
  4. 使用新的 StorageClass、shareNamesecretName 从树内 PV 创建新的 PV。 创建一个名为 azurefile-mount-pv.yaml 的文件并将以下代码复制到其中。 在 csi 下,更新 resourceGroupvolumeHandleshareName。 对于装载选项,fileMode 和 dirMode 的默认值为 0777。

    fileModedirMode 的默认值为 0777。

    apiVersion: v1
    kind: PersistentVolume
    metadata:
      name: azurefile
    spec:
      capacity:
        storage: 5Gi
      accessModes:
        - ReadWriteMany
      persistentVolumeReclaimPolicy: Retain
      storageClassName: azurefile-csi
      csi:
        driver: file.csi.azure.com
        readOnly: false
        volumeHandle: unique-volumeid  # make sure volumeid is unique for every identical share in the cluster
        volumeAttributes:
          resourceGroup: EXISTING_RESOURCE_GROUP_NAME  # optional, only set this when storage account is not in the same resource group as the cluster nodes
          shareName: aksshare
        nodeStageSecretRef:
          name: azure-secret
          namespace: default
      mountOptions:
        - dir_mode=0777
        - file_mode=0777
        - uid=0
        - gid=0
        - mfsymlinks
        - cache=strict
        - nosharesock
        - nobrl
    
  5. 使用以下代码创建一个名为 azurefile-mount-pvc.yaml 的文件,其中包含使用 PersistentVolumeClaim 的 PersistentVolume。

    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      name: azurefile
    spec:
      accessModes:
        - ReadWriteMany
      storageClassName: azurefile-csi
      volumeName: azurefile
      resources:
        requests:
          storage: 5Gi
    
  6. 使用 kubectl 命令创建 PersistentVolume。

    kubectl apply -f azurefile-mount-pv.yaml
    
  7. 使用 kubectl 命令创建 PersistentVolumeClaim。

    kubectl apply -f azurefile-mount-pvc.yaml
    
  8. 通过运行以下命令,验证 PersistentVolumeClaim 是否已创建并绑定到 PersistentVolume。

    kubectl get pvc azurefile
    

    输出如下所示:

    NAME        STATUS   VOLUME      CAPACITY   ACCESS MODES   STORAGECLASS   AGE
    azurefile   Bound    azurefile   5Gi        RWX            azurefile      5s
    
  9. 更新容器规范以引用 PersistentVolumeClaim并更新 Pod。 例如,复制以下代码并创建名为 azure-files-pod.yaml 的文件。

    ...
      volumes:
      - name: azure
        persistentVolumeClaim:
          claimName: azurefile
    
  10. 无法及时更新 Pod 规范。 使用以下 kubectl 命令删除 Pod,然后再重新创建 Pod。

    kubectl delete pod mypod
    
    kubectl apply -f azure-files-pod.yaml
    

后续步骤