基于kubectl的k8s强一致性资源迁移实践

    公司运行了2年多的PaaS平台即将升级,k8s版本跨度较大:从1.12到1.18,且涉及3000+的pods,为实现平稳过度,采用保留老平台、建设新平台即新老平台异构容灾的模式实现平稳过度。决定实施成败的关键在于:如何确保新老平台资源模板较低的差异率?因为:新老平台资源模板高度一致,能够大幅降低了排错难度,降低版本发布难度。

    通常资源迁移有以下几种方案:① 基于初始资源配置模板yaml使用helm进行构建;② 基于velero实现资源的备份和恢复;③ 本实践直接基于kubectl实现k8s资源迁移,确保k8s 1.12-1.21版本范围内的资源模板完全兼容,不再引入其他第三方开源软件。

1 环境

    源K8s版本:1.12

    目标K8s版本:1.18.8

2 资源导出

2.1 依赖工具yq

    和JSON文本操纵工具jq类似,yaml也有一个轻量级操纵工具yq,利用该工具对yaml进行格式化、字段筛选过滤较方便。这里使用的yq版本:yq-3.4.0-1.el7.x86_64。

    在资源导出前,检查yq是否已安装:

# Check if yq installed

if [ -z "`which yq 2>/dev/null`" ]; then

  echo "[Info] You need to install yq first!"

  exit 0

fi

2.2 导出前准备

    本实践将整个导出过程封装为shell,入参为namespace,即导出的最小单位,在导出前需要① 检查入参;② 是否已有导出的目录,需要确认避免覆盖已经更改的yaml;③ 根据namespace创建导出目录;

【注意】yaml文件名规范为:<资源类型缩写>-<资源名称>.yaml,例如:cm-application.yaml。

# 1. Check input parameter

if [ -z "$1" ]; then

  echo "[Info] You need to specify a namespace!"

  exit 0

fi

# Set the namespace to local variable

ns=$1

# 2. check if the export folder exists

if [ -d "/root/migrate/$ns" ]; then

read -r -p "[Warn] This folder will be overwritten! Are You Sure? [Y/n] " input

case $input in

    [yY][eE][sS]|[yY])

    ;;


    [nN][oO]|[nN])

    exit 1

    ;;


    *)

    echo "Invalid input..."

    exit 1

    ;;

esac

fi

# 3. create the export folder

if [ ! -z "`kubectl get ns $ns 2>/dev/null`" ]; then

  mkdir -p /root/migrate/$ns

else

  echo 'Namespace '$ns' does not exist!'

  exit 0

fi

2.3 导出命名空间模板 namespace

    这里使用yq删除了所有不需要的字段,删除不存在的字段也不会报错。

kubectl get ns $ns -o yaml | yq d - metadata.annotations | yq d - metadata.creationTimestamp | yq d - metadata.managedFields | yq d - metadata.resourceVersion | yq d - metadata.selfLink | yq d - status | yq d - metadata.uid | yq d - metadata.finalizers > /root/migrate/$ns/ns-$ns.yaml

2.4 导出部署模板 deployment

    这里的逻辑是,对于当前命名空间下所有的deploy分别进行导出,注意:① 这里对api版本号进行了修改,以支持版本较高的K8s;② 删除了无效的标签(根据情况需要自行修改或保持不变),在删除标签后,若metadata.labels下无其他内容,则将metadata.labels整体删除;③ 因新老PaaS平台的主机环境不同,因此需要删除无效的节点选择器;

