公司运行了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-<cm name>.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$|^<none>'`; 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$|^<none>' 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资源迁移的大致流程,脚本已在第一节所述的环境中测试通过,对于表述不清晰的地方烦请各位网友批评指正。