for deploy in `kubectl get deploy -n $ns -o custom-columns=NAME:.metadata.name | grep -v '^NAME$'`; do

  kubectl get deploy $deploy -n $ns -o yaml | yq d - metadata.annotations | yq d - metadata.creationTimestamp | yq d - metadata.managedFields | yq d - metadata.resourceVersion | yq d - generation | yq d - spec.revisionHistoryLimit | yq d - status | yq d - spec.template.metadata.annotations | yq d - metadata.selfLink | yq d - spec.template.metadata.creationTimestamp | yq d - metadata.uid | yq d - metadata.generation | yq w - apiVersion 'apps/v1' | yq d - metadata.labels.appname | yq d - metadata.labels.apptype | yq d - spec.template.metadata.labels.deploy-version | yq d - spec.template.spec.containers[0].resources.requests | yq d - spec.template.spec.nodeSelector.group-$ns | yq d - spec.template.spec.nodeSelector['kubernetes.io/role'] > /root/migrate/$ns/deploy-$deploy.yaml

  if [ ! -z "`yq r /root/migrate/$ns/deploy-$deploy.yaml spec.template.spec.nodeSelector`" ] && [ `yq r /root/migrate/$ns/deploy-$deploy.yaml spec.template.spec.nodeSelector -l` -eq 0 ]; then

    yq d -i /root/migrate/$ns/deploy-$deploy.yaml spec.template.spec.nodeSelector

  fi

  if [ ! -z "`yq r /root/migrate/$ns/deploy-$deploy.yaml metadata.labels`" ] && [ `yq r /root/migrate/$ns/deploy-$deploy.yaml metadata.labels -l` -eq 0 ]; then

    yq d -i /root/migrate/$ns/deploy-$deploy.yaml metadata.labels

  fi

done

2.5 导出配置模板 configmap

    这里需要注意的:① kube-root-ca.crt为平台生成,在新环境中不再使用,需要过滤掉;② 通常参数配置在configmap的data下,为内嵌yaml,该内嵌yaml格式可能换行符、制表符等都进行了转义,为了便于导出后的对比、修改和合并,我们将此内嵌yaml进一步展开导出为app-.yaml。

for cm in `kubectl get cm -n $ns -o custom-columns=NAME:.metadata.name | grep -v '^NAME$' | grep -v '^kube-root-ca.crt'`; do

  kubectl get cm $cm -n $ns -o yaml | yq d - metadata.annotations | yq d - metadata.creationTimestamp | yq d - metadata.resourceVersion | yq d - metadata.managedFields | yq d - status | yq d - metadata.uid | yq d - metadata.selfLink > /root/migrate/$ns/cm-$cm.yaml

  key='application.yml'

  if [ ! -z "`yq r /root/migrate/$ns/cm-$cm.yaml data[$key]`" ]; then

    yq r /root/migrate/$ns/cm-$cm.yaml data[$key] > /root/migrate/$ns/app-$cm.yaml

  fi

  key='application-prod.yml'

  if [ ! -z "`yq r /root/migrate/$ns/cm-$cm.yaml data[$key]`" ]; then

    yq r /root/migrate/$ns/cm-$cm.yaml data[$key] > /root/migrate/$ns/app-$cm.yaml

  fi

  key='filebeat.yml'

  if [ ! -z "`yq r /root/migrate/$ns/cm-$cm.yaml data[$key]`" ]; then

    yq r /root/migrate/$ns/cm-$cm.yaml data[$key] > /root/migrate/$ns/app-$cm.yaml

  fi

done

2.6 导出持久卷和持久卷声明 pv & pvc

    因PaaS上可能存在pvc没有对应的pv的情况,会造成导出空pv模板,因此需要进行判断,避免导出无效pv;另外,这里只导出当前namespace下的pvc相关联的pv。

# 导出pv

for pv in `kubectl get pvc -n $ns -o custom-columns=VOLUME:.spec.volumeName | grep -vE '^VOLUME$|^'`; do

  if [ ! -z "`kubectl get pv $pv 2>/dev/null`" ]; then

    kubectl get pv $pv -o yaml | yq d - metadata.creationTimestamp | yq d - metadata.resourceVersion | yq d - status | yq d - spec.claimRef | yq d - metadata.uid | yq d - metadata.selfLink | yq d - metadata.managedFields | yq d - metadata.annotations['kubectl.kubernetes.io/last-applied-configuration'] | yq d - spec.volumeMode > /root/migrate/$ns/pv-$pv.yaml

  fi

done

# 导出pvc

for pvc in `kubectl get pvc -n $ns -o custom-columns=NAME:.metadata.name | grep -v '^NAME$'`; do

  pv=`kubectl get pvc $pvc -n $ns -o custom-columns=VOLUME:.spec.volumeName | grep -vE '^VOLUME$|^' 2>/dev/null`

  if [ -z $pv ]; then

    continue

  fi

  if [ ! -z "`kubectl get pv $pv 2>/dev/null`" ]; then

    kubectl get pvc $pvc -n $ns -o yaml | yq d - metadata.creationTimestamp | yq d - metadata.resourceVersion | yq d - status | yq d - metadata.uid | yq d - metadata.selfLink | yq d - metadata.managedFields | yq d - metadata.annotations['kubectl.kubernetes.io/last-applied-configuration'] | yq d - spec.volumeMode > /root/migrate/$ns/pvc-$pvc.yaml

  fi

done

2.7 导出状态集 statefulsets

for sts in `kubectl get sts -n $ns -o custom-columns=NAME:.metadata.name | grep -v '^NAME$'`; do

  kubectl get sts $sts -n $ns -o yaml | yq d - metadata.creationTimestamp | yq d - metadata.generation | yq d - metadata.resourceVersion | yq d - metadata.uid | yq d - metadata.managedFields | yq d - spec.volumeClaimTemplates[0].status | yq d - status | yq d - metadata.annotations > /root/migrate/$ns/sts-$sts.yaml

done

2.8 导出服务名 service

    服务名是比较重要,且常出问题的环节,这里需要注意的是:

1、对于1.19以下的K8s,我们用k8s_low_ver参数区分,这些版本不支持clusterIPs参数;

2、对无头服务(headless service),需要特殊处理,即判断spec.clusterIP是否为None,若为None,在高版本的K8s中需要设置spec.clusterIP为None且clusterIPs[0]为None,低版本仅设置spec.clusterIP为None;

3、通常,service使用selector去定位相应的pod,无需单独导出服务端点endpoints;若没有指定selector,则需要单独创建endpoints,这个功能比较适用于外置服务的情况,即service解析为一个通用外部地址,便于新老平台都指向相同的外部地址,屏蔽差异;

k8s_low_ver="yes"

for svc in `kubectl get svc -n $ns -o custom-columns=NAME:.metadata.name | grep -v "^NAME"`; do

  kubectl get svc $svc -n $ns -o yaml | yq d - metadata.annotations | yq d - metadata.selfLink | yq d - metadata.creationTimestamp | yq d - metadata.resourceVersion | yq d - metadata.uid | yq d - metadata.managedFields | yq d - spec.clusterIPs | yq d - status | yq d - metadata.labels.appname | yq d - metadata.labels.apptype > /root/migrate/$ns/svc-$svc.yaml

  if [ "`yq r /root/migrate/$ns/svc-$svc.yaml spec.clusterIP`" == "None" ] ; then

  if [ "$k8s_low_ver" == "yes" ]; then

    yq d -i /root/migrate/$ns/svc-$svc.yaml spec.clusterIPs

  else

      yq w -i /root/migrate/$ns/svc-$svc.yaml spec.clusterIPs[0] None

    fi

  else

    yq d -i /root/migrate/$ns/svc-$svc.yaml spec.clusterIP

  fi

  if [ -z "`yq r /root/migrate/$ns/svc-$svc.yaml spec.selector`" ] ; then

    kubectl get endpoints $svc -n $ns -o yaml | yq d - metadata.annotations | yq d - metadata.creationTimestamp | yq d - metadata.managedFields | yq d - metadata.resourceVersion | yq d - metadata.selfLink | yq d - metadata.uid > /root/migrate/$ns/endpoints-$svc.yaml

  fi

  if [ ! -z "`yq r /root/migrate/$ns/svc-$svc.yaml metadata.labels`" ] && [ `yq r /root/migrate/$ns/svc-$svc.yaml metadata.labels -l` -eq 0 ]; then

    yq d -i /root/migrate/$ns/svc-$svc.yaml metadata.labels

  fi

done

2.9 导出其他资源

    导出如limits、secrets、daemonsets等资源,目前尚不支持对rbac的配置导出,正在完善中。

# Export limits' yaml

for limits in `kubectl get limits -n $ns -o custom-columns=NAME:.metadata.name | grep -v '^NAME$'`; do

  kubectl get limits $limits -n $ns -o yaml | yq d - metadata.annotations | yq d - metadata.creationTimestamp | yq d - metadata.resourceVersion | yq d - metadata.uid | yq d - metadata.managedFields | yq d - metadata.selfLink > /root/migrate/$ns/limits-$limits.yaml

done

# Export secrets' yaml

for secrets in `kubectl get secrets -n $ns --field-selector=metadata.name!='istio.default' -o custom-columns=NAME:.metadata.name | grep -vE '^NAME$|^default-token-|^istio'`; do

  kubectl get secrets $secrets -n $ns -o yaml | yq d - metadata.annotations | yq d - metadata.creationTimestamp | yq d - metadata.resourceVersion | yq d - metadata.uid | yq d - metadata.managedFields | yq d - metadata.selfLink > /root/migrate/$ns/secrets-$secrets.yaml

done

# Export daemonsets' yaml

for ds in `kubectl get ds -n $ns -o custom-columns=NAME:.metadata.name | grep -v '^NAME$'`; do

  kubectl get ds $ds -n $ns -o yaml | yq w - apiVersion 'apps/v1' | yq d - metadata.creationTimestamp | yq d - generation | yq d - metadata.resourceVersion | yq d - metadata.uid | yq d - status > /root/migrate/$ns/ds-$ds.yaml

done

3 资源导入/对比

3.1 资源的转换

    我们力争新老平台的资源0差异率,但因环境差异,一些配置无法完全一致,如:service的externalIP(和node ip有相关性);一些PaaS外部资源,如共享的MySQL数据库存储,MySQL实例存在于K8s内部,相当于两套K8s的MySQL实例指向同一个数据存储,是不支持的,存储必须分开,这样pv的指向就产生了差异。

既然有差异,就需要进行转换,如,在新平台执行下列转换:

yq w -i svc-nginx-on-k8s-external.yaml spec.externalIPs[0] "192.195.1.10"

yq w -i svc-nginx-on-k8s-external.yaml spec.externalIPs[1] "192.195.1.11"

yq w -i svc-nginx-on-k8s-external.yaml spec.externalIPs[2] "192.195.1.12"

3.2 配置模板的合并

# 内嵌yaml的名称

sub_yml="application.yml"

# 对于当前命名空间下所有内嵌yaml

for i in `ls app-*.yaml`; do

# 这里列举了对kafka配置的转换,因为,若两个PaaS平台同时消费Kafka,在业务上会造成订单的抢单处理,若新平台业务尚未就绪,则会造成订单处理失败

# 这里将kafka配置为服务名

if [ ! -z "`yq r $i spring.kafka.bootstrap-servers`" ]; then

yq w -i $i spring.kafka.bootstrap-servers "kafka.platform.svc.cluster.local:9092"

fi

# 根据内嵌yaml文件名找到对应的configmap文件名

j=`echo $i | sed 's/^app-/cm-/g'`

# 将内嵌yaml写入configmap,通过该处理,configmap的格式也发生变化,可读性增强

yq w -i $j data[$sub_yml] "`cat $i`"

done

3.3 资源的全量对比

全量对比,查缺补漏,这一步是降低新老PaaS资源模板差异率的重要环节,一般用beyond compare可视化对比效果较好,这里不再赘述。自动化差异对比:

for i in `ls *.yaml`; do

chkVal1=`md5sum $i | awk '{print $1}'`

chkVal2=`md5sum ../paas2/$i | awk '{print $1}'`

if [ "$chkVal1" != "$chkVal2" ]; then

echo $i

fi

done

3.4 资源的导入

    因资源之间千丝万缕的依赖关系,不主张kubectl create -f .的方式创建,建议按照ns-pv-pvc-deploy-svc的顺序依次创建,因顺序错误造成pods无法正常启动,或需要删除后重建。

4 总结

    本实践主要讲述了一个K8s资源迁移的大致流程,脚本已在第一节所述的环境中测试通过,对于表述不清晰的地方烦请各位网友批评指正。

你可能感兴趣的:(基于kubectl的k8s强一致性资源迁移实践)