1 - K8S 版本升级说明

K8S 版本升级说明

TKEStack提供升级 Kubernetes 版本的功能,您可通过此功能对运行中的 Kubernetes 集群进行升级。在升级 kubernetes 版本之前,建议您查阅 CHANGELOG 确认Kubernetes 版本差异。

Concept

主版本:k8s版本号的格式vx.y.z中的x为主版本,例如v1.18.3主版本是1

次要版本:k8s版本号的格式vx.y.z中的y为次要版本,例如v1.18.3的次要版本是18

补丁版本:k8s版本号的格式vx.y.z中的z为补丁版本

升级须知

1.升级属于不可逆操作、请谨慎进行。

2.请在升级集群前,查看集群下业务状态是否均为健康状态。

3.升级期间不建议对集群进行任何操作。

4.仅支持次要版本和补丁版本升级,不支持降级,不支持跨多个版本升级(例如1.16跳过1.17直接升级至1.18),且仅当集群内 Master 版本和 Node 版本一致时才可继续升级下一个版本。

升级技术原理

升级的过程为:升级包准备、升级 Master 和升级 Node。

1.升级包准备

当前运行集群所需要的镜像二进制等都保存在tke-installer中,已经安装好的TKEStack已包含至少三个连续的 K8S 版本,如TKEStack v1.5.0 中包含K8S v1.16.9, v1.17.13 和 v1.18.3,同时也包含了对应的kubeadm,kubelet和kubectl版本。

2.升级Master

Master升级采用滚动升级的方式,同一时间只会对一个节点进行升级,只有当前节点升级成功才会进行下个节点的升级。

Master升级调用kubeadm 处理集群的升级操作,Kubeadm 工作原理参考:how it works

3.升级Node

节点原地升级,采用滚动升级的方式,即同一时间只会对一个节点进行升级,只有当前节点升级成功后才会进行下个节点的升级。每个节点升级时执行以下操作:

  • 替换和重启节点上的 kubelet组件。
  • 从集群取回 kubeadm ClusterConfiguration。
  • 为本节点升级 kubelet 配置

操作步骤

1.登录 平台管理 控制台,选择左侧导航栏中的【集群管理】。

2.在“集群管理”页面,选择需要升级的集群,点击右侧的【升级Master】按钮,进入集群升级页面。

3.在集群升级页面配置升级所需参数,如下图所示:

4.在点击提交按钮,等待升级完成。同时可以点击【状态】按钮查看升级状态。

5.如果上述配置没有选择【自动升级worker】,在Master节点升级完成后,点击【升级worker】,进入node升级页面。

6.在node升级页面配置node节点升级所需参数,如下图所示:

7.在点击提交按钮,等待升级完成。同时可以点击【状态】按钮查看当前升级状态。

###其他技术细节

1.升级中任何步骤出现错误,系统都将自动进行重试,用户可在升级进度界面中得到错误信息。

风险点与规避措施

Master节点升级存在一定风险,用户在升级前,应检查集群状态是否足够健康,从而判断是否开始master节点升级,本节列举一些较为典型的风险及规避方法供用户参考。

重启kubelet的风险点

1.Not Ready Pod 数目超过设置值导致升级卡死

选择了【驱逐节点】选项,同时节点过少,而设置【最大不可用pod数】比例过低,没有足够多的节点承载pod的迁移会导致升级卡死。

规避措施:

  • 尽如果业务对pod可用比例较高,请考虑选择升级前不驱逐节点。

2.kubelet进程无法启动

当该master节点资源十分紧张的时候,启动kubelet有可能失败,从而导致节点升级无法正常完成。

规避措施:

  • 尽量不要将业务Pod运行于master节点
  • 确保节点资源不能处于高度紧张状态

3.容器重启

某些版本kubelet与旧版本对于container的管理有一定差异,有可能导致节点上所有container重启

规避措施:

  • 加强container的重启容忍度,例如确保deployment的副本数不为1

FQA

1.何时使用K8S升级功能:

答:当集群版本不满足业务需求,K8S漏洞修复,或当前集群版本低于TKEStack所能支持的最小版本。

2.升级的目标版本能否选择:

答:目前只支持升级到下一个次要版本,例如1.16.x的集群只能升到1.17.x;或者升级到补丁版本,例如1.16.x升级到 1.16.z

3.为什么我看不到升级worker按钮:

答:只有当前集群所有worker节点版本与master节点版本相同时,才允许进行master版本升级。

4.是否支持回滚:

答:不支持回滚操作。

5.自行修改的参数怎么办

答:master升级将会把用户自行修改的参数重置成与新建集群时的一致,若用户有特殊参数修改,建议升级完成后手动添加。

6.升级时出现异常情况如何处理?

答:升级过程中有可能出现意想不到的问题而导致升级失败或升级过程被卡住。针对失败发生的时间点不同,管理员处理策略有所不同。

  • 失败发生在k8s集群版本号变化之前:

此时k8集群版本号未发生变化,首节点尚未升级,可回滚。可以将Cluster.Spec.Version的版本号修改为与当前k8s版本一致,集群便可恢复正常运行状态。

  • 失败发生在k8s集群版本号变化之后:

此时集群的首节点已经升级成功,原则上不允许回滚到低版本。需要管理员排查其他节点没有按照预期进行升级到原因,解决问题后升级流程会自动向前推进。

7.Not Ready Pod数目超过设置值导致升级卡死

答:升级时遇到Not Ready Pod数目超过设置值导致升级卡死的情况,检查是否是由于驱逐导致的Not Ready状态,可尝试修改cluster.spec.features.upgrade.strategy. drainNodeBeforeUpgrade,设置false不驱逐节点, 或调大 maxUnready 值,以允许容忍更多的Not Ready Pod。

案例分析

升级时间点选择不当

  • 案例详情:集群 A 的运维人员在工作日业务高峰期进行节点升级操作,由于没有配置合理的扩缩容机制,业务 Pod 一直处于高负载状态,某个节点升级失败导致该节点 Pod 重启,剩余 Pod 无法满足高峰期时段的业务负载导致出现大量访问异常,业务受到较为严重的影响。
  • 最佳实践:选择业务低峰期进行节点升级将大大降低业务受影响的概率。

成功案例

  • 案例详情:集群 D 计划进行节点升级。运维人员决定先升级测试集群,成功后再升级生产集群,并将升级时间选在业务负载较低的周五凌晨2点。升级前,他通过监控面板观察到各目标节点的负载情况都较低,再通过节点详情页观察每个节点上 Pod 分布较为合理。第一个节点开始升级后,运维人员点击了暂停升级任务,随后第一个节点升级完成后任务到达暂停点自动转为暂停状态。运维人员再次检查被升级节点,确认无异常后点击继续任务,升级动作继续进行。在随后升级过程中,运维人员时刻观察集群和业务情况,直到升级全部完成。成功升级测试集群后,运维人员继续以同样的步骤成功升级生产集群。
  • 最佳实践:1.先升级测试集群或者开发集群,成功后再升级生产集群;2. 时间选择合理;3. 升级前检查集群状态;4.先升级少量节点观察并解决问题;5.升级过程中继续保持观察。

2 - 自定义k8s版本升级

自定义k8s版本升级

用户可以通过向TKEStack平台提供自定义版本的k8s,以允许集群升级到非内置的版本。本文将以v1.16.15版本的k8s作为例子演示用户如何将集群升级到自定义版本。本文中只以amd64环境作为示例,如果用户希望自己的物料镜像可以支持multi-CPU architecture,请在制作镜像和推送镜像阶段参考Leverage multi-CPU architecture support构建多CPU架构支持的Docker镜像

制作provider-res镜像

provider镜像用于存储kubeadm、kubelet和kubectl的二进制文件。

执行下面命令为环境设置好版本号,并从官方下载好二进制文件并压缩,若遇到网络问题请通过其他途径下载对应二进制文件:

export RELEASE=v1.16.15 && \
curl -L --remote-name-all https://storage.googleapis.com/kubernetes-release/release/$RELEASE/bin/linux/amd64/{kubeadm,kubelet,kubectl} && \
chmod +x kubeadm kubectl kubelet && \
mkdir -p kubernetes/node/bin/ && \
cp kubelet kubectl kubernetes/node/bin/ && \
tar -czvf kubeadm-linux-amd64-$RELEASE.tar.gz kubeadm && \
tar -czvf kubernetes-node-linux-amd64-$RELEASE.tar.gz kubernete

执行下面命令生成dockerfiel:

cat << EOF >Dockerfile
FROM tkestack/provider-res:v1.18.3-2

WORKDIR /data

COPY kubernetes-*.tar.gz   res/linux-amd64/
COPY kubeadm-*.tar.gz      res/linux-amd64/

ENTRYPOINT ["sh"]
EOF

制作provider-res镜像:

docker build -t registry.tke.com/library/provider-res:myversion .

此处使用了默认的registry.tke.com作为registry的domian,如未使用默认的domain请修改为自定义的domain,下文中如遇到registry.tke.com也做相同处理。

为平台准备必要镜像

从官方下载k8s组件镜像,如遇到网络问题请通过其他途径下载:

docker pull k8s.gcr.io/kube-scheduler:$RELEASE && \
docker pull k8s.gcr.io/kube-controller-manager:$RELEASE && \
docker pull k8s.gcr.io/kube-apiserver:$RELEASE && \
docker pull k8s.gcr.io/kube-proxy:$RELEAS

重新为镜像为镜像打标签:

docker tag k8s.gcr.io/kube-proxy:$RELEASE registry.tke.com/library/kube-proxy:$RELEASE && \
docker tag k8s.gcr.io/kube-apiserver:$RELEASE registry.tke.com/library/kube-apiserver:$RELEASE && \
docker tag k8s.gcr.io/kube-controller-manager:$RELEASE registry.tke.com/library/kube-controller-manager:$RELEASE && \
docker tag k8s.gcr.io/kube-scheduler:$RELEASE registry.tke.com/library/kube-scheduler:$RELEASE

导出镜像:

docker save -o kube-proxy.tar registry.tke.com/library/kube-proxy:$RELEASE && \
docker save -o kube-apiserver.tar registry.tke.com/library/kube-apiserver:$RELEASE && \
docker save -o kube-controller-manager.tar registry.tke.com/library/kube-controller-manager:$RELEASE && \
docker save -o kube-scheduler.tar registry.tke.com/library/kube-scheduler:$RELEASE && \
docker save -o provider-res.tar registry.tke.com/library/provider-res:myversion

发送到global集群节点上:

scp kube*.tar provider-res.tar root@your_global_node:/root/

在global集群上导入物料

注意在此之后执行到命令都是发生在global集群节点上,为了方便首先在环境中设置版本号:

加载镜像:

docker load -i kube-apiserver.tar && \
docker load -i kube-controller-manager.tar && \
docker load -i kube-proxy.tar && \
docker load -i kube-scheduler.tar && \
docker load -i provider-res.tar

登陆registry:

docker login registry.tke.co

此处会提示输入用户名密码,如果默认使用了内置registry,用户名密码为admin的用户名密码,如果配置了第三方镜像仓库,请使用第三方镜像仓库的用户名密码。

登陆成功后推送镜像到registry:

docker push registry.tke.com/library/kube-apiserver:$RELEASE && \
docker push registry.tke.com/library/kube-controller-manager:$RELEASE && \
docker push registry.tke.com/library/kube-proxy:$RELEASE && \
docker push registry.tke.com/library/kube-scheduler:$RELEASE && \
docker push registry.tke.com/library/provider-res:myversion

为使得导入物料可以被平台使用,首先需要修改tke-platform-controller的deployment:

kubectl edit -n tke deployments tke-platform-controller

修改spec.template.spec.initContainers[0].image中的内容为刚刚制作的provider-res镜像registry.tke.com/library/provider-res:myversion

其次需要修改cluster-info:

kubectl edit -n kube-public configmaps cluster-info

在data.k8sValidVersions内容中添加"1.16.15"

升级集群到自定义版本

触发集群升级需要在global集群上修改cluster资源对象内容:

kubectl edit cluster cls-yourcluster

修改spec.version中的内容为1.16.15

更详细的升级相关文档请参考:K8S 版本升级说明

目前Web UI不允许补丁版本升级,会导致可以在UI升级选项中可以看到1.16.15版本,但是提示无法升级到该版本,后续版本中将会修复。当前请使用kubectl�修改cluster资源对象内容升级自定义版本。

3 - wx 私有化部署最佳实践

wx 私有化部署最佳实践

[TOC]

wx tkestack 私有化部署最佳实践

背景

随着私有化项目越来越多,简单快捷部署交付需求日益强烈。在开源协同大行其道,私有化一键部署如何合理利用开源协同力量做到用好80%,做好20% 值得思而深行而简。本文将揭秘私有化一键部署结合tkestack 实现私有化一键部署最佳实践面纱。

方案选型

虽然开源协同大行其道,但合适才是最好基本原则仍然需要贯彻执行 – 量体裁衣;结合开发难度,开发效率,后期代码维护,组件维护,实施人员使用难度,实施人员现场修改难度等多维度进行考量。方案如下:

  • kubeadm+ansible: **优点:**对kubernetes版本,网络插件,docker 版本自主可控; **缺点:**维护成本高,特别kubernetes升级,,无法专注于业务层面的一键部署;
  • tkestack+ansible: **优点:**对于kubernetes的集成,维护无需关心,只需要用好tkestack也就掌握tkestack的基本原理做好应急;专注于业务层面的一键部署集成即可。通过ansible进行机器批量初始化,部署方便快捷对于实施人员要求低,可以随时现场修改; 缺点: kubernetes 版本,网络插件,docker 版本不自主可控;
  • tkestack+operator: **优点:**私有化一键部署产品化,平台化; **缺点:**operator开发成本高,客户环境多变复杂,出现问题没法现场修改;

综合上述方案考虑维护kubernetes成本有点高,另外tkestack+ansible通过hooks方式进行扩展,能实现快速集成,可以专注于业务组件集成即可;ansible 入手容易,降低实施人员学习成本,并且可以随时根据现场环境随时修改适配;综合考量选择tkestack+ansible 模式。

需求

功能性需求:

功能说明
主机初始化安装前进行主机初始化,比如添加域名hosts,安装压测工具,离线yum源等
主机检查检查当前主机的性能是否符合需求,磁盘大小是否符合需求,操作系统版本,内核版本,性能压测等
tkestack部署部署kuberntes和tkestack
业务依赖组件部署部署业务依赖组件,比如redis,mysql,部署运维组件elk,prometheus等
业务部署部署业务服务

非功能性需求:

功能说明
解耦针对一些已有kubernetes/tke 平台,此时需要只部署业务依赖组件及业务,所以需要和tkestack解耦
扩展性业务依赖组件不同项目需要采用不同的依赖组件,需要快捷集成新的组件
幂等部署及卸载时可以重复执行

实现

1. 走进tkesack

从tkestack git 获取到的架构图可以看出tkestack分为installer, Global,cluster这三种角色;其中installer 负责tkestack Global集群的安装,当前提供命令行安装模式和图形化安装模式;cluster 角色是作为业务集群,通Global集群纳管。当前我们只需要部署一个Global集群作为业务集群即可满足需求,cluster集群只是为了提供给客户使用tkestack多集群管理使用。

tkestack 在installer 以hooks 方式实现用户自定义扩展, 有如下hook脚本:

  • pre-installer: 主要集群部署前的一些自定义初始化操作
  • post-cluster-ready: 种子集群ready后针对tkestack 部署前的初始化操作
  • post-install: tkestack 部署完毕,部署自定义扩展

默认tkestack部署流程如下:

由于installer 节点在tkestack 设计上计划安装完毕直接废弃,所以tkestack 会在global集群重新部署一个镜像仓库作为后续业务使用,当然也会将tkestack 平台的镜像重新repush到集群内的镜像仓库。所以容器化的自定义扩展主件的部署需要放到post-install 脚本进行触发。

2. 魔改部署配置

  • tkestack git 使用手册给出了两种部署模式一种是web页面配置模式,一种是命令行模式;经过使用发现tkestack有个亮点特性就是配置文件记录了一个step 安装步骤,可以在安装失败后解决问失败原因直接重启tke-installer 即可根据当前step 步骤继续进行安装部署;我们利用这特性实现web页面配置模式也可以命令行模式部署。具体操作是先通过页面配置得到配置文件,把配置文件做成模板; 部署时候通过ansible templet 模块进行渲染。 当前抽取出来配置模板有:
  • tke-ha-lb.json.j2 对应web页面的使用已有,也就是采用负载均衡ip地址作为tkestack集群高可用
  • tke-ha-keepalived.json.j2 对应web页面的TKE提供,采用vip通过keepalived 浮动漂移实现高可用
  • tke-sigle.json.j2 对应web页面的不设置场景, 也就是单master版场景 以下以tke-ha-lb.json.j2 为例:
{
"config": {
"ServerName": "tke-installer",
"ListenAddr": ":8080",
"NoUI": false,
"Config": "conf/tke.json",
"Force": false,
"SyncProjectsWithNamespaces": false,
"Replicas": {{ tke_replicas }}
},
"para": {
"cluster": {
 "kind": "Cluster",
 "apiVersion": "platform.tkestack.io/v1",
 "metadata": {
  "name": "global"
 },
 "spec": {
  "finalizers": [
   "cluster"
  ],
  "tenantID": "default",
  "displayName": "TKE",
  "type": "Baremetal",
  "version": "{{ k8s_version }}",
  "networkDevice": "{{ net_interface }}",
  "clusterCIDR": "{{ cluster_cidr }}",
  "dnsDomain": "cluster.local",
  "features": {
   "ipvs": {{ ipvs }},
   "enableMasterSchedule": true,
   "ha": {
    "thirdParty": {
     "vip": "{{ tke_vip }}",
     "vport": {{ tke_vport}}
    }
   }
  },
  "properties": {
   "maxClusterServiceNum": {{ max_cluster_service_num }},
   "maxNodePodNum": {{ max_node_pod_num }}
  },
  "machines": [
    {
     "ip": "{{ groups['masters'][0] }}",
     "port": {{ ansible_port }},
     "username": "{{ ansible_ssh_user }}",
     "password": "{{ ansible_ssh_pass_base64 }}"
    },
    {
     "ip": "{{ groups['masters'][1] }}",
     "port": {{ ansible_port }},
     "username": "{{ ansible_ssh_user }}",
     "password": "{{ ansible_ssh_pass_base64 }}"
    },
    {
      "ip": "{{ groups['masters'][2] }}",
      "port": {{ ansible_port }},
      "username": "{{ ansible_ssh_user }}",
      "password": "{{ ansible_ssh_pass_base64 }}"
    }
  ],
  "dockerExtraArgs": {
   "data-root": "{{ docker_data_root }}"
  },
  "kubeletExtraArgs": {
   "root-dir": "{{ kubelet_root_dir }}"
  },
  "apiServerExtraArgs": {
   "runtime-config": "apps/v1beta1=true,apps/v1beta2=true,extensions/v1beta1/daemonsets=true,extensions/v1beta1/deployments=true,extensions/v1beta1/replicasets=true,extensions/v1beta1/networkpolicies=true,extensions/v1beta1/podsecuritypolicies=true"
  }
 }
},
"Config": {
 "basic": {
  "username": "{{ tke_admin_user }}",
  "password": "{{ tke_pwd_base64 }}"
 },
 "auth": {
  "tke": {
   "tenantID": "default",
   "username": "{{ tke_admin_user }}",
   "password": "{{ tke_pwd_base64 }}"
  }
 },
 "registry": {
  "tke": {
   "domain": "{{ tke_registry_domain }}",
   "namespace": "library",
   "username": "{{ tke_admin_user }}",
   "password": "{{ tke_pwd_base64 }}"
  }
 },
 "business": {},
 "monitor": {
  "influxDB": {
   "local": {}
  }
 },
 "ha": {
  "thirdParty": {
    "vip": "{{ tke_vip }}",
    "vport": {{ tke_vport}}
  }
 },
 "gateway": {
  "domain": "{{ tke_console_domain }}",
  "cert": {
   "selfSigned": {}
  }
 }
}
},
"cluster": {
"kind": "Cluster",
"apiVersion": "platform.tkestack.io/v1",
"metadata": {
 "name": "global"
},
"spec": {
 "finalizers": [
  "cluster"
 ],
 "tenantID": "default",
 "displayName": "TKE",
 "type": "Baremetal",
 "version": "{{ k8s_version }}",
 "networkDevice": "{{ net_interface }}",
 "clusterCIDR": "{{ cluster_cidr }}",
 "dnsDomain": "cluster.local",
 "features": {
  "ipvs": {{ ipvs }},
  "enableMasterSchedule": true,
  "ha": {
   "thirdParty": {
     "vip": "{{ tke_vip }}",
     "vport": {{ tke_vport}}
   }
  }
 },
 "properties": {
  "maxClusterServiceNum": {{ max_cluster_service_num }},
  "maxNodePodNum": {{ max_node_pod_num }}
 },
 "machines": [
   {
     "ip": "{{ groups['masters'][0] }}",
     "port": {{ ansible_port }},
     "username": "{{ ansible_ssh_user }}",
     "password": "{{ ansible_ssh_pass_base64 }}"
   },
   {
     "ip": "{{ groups['masters'][1] }}",
     "port": {{ ansible_port }},
     "username": "{{ ansible_ssh_user }}",
     "password": "{{ ansible_ssh_pass_base64 }}"
   },
   {
     "ip": "{{ groups['masters'][2] }}",
     "port": {{ ansible_port }},
     "username": "{{ ansible_ssh_user }}",
     "password": "{{ ansible_ssh_pass_base64 }}"
   }
 ],
 "dockerExtraArgs": {
  "data-root": "{{ docker_data_root }}"
 },
 "kubeletExtraArgs": {
  "root-dir": "{{ kubelet_root_dir }}"
 },
 "apiServerExtraArgs": {
  "runtime-config": "apps/v1beta1=true,apps/v1beta2=true,extensions/v1beta1/daemonsets=true,extensions/v1beta1/deployments=true,extensions/v1beta1/replicasets=true,extensions/v1beta1/networkpolicies=true,extensions/v1beta1/podsecuritypolicies=true"
 }
}
},
"step": 0 # 重启tke-installer 后会按此步骤执行继续的安装,当前设置为0意味着从零开始
}

为了实现此方式安装,我们的安装脚本如下:

#!/bin/bash
# Author: yhchen
set -e

BASE_DIR=$(cd `dirname $0` && pwd)
cd $BASE_DIR

# get offline-pot parent dir
OFFLINE_POT_PDIR=`echo ${BASE_DIR} | awk -Foffline-pot '{print $1}'`

INSTALL_DIR=/opt/tke-installer
DATA_DIR=${INSTALL_DIR}/data
HOOKS=${OFFLINE_POT_PDIR}offline-pot
IMAGES_DIR="${OFFLINE_POT_PDIR}offline-pot-images"
TGZ_DIR="${OFFLINE_POT_PDIR}offline-pot-tgz"
REPORTS_DIR="${OFFLINE_POT_PDIR}perfor-reports"
version=v1.2.4

init_tke_installer(){
  if [ `docker images | grep tke-installer | grep ${version} | wc -l` -eq 0 ]; then
    if [ `docker ps -a | grep tke-installer | wc -l` -gt 0 ]; then
      docker rm -f tke-installer
    fi
    if [ `docker images | grep tke-installer | wc -l` -gt 0 ]; then
      docker rmi -f `docker images | grep tke-installer | awk '{print $3}'`
    fi 
    cd ${OFFLINE_POT_PDIR}tkestack
    if [ -d "${OFFLINE_POT_PDIR}tkestack/tke-installer-x86_64-${version}.run.tmp" ]; then
      rm -rf ${OFFLINE_POT_PDIR}tkestack/tke-installer-x86_64-${version}.run.tmp
    fi
    sha256sum --check --status tke-installer-x86_64-$version.run.sha256 && \
    chmod +x tke-installer-x86_64-$version.run && ./tke-installer-x86_64-$version.run
  fi
}

reinstall_tke_installer(){
  if [ -d "${REPORTS_DIR}" ]; then
    mkdir -p ${REPORTS_DIR}
  fi
  if [ `docker ps -a | grep tke-installer | wc -l` -eq 1 ]; then
    docker rm -f tke-installer
    rm -rf /opt/tke-installer/data
  fi
  docker run --restart=always --name tke-installer -d --privileged --net=host -v/etc/hosts:/app/hosts \
  -v/etc/docker:/etc/docker -v/var/run/docker.sock:/var/run/docker.sock -v$DATA_DIR:/app/data \
  -v$INSTALL_DIR/conf:/app/conf -v$HOOKS:/app/hooks -v$IMAGES_DIR:${IMAGES_DIR} -v${TGZ_DIR}:${TGZ_DIR} \
  -v${REPORTS_DIR}:${REPORTS_DIR} tkestack/tke-installer:$version
  if [ -f "hosts" ]; then
    # set hosts file's dpl_dir variable
    sed -i 's#^dpl_dir=.*#dpl_dir=\"'"${HOOKS}"'\"#g' hosts
    installer_ip=`cat hosts | grep -A 1 '\[installer\]' | grep -v installer`
    echo "please exec install-offline-pot.sh or access http://${installer_ip}:8080 to install offline-pot"
  fi
}

main(){
  init_tke_installer # 此函数是为了实现当前节点尚未安装过tke-installer, 进行第一次安装实现初始化
  reinstall_tke_installer # 此函数是实现自定义安装tke-installer, 主要是为了将扩展的hooks脚本挂载到tke-installer,以及hooks脚本调用到的整个一键部署脚本。
}
main

最终实现开始部署tkestack脚本如下:

#!/bin/bash
# Author: yhchen
set -e

BASE_DIR=$(cd `dirname $0` && pwd)
cd $BASE_DIR

CALL_FUN="defaut"

help(){
  echo "show usage:"
  echo "init_and_check: will be init hosts, inistall tke-installer and hosts check"
  echo "dpl_offline_pot: init tke config and deploy offline-pot"
  echo "init_keepalived: just tmp use, when tkestack fix keepalived issue will be remove"
  echo "only_install_tkestack: if you want only install tkestack, please -f parameter pass only_install_tkestack"
  echo "defualt: will be exec dpl_offline_pot and init_keepalived"
  echo "all_func: execute init_and_check, dpl_offline_pot, init_keepalived"
  exit 0
}

while getopts ":f:h:" opt
do
  case $opt in
    f)
    CALL_FUN="${OPTARG}"
    ;;
    h)
    hosts="${OPTARG}"
    ;;
    ?)
    echo "unkown args! just suport -f[call function] and -h[ansible hosts group] arg!!!"
    exit 0;;
  esac
done

INSTALL_DATA_DIR=/opt/tke-installer/data/

init_and_check(){
  sh ./init-and-check.sh
}

# init tke config and deploy offline-pot
dpl_offline_pot(){
  echo "###### deploy offline-pot start ######"
  if [ `docker ps | grep tke-installer | wc -l` -eq 1 ]; then
    # deploy tkestack , base commons and business
    sh ./offline-pot-cmd.sh -s init-tke-config.sh -f init
    docker restart tke-installer
    if [ -f "hosts" ]; then
      installer_ip=`cat hosts | grep -A 1 '\[installer\]' | grep -v installer`
      echo "please exec tail -f ${INSTALL_DATA_DIR}/tke.log or access http://${installer_ip}:8080 check install progress..."
    fi
  elif [ ! -d "../tkestack" ]; then
    # deploy base commons and business on other kubernetes plat
    sh ./post-install
  else
    echo "if first install,please exec init-and-check.sh script, else exec reinstall-offline-pot.sh script" && exit 0
  fi
  echo "###### deploy offline-pot end ######"
}

# just tmp use, when tkestack fix keepalived issue will be remove
init_keepalived(){
  echo "###### init keepalived start  ######"
  if [ -f "${INSTALL_DATA_DIR}/tke.json" ]; then
    if [ `cat ${INSTALL_DATA_DIR}/tke.json | grep -i '"ha"' | wc -l` -gt 0 ]; then
      nohup sh ./init_keepalived.sh 2>&1 > ${INSTALL_DATA_DIR}/dpl-keepalived.log &
    fi
  fi
  echo "###### init keepalived end ######"
}

# only install tkestack
only_install_tkestack(){
  echo "###### install tkestack start ######"
  # change tke components's replicas number
  if [ -f "hosts" ]; then 
    sed -i 's/tke_replicas="1"/tke_replicas="2"/g' hosts
  fi
  # hosts init
  if [ `docker ps | grep tke-installer | wc -l` -eq 1 ]; then
    sh ./offline-pot-cmd.sh -s host-init.sh -f sshd_init
    sh ./offline-pot-cmd.sh -s host-init.sh -f selinux_init
    sh ./offline-pot-cmd.sh -s host-init.sh -f remove_devnet_proxy
    sh ./offline-pot-cmd.sh -s host-init.sh -f add_domains
    sh ./offline-pot-cmd.sh -s host-init.sh -f data_disk_init
    sh ./offline-pot-cmd.sh -s host-init.sh -f check_iptables
  else
    echo "please exec install-tke-installer.sh to start tke-installer" && exit 0
  fi
  # start install tkestack
  dpl_offline_pot
  init_keepalived
  echo "###### install tkestack end ######"
}

defaut(){
  # change tke components's replicas number
  if [ -f "hosts" ]; then 
    sed -i 's/tke_replicas="2"/tke_replicas="1"/g' hosts
  fi
  # only deploy tkestack
  if [ -d '../tkestack' ] && [ ! -d "../offline-pot-images" ] && [ ! -d "../offline-pot-tgz" ]; then
    only_install_tkestack
  fi
  dpl_offline_pot
  # when deploy tkestack will be init keepalived config
  if [ -d '../tkestack' ]; then
    init_keepalived
  fi
}

all_func(){
  # change tke components's replicas number
  if [ -f "hosts" ]; then 
    sed -i 's/tke_replicas="2"/tke_replicas="1"/g' hosts
  fi
  init_and_check
  defaut
}

main(){
  $CALL_FUN || help
}
main

此脚本主要是判断当前部署是否需要部署tkestack或者是否单独部署tkestack,若是部署tkestack则生成tkestack 所需的配置文件,然后通过docker restart tke-installer 即可出发tkestack部署以及业务依赖组件,业务部署。

  • 添加worker节点

  • 增加自定义参数使集群更稳,更强。主要增加自定义参数如下:

    1. dockerExtraArgs data-root 制定docker 目录到数据盘,避免系统盘太小导致节点磁盘使用率很快到达节点压力阈值以至于节点处于not ready状态
    2. kubeletExtraArgs kubelete自定义参数 root-dir 和docker data-root 参数作用一致
    3. kubeletExtraArgs  kube-apiserver runtime-config apps/v1beta1=true,apps/v1beta2=true,extensions/v1beta1/daemonsets=true,extensions/v1beta1/deployments=true,extensions/v1beta1/replicasets=true,extensions/v1beta1/networkpolicies=true,extensions/v1beta1/podsecuritypolicies=true 增加工作负载deployment
    

,statefulset的api version兼容性


当前通过ansible set facts 方式,ansible when 条件执行,以及shell 命令增加判断方式实现幂等;通过设置开关+hooks+ansible tag方式实现扩展性和解耦。  
 最终私有化一键部署流程如下:

#### 3. 业务应用

  
 业务通过helmfile release(release名称必须以${中心名}-${客户名简称})来组织不同客户部署不同的业务组件,不同release对应到不同的业务组件的helm chart value,当打包业务helm时会根据release ${中心名}-${客户名简称}.yaml文件定义的业务组件进行过滤打包,完成业务按需部署。  
   
 私有化一键部署时会通过helmfile 工具进行部署,如上图所示。  
   
 更新过程私有化一键部署当前不纳入管控,只负责将 agent 部署到master1 节点作为cicd执行更新agent,具体流程如上图所示。

合理利用tkestack特性(用好80%),结合自身业务场景做出满足需求私有化一键部署\(做好20%\)。

### 不足

* 当前所有镜像都是打成tar附件模式打包安装包,使得安装包有点大;同时部署集群镜像仓库时还需要从installer节点的镜像仓库重新将镜像推送至集群镜像仓库,这个耗时很大;建议将出包时将镜像推送到离线镜像仓库,然后将离线镜像仓库持久化目录打包这样合理利用镜像特性缩减安装包大小;部署时拷贝镜像仓库持久化数据到对应目录并挂载,加速部署。

4 - 使用存储的实践

使用存储的实践

本文以介绍如何在不同场景下选用合适的存储类型,并以实际的例子演示如何通过 TKEStack 部署和管理一个分布式存储服务,并以云原生的方式为容器化的应用提供高可用、高性能的存储服务。

简介

TKEStack 是腾讯开源的一款集易用性和扩展性于一身的企业级容器服务平台,帮助用户在私有云环境中敏捷、高效地构建和发布应用服务。TKEStack 本身不提供存储功能,但是可以通过集成云原生的存储应用,或者通过存储扩展组件的方式,对接用户的存储设施,扩展 TKEStack 平台的存储能力。

在本文中,通过 TKEStack 集成不同类型存储的介绍,帮助用户掌握云原生环境下存储的使用与管理,助力用户构建面向不同业务场景的容器云解决方案。

本文所介绍的实践方案基于社区开源的存储方案,以及部分云提供商的存储服务方案,TKEStack 不提供对存储服务的质量保证。用户请根据自身实际情况,选择合适的存储方案,或者联系 TKEStack 官方社区、论坛寻求帮助。

TKEStack 支持通过 CSI 存储插件的方式对接外部存储系统,详情请参考 TKEStack CSI Operator

存储类型的选择

存储类型大致有三种:块存储、文件存储及对象存储

  1. 块存储:是以块为单位,块存储实际上是管理到数据块一级,相当于直接管理硬盘的数据块。高性能、低时延,满足随机读写,使用时需要格式化为指定的文件系统后才能访问。
  2. 文件存储:文件系统存储,文件系统是操作系统概念的一部份,支持 POSIX 的文件访问接口。优势是易管理、易共享,但由于采用上层协议, 因此开销大, 延时比块存储高。
  3. 对象存储:提供 Key-Value(简称 K/V)方式的 RESTful 数据读写接口,并且常以网络服务的形式提供数据的访问。优点是高可用性、全托管、易扩展。

上述几种存储类型各具特色,分别对应不同场景下的需求,例如:

  1. 块存储具备高性能的读写,提供原始块设备操作能力,非常适合作为一些数据库系统的底层存储。
  2. 文件系统有着与操作系统一致的 POSIX 文件访问接口,能够方便的在特定范围内共享文件空间,典型的场景是 AI 学习、模型训练场景下对训练数据,模型和结果的存储。
  3. 对象存储是近年来兴起的一种新的存储方式,适用于分布式云计算场景下的应用业务的海量,高并发的互联网产品场景。

更多存储系统的信息,请参考:wiki

块存储参考实践

本节将介绍如何通过 TKEStack 部署一套块存储系统,并演示如何在集群中使用该存储,在集群中部署 ElasticSearch 对外提供服务。

块存储系统有着较长的历史,基于传统 SAN 存储系统的方案已经非常成熟,但是 SAN 系统的价格较高,且可扩展性较差,难以满足大规模云计算系统下的使用需求。

但块存储本身具有的高带宽、低延迟,高吞吐率等优势,使它在云计算领域仍具有一席之地,典型的产品有 Ceph RBD,AWS EBS,腾讯云 CBS 等。

本节将以 Ceph RBD 为例,通过 Rook 在 TKEStack 容器平台中部署 Ceph RBD 块存储集群,对业务提供块存储服务。

Rook 是一个自管理的分布式存储编排系统,可以为 Kubernetes 提供便利的存储解决方案。

Rook 支持在 K8S 中部署,主要由 Operator 和 Cluster 两部分组成:

  1. Operator:Rook 的核心组件,自动启动存储集群,并监控存储守护进程,来确保存储集群的健康。
  2. Cluster:负责创建 CRD 对象,指定相关参数,包括 Ceph 镜像、元数据持久化位置、磁盘位置、dashboard 等等。

部署块存储系统

Rook 支持通过 helm 或 yaml 文件的方式进行部署,本文直接使用官方的 yaml 文件部署 Rook Ceph 存储集群。

  1. 登录 TKEStack 管理页面,进入集群管理页下,新建一个至少包括三台节点的集群

  2. 登录至该集群的任意节点下,下载 Rook 项目,通过官方例子部署 Rook 集群

    git clone --single-branch --branch release-1.3 https://github.com/rook/rook.git
    cd rook/cluster/examples/kubernetes/ceph
    kubectl create -f common.yaml
    kubectl create -f operator.yaml
    kubectl create -f cluster.yaml
    

文件中有几个地方要注意:

  • dataDirHostPath: 这个路径是会在宿主机上生成的,默认为 /var/lib/rook,保存的是 ceph 的相关的配置文件,再重新生成集群的时候要确保这个目录为空,否则 Ceph 监视器守护进程 MON 会无法启动
  • useAllDevices: 使用节点上所有的设备,默认为 true,使用宿主机所有可用的磁盘
  • useAllNodes:使用所有的 node 节点,默认为 true,使用用 k8s 集群内的所有 node 来搭建 Ceph
  • network.hostNetwork: 使用宿主机的网络进行通讯,默认为 false,如果需要集群外挂载的场景可以开启这个选项

部署完毕后,检查 Rook 组件工作状态:

# kubectl get pod -n rook-ceph
NAME                                         READY   STATUS      RESTARTS   AGE
csi-rbdplugin-b52jx                          3/3     Running     3          9d
csi-rbdplugin-jpmgv                          3/3     Running     1          2d9h
csi-rbdplugin-provisioner-54cc7d5848-5fn8c   5/5     Running     0          2d8h
csi-rbdplugin-provisioner-54cc7d5848-w8dvk   5/5     Running     3          4d22h
csi-rbdplugin-z8dlc                          3/3     Running     1          2d9h
rook-ceph-mgr-a-6775645c-7qg5t               1/1     Running     1          4d7h
rook-ceph-mon-a-98664df75-jm72n              1/1     Running     0          2d9h
rook-ceph-operator-676bcb686f-2kwmz          1/1     Running     0          9d
rook-ceph-osd-0-6949755785-gjpxw             1/1     Running     0          2d9h
rook-ceph-osd-1-647fdc4d84-6lvw2             1/1     Running     0          11d
rook-ceph-osd-2-c6c6db577-gtcpz              1/1     Running     0          2d8h
rook-ceph-osd-prepare-172.21.64.15-xngl7     0/1     Completed   0          2d8h
rook-ceph-osd-prepare-172.21.64.36-gv2kw     0/1     Completed   0          28d
rook-ceph-osd-prepare-172.21.64.8-bhffr      0/1     Completed   0          2d8h
rook-ceph-tools-864695994d-b7nb8             1/1     Running     0          2d7h
rook-discover-28fqr                          1/1     Running     0          2d9h
rook-discover-m529m                          1/1     Running     0          9d
rook-discover-t2jzc                          1/1     Running     0          2d9h

至此一个完整的 Ceph RBD 集群就建立完毕,每台节点上都部署有 Ceph 对象存储守护进程 OSD,默认使用节点下的 /var/lib/rook 目录存储数据。

使用块存储部署 ElasticSearch

  1. 创建 Ceph pool,创建 StorageClass

    # cat storageclass.yaml
    ---                                                                                                                                                                                                                                
    apiVersion: ceph.rook.io/v1                                                                                           
    kind: CephBlockPool                                                                                                   
    metadata:                                                                                                             
      name: replicapool                                                                                                  
      namespace: rook-ceph                                                                                                
    spec:                                                                                                                 
      failureDomain: host                                                                                                 
      replicated:                                                                                                        
        size: 1        # 池中数据的副本数                                                                                     
    ---                                                                                                                   
    apiVersion: storage.k8s.io/v1                                                                                         
    kind: StorageClass                                                                                                    
    metadata:                                                                                                             
      name: global-storageclass                                                                                           
    provisioner: rook-ceph.rbd.csi.ceph.com                                                                               
    parameters:                                                                                                           
      # clusterID is the namespace where the rook cluster is running                                                      
      # If you change this namespace, also change the namespace below where the secret namespaces are defined             
      clusterID: rook-ceph                                                                                               
    
      # Ceph pool into which the RBD image shall be created                                                               
      pool: replicapool                                                                                                   
    
      # RBD image format. Defaults to "2".                                                                                
      imageFormat: \"2\"                                                                                                  
    
      # RBD image features. Available for imageFormat: "2". CSI RBD currently supports only layering feature.             
      imageFeatures: layering                                                                                             
    
      # The secrets contain Ceph admin credentials. These are generated automatically by the operator                     
      # in the same namespace as the cluster.                                                                             
      csi.storage.k8s.io/provisioner-secret-name: rook-csi-rbd-provisioner                                                
      csi.storage.k8s.io/provisioner-secret-namespace: rook-ceph                                                          
      csi.storage.k8s.io/node-stage-secret-name: rook-csi-rbd-node                                                        
      csi.storage.k8s.io/node-stage-secret-namespace: rook-ceph                                                          
      # Specify the filesystem type of the volume. If not specified, csi-provisioner                                      
      # will set default as ext4.                                                                                         
      csi.storage.k8s.io/fstype: ext4                                                                                    
    # uncomment the following to use rbd-nbd as mounter on supported nodes                                                
    #mounter: rbd-nbd                                                                                                     
    reclaimPolicy: Delete                                              
    
  2. 通过 StorageClass 动态创建 PVC,检查 PVC 能够正确的创建并绑定

    # cat pvc.yaml
    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      name: rbd-pvc
    spec:
      accessModes:
      - ReadWriteOnce
      resources:
        requests:
          storage: 1Gi
      storageClassName: global-storageclass
    # kubectl get pvc
    NAME      STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS          AGE
    rbd-pvc   Bound    pvc-f9703e0d-9887-4bf2-8e31-1c3103a6ce2f   1Gi        RWO            global-storageclass   2s
    
  3. 创建 ElasticSearch 应用,通过 StorageClass 动态申请块存储,使用 Ceph RBD 集群作为 ElasticSearch 的后端存储设备

    # cat es.yaml
    apiVersion: v1
    kind: Namespace
    metadata:
      name: efk   
    
    ---
    apiVersion: apps/v1
    kind: StatefulSet
    metadata:
      name: elasticsearch-master
      namespace: efk
      labels:
        app: elasticsearch-master
    spec:
      podManagementPolicy: Parallel
      serviceName: elasticsearch-master
      replicas: 3
      selector:
        matchLabels:
          app: elasticsearch-master
      template:
        metadata:
          labels:
            app: elasticsearch-master
        spec:
          affinity:
            podAntiAffinity:
              preferredDuringSchedulingIgnoredDuringExecution:
              - weight: 1
                podAffinityTerm:
                  topologyKey: kubernetes.io/hostname
                  labelSelector:
                    matchLabels:
                      app: "elasticsearch-master"
          initContainers:
          # see https://www.elastic.co/guide/en/elasticsearch/reference/current/vm-max-map-count.html
          # and https://www.elastic.co/guide/en/elasticsearch/reference/current/setup-configuration-memory.html#mlockall
          - name: "sysctl"
            image: "busybox"
            imagePullPolicy: "Always"
            command: ["sysctl", "-w", "vm.max_map_count=262144"]
            securityContext:
              allowPrivilegeEscalation: true
              privileged: true
          containers:
          - name: elasticsearch
            env:
            - name: cluster.name
              value: elasticsearch-cluster
            - name: discovery.zen.ping.unicast.hosts
              value: elasticsearch-master
            - name: discovery.zen.minimum_master_nodes
              value: "2"
            - name: KUBERNETES_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
            # node roles, default to all true
            # - name: NODE_MASTER
            #   value: "true"
            # - name: NODE_DATA
            #   value: "true"
            # - name: NODE_INGEST
            #   value: "true"
            - name: PROCESSORS
              valueFrom:
                resourceFieldRef:
                  resource: limits.cpu
            - name: ES_JAVA_OPTS
              value: "-Djava.net.preferIPv4Stack=true -Xmx1g -Xms1g"
            resources:
            readinessProbe:
              httpGet:
                path: /_cluster/health?local=true
                port: 9200
              initialDelaySeconds: 5
            image: docker.elastic.co/elasticsearch/elasticsearch-oss:6.2.4
            imagePullPolicy: IfNotPresent
            ports:
            - containerPort: 9300
              name: transport
            - containerPort: 9200
              name: http
            volumeMounts:
            - mountPath: /usr/share/elasticsearch/data
              name: elasticsearch-master
          volumes:
          terminationGracePeriodSeconds: 120
      volumeClaimTemplates:
      - metadata:
          name: elasticsearch-master
        spec:
          accessModes:
          - ReadWriteOnce
          resources:
            requests:
              storage: 20Gi
          storageClassName: global-storageclass
          volumeMode: Filesystem
    
    ---
    
    apiVersion: v1
    kind: Service
    metadata:
      name: elasticsearch-master
      namespace: efk
      labels:
        app: elasticsearch-master
    spec:
      type: ClusterIP
      ports:
        - name: http
          port: 9200
          protocol: TCP
          targetPort: 9200
        - name: transport
          port: 9300
          protocol: TCP
          targetPort: 9300
      selector:
        app: elasticsearch-master
    
  4. 等待 ElasticSearch 实例建立完成,磁盘被正确的挂载

    # kubectl get pod -n efk
    NAME                          READY   STATUS    RESTARTS   AGE
    elasticsearch-master-0        1/1     Running   0          2d19h
    elasticsearch-master-1        1/1     Running   0          2d19h
    elasticsearch-master-2        1/1     Running   0          2d19h
    # kubectl get pvc -n efk
    NAME                                          STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS          AGE
    elasticsearch-master-elasticsearch-master-0   Bound    pvc-405fc7b2-82eb-4eda-a6b7-e54d1713cbeb   20Gi       RWO            global-storageclass   41d
    elasticsearch-master-elasticsearch-master-1   Bound    pvc-6e89f8d5-4702-4b2f-81c2-f9f89a249b0a   20Gi       RWO            global-storageclass   41d
    elasticsearch-master-elasticsearch-master-2   Bound    pvc-16c9c1ab-4c77-463f-b5f0-fc643ced2fce   20Gi       RWO            global-storageclass   41d
    

块存储小结

按照本节指引,用户可以在 TKEStack 平台下创建出一个 Ceph RBD 块存储集群,并通过 StorageClass 方式动态的申请和使用块存储资源,并能够搭建出 ElasticSearch 应用对外提供服务。

由于块存储的特性,在 K8S 场景下仅支持 ReadWriteOnce 和 ReadOnlyMany 访问方式,并且这里的访问方式是节点级别的,例如 ReadOnlyMany, 只能被同一节点的多个 Pod 挂载,如果从多个节点挂载,系统会报 Multi-Attach 错误。

因此在 K8S 使用块存储场景上,基本上一个 Pod 挂载一个 PVC,典型的应用场景是 Redis,ElasticSearch,Mysql数据库等。

如果用户已存在块存储设备,TKEStack 支持通过 CSI 插件对接已有的存储设备,详情请参考 TKEStack CSI Operator

文件存储参考实践

本节将介绍如何在 TKEStack 中部署一套文件存储系统,并演示如何在平台中使用该存储。

本节以 ChubaoFS (储宝文件系统)为例,通过在集群中部署和集成 ChubaoFS,向用户展示如何在 TKEStack 中使能文件存储功能。

ChubaoFS (储宝文件系统)是为大规模容器平台设计的分布式文件系统,详情请参考ChubaoFS 官方文档

ChubaoFS 支持在 k8s 集群中部署,通过 Helm 的方式在集群中安装元数据子系统,数据子系统和资源管理节点等,对外提供文件存储服务。

部署文件存储系统

  1. 登录 TKEStack 管理页面,进入集群管理页下,新建一个至少包括五台节点的集群,并为该集群使能"Helm 应用管理"扩展组件

  2. 设置节点标签,ChubaoFS 将根据标签分配不同的组件到节点上运行

    kubectl label node <nodename> chuabaofs-master=enabled
    kubectl label node <nodename> chuabaofs-metanode=enabled
    kubectl label node <nodename> chuabaofs-datanode=enabled
    

    注:至少保证有3台 Master,3台 Metanode,5台 Datanode

  3. 登录到该集群下的一台节点上,下载 ChubaoFS 应用的 chart 包,根据环境修改 values.yaml 文件中的参数(本文使用默认值)

    # ls chubaofs
    Chart.yaml  config  README.md  templates  values.yaml
    
  4. 本地安装 Helm 客户端, 更多可查看 安装 Helm,使用 Helm 客户端安装 ChubaoFS ,等待安装完成

    # helm install --name chubao ./chubaofs
    # ./helm status chubao
    
  5. 安装完成后,检查所有组件工作正常

    # kubectl get pod -n chubaofs
    NAME                          READY   STATUS    RESTARTS   AGE
    client-c5c5b99f6-qqf7h        1/1     Running   0          17h
    consul-6d67d5c55-jgw9z        1/1     Running   0          18h
    datanode-9vqvm                1/1     Running   0          18h
    datanode-bgffs                1/1     Running   0          18h
    datanode-dtckp                1/1     Running   0          18h
    datanode-jtrzj                1/1     Running   0          18h
    datanode-p5nmc                1/1     Running   0          18h
    grafana-7cc9db7489-st27v      1/1     Running   0          18h
    master-0                      1/1     Running   0          18h
    master-1                      1/1     Running   0          18h
    master-2                      1/1     Running   0          17h
    metanode-ghrpm                1/1     Running   0          18h
    metanode-gn5kl                1/1     Running   0          18h
    metanode-wqzwp                1/1     Running   0          18h
    prometheus-77d5d6cb7f-xs748   1/1     Running   0          18h
    kubectl get svc -n chubaofs
    NAME                 TYPE        CLUSTER-IP        EXTERNAL-IP   PORT(S)          AGE
    consul-service       NodePort    192.168.255.116   <none>        8500:30831/TCP   20h
    grafana-service      ClusterIP   192.168.255.16    <none>        3000/TCP         20h
    master-service       NodePort    192.168.255.104   <none>        8080:32102/TCP   20h
    prometheus-service   ClusterIP   192.168.255.4     <none>        9090/TCP         20h
    

使用文件存储系统

  1. 参考 ChubaoFS CSI 文档,在想要使用文件存储的集群上部署 ChubaoFS CSI Driver 插件

    # git clone https://github.com/chubaofs/chubaofs-csi.git
    # cd chubaofs-csi
    # kubectl apply -f deploy/csi-controller-deployment.yaml
    # kubectl apply -f deploy/csi-node-daemonset.yaml
    
  2. 创建 StorageClass,指定 master 和 consul 的访问地址

    kind: StorageClass
    apiVersion: storage.k8s.io/v1
    metadata:
      name: chubaofs-sc
    provisioner: csi.chubaofs.com
    reclaimPolicy: Delete
    parameters:
      masterAddr: "172.21.64.14:32102"	# Master地址
      owner: "csiuser"
      # cannot set profPort and exporterPort value, reason: a node may be run many cfs-client
      #  profPort: "10094"
      #  exporterPort: "9513"
      consulAddr: "172.21.64.14:30831"	# 监控系统的地址
      logLevel: "debug"
    
  3. 通过 StorageClass 创建 PVC,创建使用 PVC 的应用

    # cat pvc.yaml
    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      name: chubaofs-pvc
    spec:
      accessModes:
        - ReadWriteOnce
      resources:
        requests:
          storage: 1Gi
      storageClassName: chubaofs-sc
    # cat deployment.yaml 
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: cfs-csi-demo
      namespace: default
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: cfs-csi-demo-pod
      template:
        metadata:
          labels:
            app: cfs-csi-demo-pod
        spec:
          containers:
            - name: chubaofs-csi-demo
              image: nginx:1.17.9
              imagePullPolicy: "IfNotPresent"
              ports:
                - containerPort: 80
                  name: "http-server"
              volumeMounts:
                - mountPath: "/usr/share/nginx/html"
                  name: mypvc
          volumes:
            - name: mypvc
              persistentVolumeClaim:
                claimName: chubaofs-pvc
    

文件存储小结

通过上面的例子,用户可以创建一个 ChubaoFS 的文件系统集群,并通过 ChubaoFS CSI Driver 插件,将文件存储映射为 K8S 的资源(PVC,StorageClass)。

由于文件存储的支持多读多写的特性,使得用户在有共享存储需求的场景下,如 AI 计算、模型训练等,通过 TKEStack + ChubaoFS 的方案,快速构建出容器产品和解决方案。

如果用户已存在文件存储设备,TKEStack支持通过 CSI 插件对接已有的存储设备,详情请参考 TKEStack CSI Operator

对象存储参考实践

本节介绍最后一个存储类型——对象存储, 对象存储的访问接口基本都是 RESTful API,用户可通过网络存储和查看数据,具备高扩展性、低成本、可靠和安全特性。

常见的对象存储有 Ceph 的 RADOS、OpenStack 的 Swift、AWS S3 等,并且各大主流的云提供商的都有提供对象存储服务,方便互联网用户快速地接入,实现了海量数据访问和管理。

本节将在一个公有云的环境下,申请云提供商提供的对象存储,通过标准的 S3 接口对接 TKEStack 的镜像仓库的服务,这样就可以将 TKEStack 平台下的镜像存储在对象存储中,方便扩展和管理。

申请对象存储

以腾讯云为例,登录控制台后进入对象存储产品中心,在存储桶列表页面下创建一个新的存储桶,创建成功后记录下访问域名,所属地域,访问 ID 和密钥等。

注:访问 ID 和密钥信息请参考腾讯云对象存储文档,更多关于对象存储的信息访问官网

配置镜像仓库

TKEStack 提供镜像仓库功能,为用户提供容器镜像的上传,下载和管理功能,并且镜像仓库中保存平台所需的所有镜像,满足各种离线环境的需求。

如果按照默认方式安装配置,TKEStack 的镜像仓库默认使用 Global 集群下 Master 主机上的存储资源,该种方式占用了有限的主机资源,且不方便进行扩展和迁移,有必要对镜像仓库模块重新配置,使其对接对象存储,方便扩展。

  1. 登录 TKEStack 管理界面,进入 Global 集群配置管理,找到 tke 命名空间下的 tke-registry-api 配置(configmap),修改 storage 字段如下

      tke-registry-config.yaml: |
        apiVersion: registry.config.tkestack.io/v1
        kind: RegistryConfiguration
        storage:
    #      fileSystem:
    #        rootDirectory: /storage
          s3:
            bucket: xxxxxxxxxxxxx
            region: ap-beijing
            accessKey: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
            secretKey: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
            secure: false
            regionEndpoint: https://xxxxxxxxxxxxxx.cos.ap-beijing.myqcloud.com
    
  2. 修改完成后,重启 tke 命名空间下的 tke-registry-api 的 pod,使配置生效,等待重启后 pod 恢复

  3. 此时镜像仓库已使用基于 S3 接口的对象存储,用户可以向该仓库推送或下载镜像,验证功能

注意:

  • 由于更换了底层存储,重启后的镜像仓库中没有镜像,需要用户提前将原仓库中的所有镜像备份后,重新恢复至新的镜像仓库中
  • 也可以在安装 TKEStack 平台时,为镜像仓库,以及监控存储配置对象存储服务,免去后期转移镜像和存储的操作,关于如何在安装时指定存储服务,详见高可用部署相关文章

对象存储小结

本节展示如果通过云提供商提供的对象存储服务,增强 TKEStack 的镜像仓库服务。在实际场景中,用户根据自身情况,选择合适的公有云上服务。

本文前面介绍的 Rook Ceph,ChubaoFS 等都支持对象存储服务,用户也可自行搭建本地的对象存储集群,详细指引参考对应产品的官网。

总结

综上所述,TKEStack 平台能够通过各种云原生及扩展组件的方式,对接和集成不同种类的存储服务,满足各类应用场景的需求。后续 TKEStack 还会继续增强在存储方面的功能,不断完善操作体验,使得容器平台存储功能具备易于上手,种类丰富,灵活扩展的能力。

参考链接

  1. https://github.com/tkestack/tke
  2. https://www.zhihu.com/question/21536660
  3. https://rook.io/
  4. https://github.com/chubaofs/chubaofs
  5. https://cloud.tencent.com/document/product/436/6222

5 - 基于 Jenkins 的 CI/CD

基于 Jenkins 的 CI/CD

持续构建与发布是日常工作中必不可少的一个步骤,目前大多公司都采用 Jenkins 集群来搭建符合需求的 CI/CD 流程,然而传统的 Jenkins Slave 一主多从方式会存在一些痛点,比如:

  • 主 Master 发生单点故障时,整个流程都不可用了
  • 每个 Slave 的配置环境不一样,来完成不同语言的编译打包等操作,但是这些差异化的配置导致管理起来非常不方便,维护起来也是比较费劲
  • 资源分配不均衡,有的 Slave 要运行的 job 出现排队等待,而有的 Slave 处于空闲状态
  • 资源有浪费,每台 Slave 可能是物理机或者虚拟机,当 Slave 处于空闲状态时,也不会完全释放掉资源。

正因为上面的这些种种痛点,渴望一种更高效更可靠的方式来完成这个 CI/CD 流程,而 Docker 虚拟化容器技术能很好的解决这个痛点,又特别是在 Kubernetes 集群环境下面能够更好来解决上面的问题,下图是基于 Kubernetes 搭建 Jenkins 集群的简单示意图:

从图上可以看到 Jenkins Master 和 Jenkins Slave 以 Pod 形式运行在 Kubernetes 集群的 Node 上,Master 运行在其中一个节点,并且将其配置数据存储到一个 Volume 上去,Slave 运行在各个节点上,并且它不是一直处于运行状态,它会按照需求动态的创建并自动删除。

这种方式的工作流程大致为:当 Jenkins Master 接受到 Build 请求时,会根据配置的 Label 动态创建一个运行在 Pod 中的 Jenkins Slave 并注册到 Master 上,当运行完 Job 后,这个 Slave 会被注销并且这个 Pod 也会自动删除,恢复到最初状态。

使用这种方式带来的好处:

  • 服务高可用:当 Jenkins Master 出现故障时,Kubernetes 会自动创建一个新的 Jenkins Master 容器,并且将 Volume 分配给新创建的容器,保证数据不丢失,从而达到集群服务高可用。
  • 动态伸缩:合理使用资源,每次运行 Job 时,会自动创建一个 Jenkins Slave,Job 完成后,Slave 自动注销并删除容器,资源自动释放,而且 Kubernetes 会根据每个资源的使用情况,动态分配 Slave 到空闲的节点上创建,降低出现因某节点资源利用率高,还排队等待在该节点的情况。
  • 扩展性好:当 Kubernetes 集群的资源严重不足而导致 Job 排队等待时,可以很容易的添加一个 Kubernetes Node 到集群中,从而实现扩展。

安装 Jenkins Master

既然要基于Kubernetes来做CI/CD,这里需要将 Jenkins 安装到 Kubernetes 集群当中,新建一个 Deployment:(jenkins2.yaml)

前提:集群中可以使用 PVC

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: jenkins2
  namespace: kube-ops
spec: 
  selector:
    matchLabels:
      app: jenkins2
  template:
    metadata:
      labels:
        app: jenkins2
    spec:
      terminationGracePeriodSeconds: 10
      serviceAccount: jenkins2
      containers:
      - name: jenkins
        image: jenkins/jenkins:lts
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 8080
          name: web
          protocol: TCP
        - containerPort: 50000
          name: agent
          protocol: TCP
        resources:
          limits:
            cpu: 1000m
            memory: 1Gi
          requests:
            cpu: 500m
            memory: 512Mi
        livenessProbe:
          httpGet:
            path: /login
            port: 8080
          initialDelaySeconds: 60
          timeoutSeconds: 5
          failureThreshold: 12
        readinessProbe:
          httpGet:
            path: /login
            port: 8080
          initialDelaySeconds: 60
          timeoutSeconds: 5
          failureThreshold: 12
        volumeMounts:
        - name: jenkinshome
          subPath: jenkins2
          mountPath: /var/jenkins_home
      securityContext:
        fsGroup: 1000
      volumes:
      - name: jenkinshome
        persistentVolumeClaim:
          claimName: opspvc

---
apiVersion: v1
kind: Service
metadata:
  name: jenkins2
  namespace: kube-ops
  labels:
    app: jenkins2
spec:
  selector:
    app: jenkins2
  type: NodePort
  ports:
  - name: web
    port: 8080
    targetPort: web
    nodePort: 30002
  - name: agent
    port: 50000
    targetPort: agent

这里将所有的对象资源都放置在一个名为 kube-ops 的 namespace 下面:

$ kubectl create namespace kube-ops

这里使用一个名为 jenkins/jenkins:lts 的镜像,这是 jenkins 官方的 Docker 镜像,然后也有一些环境变量,当然也可以根据自己的需求来定制一个镜像,比如可以将一些插件打包在自定义的镜像当中,可以参考文档:https://github.com/jenkinsci/docker,我们这里使用默认的官方镜像就行,另外一个还需要注意的是将容器的 /var/jenkins_home 目录挂载到了一个名为 opspvc 的 PVC 对象上面,所以同样还得提前创建一个对应的 PVC 对象,当然也可以使用 StorageClass 对象来自动创建:(pvc.yaml)

apiVersion: v1
kind: PersistentVolume
metadata:
  name: opspv
spec:
  capacity:
    storage: 20Gi
  accessModes:
  - ReadWriteMany
  persistentVolumeReclaimPolicy: Delete
  nfs:
    server: 42.194.158.74
    path: /data/k8s

---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: opspvc
  namespace: kube-ops
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 20Gi

创建需要用到的 PVC 对象:

$ kubectl create -f pvc.yaml

另外这里还需要使用到一个拥有相关权限的 serviceAccount:jenkins2,我们这里只是给 jenkins 赋予了一些必要的权限,当然如果你对 serviceAccount 的权限不是很熟悉的话,给这个 SA 绑定一个 cluster-admin 的集群角色权限也是可以的,当然这样具有一定的安全风险:(rbac.yaml)

apiVersion: v1
kind: ServiceAccount
metadata:
  name: jenkins2
  namespace: kube-ops

---

kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: jenkins2
rules:
  - apiGroups: ["extensions", "apps"]
    resources: ["deployments"]
    verbs: ["create", "delete", "get", "list", "watch", "patch", "update"]
  - apiGroups: [""]
    resources: ["services"]
    verbs: ["create", "delete", "get", "list", "watch", "patch", "update"]
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["create","delete","get","list","patch","update","watch"]
  - apiGroups: [""]
    resources: ["pods/exec"]
    verbs: ["create","delete","get","list","patch","update","watch"]
  - apiGroups: [""]
    resources: ["pods/log"]
    verbs: ["get","list","watch"]
  - apiGroups: [""]
    resources: ["secrets"]
    verbs: ["get"]

---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: jenkins2
  namespace: kube-ops
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: jenkins2
subjects:
  - kind: ServiceAccount
    name: jenkins2
    namespace: kube-ops

创建 RBAC 相关的资源对象:

$ kubectl create -f rbac.yaml
serviceaccount "jenkins2" created
role.rbac.authorization.k8s.io "jenkins2" created
rolebinding.rbac.authorization.k8s.io "jenkins2" created

最后为了方便测试,这里通过 NodePort 的形式来暴露 Jenkins 的 web 服务,固定为 30002 端口,另外还需要暴露一个 agent 的端口,这个端口主要是用于 Jenkins 的 master 和 slave 之间通信使用的。

一切准备的资源准备好过后,直接创建 Jenkins 服务:

$ kubectl create -f jenkins2.yaml
deployment.extensions "jenkins2" created
service "jenkins2" created

创建完成后,要去拉取镜像可能需要等待一会儿,然后我们查看下 Pod 的状态:

$ kubectl get pods -n kube-ops
NAME                        READY     STATUS    RESTARTS   AGE
jenkins2-7f5494cd44-pqpzs   0/1       Running   0          2m

可以看到该 Pod 处于 Running 状态,但是 READY 值确为 0,然后用 describe 命令去查看下该 Pod 的详细信息:

$ kubectl describe pod jenkins2-7f5494cd44-pqpzs -n kube-ops
...
Normal   Created                3m                kubelet, node01    Created container
  Normal   Started                3m                kubelet, node01    Started container
  Warning  Unhealthy              1m (x10 over 2m)  kubelet, node01    Liveness probe failed: Get http://10.244.1.165:8080/login: dial tcp 10.244.1.165:8080: getsockopt: connection refused
  Warning  Unhealthy              1m (x10 over 2m)  kubelet, node01    Readiness probe failed: Get http://10.244.1.165:8080/login: dial tcp 10.244.1.165:8080: getsockopt: connection refused

可以看到上面的 Warning 信息,健康检查没有通过,可以通过查看日志进一步了解:

$ kubectl logs -f jenkins2-7f5494cd44-pqpzs -n kube-ops
touch: cannot touch '/var/jenkins_home/copy_reference_file.log': Permission denied
Can not write to /var/jenkins_home/copy_reference_file.log. Wrong volume permissions?

很明显可以看到上面的错误信息,意思就是:当前用户没有权限在 jenkins 的 home 目录下面创建文件,这是因为默认的镜像使用的是 jenkins 这个用户,而通过 PVC 挂载到 NFS 服务器的共享数据目录下面却是 root 用户的,所以没有权限访问该目录,要解决该问题,也很简单,只需要在 NFS 共享数据目录下面把目录权限重新分配下即可:

$ chown -R 1000 /data/k8s/jenkins2

当然还有另外一种方法是:自定义一个镜像,在镜像中指定使用 root 用户也可以

然后再重新创建:

$ kubectl delete -f jenkins.yaml
deployment.extensions "jenkins2" deleted
service "jenkins2" deleted
$ kubectl create -f jenkins.yaml
deployment.extensions "jenkins2" created
service "jenkins2" created

现在我们再去查看新生成的 Pod 已经没有错误信息了:

$ kubectl get pods -n kube-ops
NAME                        READY     STATUS        RESTARTS   AGE
jenkins2-7f5494cd44-smn2r   1/1       Running       0          25s

等到服务启动成功后,我们就可以根据任意节点的 IP:30002 端口就可以访问 jenkins 服务了,可以根据提示信息进行安装配置即可: 初始化的密码可以在 jenkins 的容器的日志中进行查看,也可以直接在 NFS 的共享数据目录中查看:

$ cat /data/k8s/jenkins2/secrets/initialAdminPassword

然后选择安装推荐的插件即可:

安装完成后添加管理员帐号即可进入到 jenkins 主界面:

配置 Jenkins Slave

接下来就需要来配置 Jenkins,让他能够动态的生成 Slave 的 Pod。

第1步:需要安装 kubernetes plugin, 点击 Manage Jenkins -> Manage Plugins -> Available -> Kubernetes plugin 勾选安装即可。

第2步: 安装完毕后,点击 Manage Jenkins —> Configure System —> (拖到最下方的 Cloud,点击 a separate configuration page) —> Add a new cloud —> 选择 Kubernetes,然后填写 Kubernetes 和 Jenkins 配置信息。

  • Kubernetes 名称:kubernetes

  • Kubernetes 地址:https://kubernetes.default.svc.cluster.local

  • Kubernetes 命名空间: kube-ops

    然后点击 连接测试,如果出现 Connected to Kubernetes 1.18 的提示信息证明 Jenkins 已经可以和 Kubernetes 系统正常通信了。如果失败的话,很有可能是权限问题,这里就需要把创建的 jenkins 的 serviceAccount 对应的 secret 添加到这里的 Credentials 里面。

  • Jenkins URL 地址:http://jenkins2.kube-ops.svc.cluster.local:8080

    这里的格式为:服务名.namespace.svc.cluster.local:8080

第3步:配置 Pod Template,其实就是配置 Jenkins Slave 运行的 Pod 模板

  • 名称:jnlp
  • 命名空间:kube-ops
  • Labels :haimaxy-jnlp,这里也非常重要,对于后面执行 Job 的时候需要用到该值
  • 容器:使用 cnych/jenkins:jnlp 镜像,这个镜像是在官方的 jnlp 镜像基础上定制的,加入了 kubectl 等一些实用的工具

注意:由于新版本的 Kubernetes 插件变化较多,如果你使用的 Jenkins 版本在 2.176.x 版本以上,注意将上面的镜像替换成cnych/jenkins:jnlp6,否则使用会报错,配置如下图所示:

另外需要注意挂载两个主机目录,一个是/var/run/docker.sock,该文件是用于 Pod 中的容器能够共享宿主机的 Docker,这就是大家说的 docker in docker 的方式,Docker 二进制文件我们已经打包到上面的镜像中了,另外一个目录下/root/.kube目录,将这个目录挂载到容器的/root/.kube目录下面这是为了能够在 Pod 的容器中能够使用 kubectl 工具来访问的 Kubernetes 集群,方便后面在 Slave Pod 部署 Kubernetes 应用。

另外还有几个参数需要注意,如下图中的Time in minutes to retain slave when idle,这个参数表示的意思是当处于空闲状态的时候保留 Slave Pod 多长时间,这个参数最好保存默认就行了,如果你设置过大的话,Job 任务执行完成后,对应的 Slave Pod 就不会立即被销毁删除。

另外一些用户在配置了后运行 Slave Pod 的时候出现了权限问题,因为 Jenkins Slave Pod 中没有配置权限,所以需要配置上 ServiceAccount,在 Slave Pod 配置的地方点击下面的高级,添加上对应的 ServiceAccount 即可:

还有一些用户在配置完成后发现启动 Jenkins Slave Pod 的时候,出现 Slave Pod 连接不上,然后尝试100次连接之后销毁 Pod,然后会再创建一个 Slave Pod 继续尝试连接,无限循环,类似于下面的信息:

如果出现这种情况的话就需要将 Slave Pod 中的运行命令和参数两个值给清空掉

到这里我们的 Kubernetes Plugin 插件就算配置完成了。

测试

Kubernetes 插件的配置工作完成了,接下来添加一个 Job 任务,看是否能够在 Slave Pod 中执行,任务执行完成后看 Pod 是否会被销毁。

在 Jenkins 首页点击 新建item,创建一个测试的任务,输入任务名称,然后选择 Freestyle project 类型的任务:

注意在下面的 Label Expression 这里要填入haimaxy-jnlp,就是前面配置的 Slave Pod 中的 Label,这两个地方必须保持一致

然后往下拉,在 Build 区域选择Execute shell

然后输入测试命令

echo "测试 Kubernetes 动态生成 jenkins slave"
echo "==============docker in docker==========="
docker info

echo "=============kubectl============="
kubectl get pods

最后点击保存:

现在直接在页面点击做成的 Build now 触发构建即可,然后观察 Kubernetes 集群中 Pod 的变化:

$ kubectl get pods -n kube-ops
NAME                       READY     STATUS              RESTARTS   AGE
jenkins2-7c85b6f4bd-rfqgv   1/1       Running             3          1d
jnlp-9gjhr                 0/1       ContainerCreating   0          7s

可以看到在点击“立刻构建”的时候可以看到一个新的 Pod:jnlp-9gjhr 被创建了,这就是 Jenkins Slave。任务执行完成后可以看到任务信息,如下图所示:比如这里是花费了 2.3s 时间在 jnlp-hfmvd 这个 Slave上面:

如果没有看见新的 Pod:jnlp-9gjhr,可能原因是已经构建完成,Pod 被自动删除了。

同样也可以查看到对应的控制台信息:

到这里证明任务已经构建完成,然后这个时候再去集群查看我们的 Pod 列表,发现 kube-ops 这个 namespace 下面已经没有之前的 Slave 这个 Pod 了。

$ kubectl get pods -n kube-ops
NAME                       READY     STATUS    RESTARTS   AGE
jenkins2-7c85b6f4bd-rfqgv   1/1       Running   3          1d

Jenkins Pipeline

要实现在 Jenkins 中的构建工作,可以有多种方式,比如 Pipeline。简单来说,就是一套运行在 Jenkins 上的工作流框架,将原来独立运行于单个或者多个节点的任务连接起来,实现单个任务难以完成的复杂流程编排可视化的工作。

Jenkins Pipeline 核心概念:

  • Node:节点,一个 Node 就是一个 Jenkins 节点,Master 或者 Agent,是执行 Step 的具体运行环境,比如[上面](#配置 Jenkins Slave)动态运行的 Jenkins Slave 就是一个 Node 节点
  • Stage:阶段,一个 Pipeline 可以划分为若干个 Stage,每个 Stage 代表一组操作,比如:Build、Test、Deploy,Stage 是一个逻辑分组的概念,可以跨多个 Node
  • Step:步骤,Step 是最基本的操作单元,可以是打印一句话,也可以是构建一个 Docker 镜像,由各类 Jenkins 插件提供,比如命令:sh ‘make’,就相当于平时 shell 终端中执行 make 命令一样。

创建 Jenkins Pipline:

  • Pipeline 脚本是由 Groovy 语言实现的
  • Pipeline 支持两种语法:Declarative(声明式)和 Scripted Pipeline(脚本式)语法
  • Pipeline 也有两种创建方法:可以直接在 Jenkins 的 Web UI 界面中输入脚本;也可以通过创建一个 Jenkinsfile 脚本文件放入项目源码库中
  • 一般推荐在 Jenkins 中直接从源代码控制(SCMD)中直接载入 Jenkinsfile Pipeline 这种方法

创建一个简单的 Pipeline

这里快速创建一个简单的 Pipeline,直接在 Jenkins 的 Web UI 界面中输入脚本运行。

  • 新建 Job:在 Web UI 中点击 “新建任务” -> 输入名称:pipeline-demo -> 选择下面的 “流水线” -> 点击 OK

  • 配置:在最下方的 Pipeline 区域输入如下 Script 脚本,然后点击保存。

    node {
      stage('Clone') {
        echo "1.Clone Stage"
      }
      stage('Test') {
        echo "2.Test Stage"
      }
      stage('Build') {
        echo "3.Build Stage"
      }
      stage('Deploy') {
        echo "4. Deploy Stage"
      }
    }
    
  • 构建:点击左侧区域的 “立即构建”,可以看到 Job 开始构建了

隔一会儿,构建完成,可以点击左侧区域的 Console Output,就可以看到如下输出信息:

可以看到上面 Pipeline 脚本中的4条输出语句都打印出来了,证明是符合预期的。

如果对 Pipeline 语法不是特别熟悉的,可以点击脚本的下面的“流水线语法”进行查看,这里有很多关于 Pipeline 语法的介绍,也可以自动帮我们生成一些脚本。

在 Slave 中构建任务

上面创建了一个简单的 Pipeline 任务,但是可以看到这个任务并没有在 Jenkins 的 Slave 中运行,那么如何让我们的任务跑在 Slave 中呢?之前在添加 Slave Pod 的时候,有为其添加的 label,因此在创建任务的时候,可以通过 label 的形式将任务运行在指定 Slave 中。重新编辑上面创建的 Pipeline 脚本,给 node 添加一个 label 属性,如下:

node('haimaxy-jnlp') {
    stage('Clone') {
      echo "1.Clone Stage"
    }
    stage('Test') {
      echo "2.Test Stage"
    }
    stage('Build') {
      echo "3.Build Stage"
    }
    stage('Deploy') {
      echo "4. Deploy Stage"
    }
}

这里只是给 node 添加了一个 haimaxy-jnlp 这样的一个 label,然后保存,构建之前查看下 kubernetes 集群中的 Pod:

$ kubectl get pods -n kube-ops
NAME                       READY     STATUS              RESTARTS   AGE
jenkins-7c85b6f4bd-rfqgv   1/1       Running             4          6d

然后重新触发“立刻构建”:

$ kubectl get pods -n kube-ops
NAME                       READY     STATUS    RESTARTS   AGE
jenkins-7c85b6f4bd-rfqgv   1/1       Running   4          6d
jnlp-ntn1s                 1/1       Running   0          23s

发现多了一个名叫jnlp-ntn1s的 Pod 正在运行,隔一会儿这个 Pod 就不再了:

$ kubectl get pods -n kube-ops
NAME                       READY     STATUS    RESTARTS   AGE
jenkins-7c85b6f4bd-rfqgv   1/1       Running   4          6d

这也证明 Job 构建完成了,同样回到 Jenkins 的 Web UI 界面中查看 Console Output,可以看到如下的信息:

由此证明当前的任务在跑在上面动态生成的这个 Pod 中。回到任务的主界面,也可以看到“阶段视图”界面:

部署 Kubernetes 应用

要部署 Kubernetes 应用,熟悉一下 Kubernetes 应用的部署流程:

  • 编写代码
  • 测试
  • 编写 Dockerfile
  • 构建打包 Docker 镜像
  • 推送 Docker 镜像到仓库
  • 编写 Kubernetes YAML 文件
  • 更改 YAML 文件中 Docker 镜像 TAG
  • 利用 kubectl 工具部署应用

需要把上面这些流程放入 Jenkins 中来自动完成(编码除外),从测试到更新 YAML 文件属于 CI 流程,后面部署属于 CD 的流程。如果按照上面的示例,现在要来编写一个 Pipeline 的脚本:

node('haimaxy-jnlp') {
    stage('Clone') {
      echo "1.Clone Stage"
    }
    stage('Test') {
      echo "2.Test Stage"
    }
    stage('Build') {
      echo "3.Build Docker Image Stage"
    }
    stage('Push') {
      echo "4.Push Docker Image Stage"
    }
    stage('YAML') {
      echo "5. Change YAML File Stage"
    }
    stage('Deploy') {
      echo "6. Deploy Stage"
    }
}

这里将一个简单 golang 程序,部署到 kubernetes 环境中,代码链接:https://github.com/willemswang/jenkins-demo。如果按照之前的示例,Pipeline 脚本编写顺序如下:

  • 第一步,clone 代码
  • 第二步,进行测试,如果测试通过了才继续下面的任务
  • 第三步,由于 Dockerfile 基本上都是放入源码中进行管理的,所以这里就是直接构建 Docker 镜像了
  • 第四步,镜像打包完成,推送到镜像仓库
  • 第五步,镜像推送完成,更改 YAML 文件中的镜像 TAG 为这次镜像的 TAG
  • 第六步,使用 kubectl 命令行工具进行部署了

到这里整个 CI/CD 的流程就完成了。

接下来对每一步具体要做的事情进行详细描述:

第一步:Clone 代码

stage('Clone') {
    echo "1.Clone Stage"
    git url: "https://github.com/willemswang/jenkins-demo.git"
}

第二步:测试

由于示例代码比较简单,可以忽略该步骤

第三步:构建镜像

stage('Build') {
    echo "3.Build Docker Image Stage"
    sh "docker build -t default.registry.tke.com/library/jenkins-demo:${build_tag} ."
}

平时构建的时候一般是直接使用docker build命令进行构建就行了,但是 Slave Pod 的镜像里面采用的是 Docker In Docker 的方式,也就是说可以直接在 Slave 中使用 docker build 命令,所以这里可以直接使用 sh 直接执行 docker build 命令即可。但是镜像的 tag 呢?如果使用镜像 tag,则每次都是 latest 的 tag,这对于以后的排查或者回滚之类的工作会带来很大麻烦,这里可以采用和 git commit的记录为镜像的 tag,这里的好处就是镜像的 tag 可以和 git 提交记录对应起来,也方便日后对应查看。但是由于这个 tag 不只是这一个 stage 需要使用,下一个推送镜像是不是也需要,所以这里把 tag 编写成一个公共的参数,把它放在 Clone 这个 stage 中,这样一来前两个 stage 就变成了下面这个样子:

stage('Clone') {
    echo "1.Clone Stage"
    git url: "https://github.com/willemswang/jenkins-demo.git"
    script {
        build_tag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
    }
}
stage('Build') {
    echo "3.Build Docker Image Stage"
    sh "docker build -t default.registry.tke.com/library/jenkins-demo:${build_tag} ."
}

第四步:推送镜像

镜像构建完成了,现在就需要将此处构建的镜像推送到镜像仓库中去,可以是私有镜像仓库,也可以直接使用 docker hub 即可。

docker hub 是公共的镜像仓库,任何人都可以获取上面的镜像,但是要往上推送镜像就需要用到一个帐号了,所以需要提前注册一个 docker hub 的帐号,记住用户名和密码,这里需要使用。正常来说在本地推送 docker 镜像的时候,需要使用docker login命令,然后输入用户名和密码,认证通过后,就可以使用docker push命令来推送本地的镜像到 docker hub 上面去了。此外,由于 TKEStack 本身提供了镜像仓库的能力,因此,这里以使用 TKEStack的镜像仓库:

stage('Push') {
    echo "4.Push Docker Image Stage"
    sh "docker login -u tkestack -p 【访问凭证】 default.registry.tke.com"
    sh "docker push default.registry.tke.com/library/jenkins-demo:${build_tag}"
}

如果只是在 Jenkins 的 Web UI 界面中来完成这个任务的话,这里的 Pipeline 是可以这样写的。但一般推荐使用 Jenkinsfile 的形式放入源码中进行版本管理,这样同时会引发另一个问题:直接把镜像仓库的用户名和密码暴露给别人了,很显然是非常不安全的,更何况这里使用的是 github 的公共代码仓库,所有人都可以直接看到源码,所以应该用一种方式来隐藏用户名和密码这种私密信息,幸运的是 Jenkins 提供了解决方法。

在首页点击 “系统管理” -> Manage Credentials -> 全局凭据 Global credentials (unrestricted) -> 左侧的添加凭据:添加一个 Username with password 类型的认证信息,如下:

输入 docker hub 的用户名和密码,ID 部分输入TKEStack,注意,这个值非常重要,在后面 Pipeline 的脚本中我们需要使用到这个 ID 值。

有了上面的镜像仓库的用户名和密码的认证信息,现在可以在 Pipeline 中使用这里的用户名和密码了:

stage('Push') {
    echo "4.Push Docker Image Stage"
    withCredentials([usernamePassword(credentialsId: 'TKEStack', passwordVariable: 'TKEStackPassword', usernameVariable: 'TKEStackUser')]) {
        sh "docker login -u ${TKEStackUser} -p ${TKEStackPassword} default.registry.tke.com"
        sh "docker push default.registry.tke.com/library/jenkins-demo:${build_tag}"
    }
}

注意这里在 stage 中使用了一个新的函数 withCredentials,其中有一个 credentialsId 值就是刚刚创建的 ID 值,而对应的用户名变量就是 ID 值加上 User,密码变量就是 ID 值加上 Password,然后就可以在脚本中直接使用这里两个变量值来直接替换掉之前的登录镜像仓库的用户名和密码,现在就很安全了,只是传递进去了两个变量而已,别人并不知道真正用户名和密码,只有自己的 Jenkins 平台上添加的才知道。

第五步:更改 YAML

上面已经完成了镜像的打包、推送的工作,接下来应该更新 Kubernetes 系统中应用的镜像版本了,当然为了方便维护,都是用 YAML 文件的形式来编写应用部署规则,比如这里的 YAML 文件:(k8s.yaml)

apiVersion: app/v1
kind: Deployment
metadata:
  name: jenkins-demo
  namespace: kube-ops
spec:
  selector:
    matchLabels:
      app: jenkins-demo
  template:
    metadata:
      labels:
        app: jenkins-demo
    spec:
      containers:
      - image: default.registry.tke.com/library/jenkins-demo:
        imagePullPolicy: IfNotPresent
        name: jenkins-demo
        env:
        - name: branch
          value: 

这个 YAML 文件使用一个 Deployment 资源对象来管理 Pod,该 Pod 使用的就是上面推送的镜像,唯一不同的地方是 Docker 镜像的 tag 不是平常见的具体的 tag,而是一个标识,实际上如果将这个标识替换成上面的 Docker 镜像的 tag,就是最终本次构建需要使用到的镜像,可以使用一个sed命令就可以实现tag的替换:

stage('YAML') {
    echo "5. Change YAML File Stage"
    sh "sed -i 's//${build_tag}/' k8s.yaml"
    sh "sed -i 's//${env.BRANCH_NAME}/' k8s.yaml"
}

上面的 sed 命令就是将 k8s.yaml 文件中的 标识给替换成变量 build_tag 的值。

第六步:部署

Kubernetes 应用的 YAML 文件已经更改完成了,之前手动的环境下,直接使用 kubectl apply 命令就可以直接更新应用,当然这里只是写入到了 Pipeline 里面,思路都是一样的:

stage('Deploy') {
    echo "6. Deploy Stage"
    sh "kubectl apply -f k8s.yaml"
}

这样到这里整个流程就算完成了。

人工确认

理论上来说上面的6个步骤其实已经完成了,但是一般在实际项目实践过程中,可能还需要一些人工干预的步骤。这是因为比如提交了一次代码,测试也通过了,镜像也打包上传了,但是这个版本并不一定就是要立刻上线到生产环境的,可能需要将该版本先发布到测试环境、QA 环境、或者预览环境之类的,总之直接就发布到线上环境去还是挺少见的,所以需要增加人工确认的环节,一般都是在 CD 的环节才需要人工干预,比如这里的最后两步,就可以在前面加上确认,比如:

stage('YAML') {
    echo "5. Change YAML File Stage"
    def userInput = input(
        id: 'userInput',
        message: 'Choose a deploy environment',
        parameters: [
            [
                $class: 'ChoiceParameterDefinition',
                choices: "Dev\nQA\nProd",
                name: 'Env'
            ]
        ]
    )
    echo "This is a deploy step to ${userInput.Env}"
    sh "sed -i 's//${build_tag}/' k8s.yaml"
    sh "sed -i 's//${env.BRANCH_NAME}/' k8s.yaml"
}

这里使用了 input 关键字,里面使用一个 Choice 的列表来让用户进行选择,然后选择了部署环境后,当然也可以针对不同的环境再做一些操作,比如可以给不同环境的 YAML 文件部署到不同的 namespace 下面去,增加不同的标签等等操作:

stage('Deploy') {
    echo "6. Deploy Stage"
    if (userInput.Env == "Dev") {
      // deploy dev stuff
    } else if (userInput.Env == "QA"){
      // deploy qa stuff
    } else {
      // deploy prod stuff
    }
    sh "kubectl apply -f k8s.yaml"
}

由于这一步也属于部署的范畴,所以可以将最后两步都合并成一步,我们最终的 Pipeline 脚本如下:

node('haimaxy-jnlp') {
    stage('Clone') {
        echo "1.Clone Stage"
        git url: "https://github.com/willemswang/jenkins-demo.git"
        script {
            build_tag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
        }
    }
    stage('Test') {
      echo "2.Test Stage"
    }
    stage('Build') {
        echo "3.Build Docker Image Stage"
        sh "docker build -t default.registry.tke.com/library/jenkins-demo:${build_tag} ."
    }
    stage('Push') {
        echo "4.Push Docker Image Stage"
        withCredentials([usernamePassword(credentialsId: 'TKEStack', passwordVariable: 'TKEStackPassword', usernameVariable: 'TKEStackUser')]) {
            sh "docker login -u ${TKEStackUser} -p ${TKEStackPassword} default.registry.tke.com"
            sh "docker push default.registry.tke.com/library/jenkins-demo:${build_tag}"
        }
    }
    stage('Deploy') {
        echo "5. Deploy Stage"
        def userInput = input(
            id: 'userInput',
            message: 'Choose a deploy environment',
            parameters: [
                [
                    $class: 'ChoiceParameterDefinition',
                    choices: "Dev\nQA\nProd",
                    name: 'Env'
                ]
            ]
        )
        echo "This is a deploy step to ${userInput}"
        sh "sed -i 's//${build_tag}/' k8s.yaml"
        sh "sed -i 's//${env.BRANCH_NAME}/' k8s.yaml"
        if (userInput == "Dev") {
            // deploy dev stuff
        } else if (userInput == "QA"){
            // deploy qa stuff
        } else {
            // deploy prod stuff
        }
        sh "kubectl apply -f k8s.yaml"
    }
}

现在可以在 Jenkins Web UI 中重新配置 pipeline-demo 这个任务,将上面的脚本粘贴到 Script 区域,重新保存,然后点击左侧的 Build Now,触发构建,然后过一会儿就可以看到 Stage View 界面出现了暂停的情况:

这就是上面 Deploy 阶段加入了人工确认的步骤,所以这个时候构建暂停了,需要人为的确认下,比如这里选择 “QA”,然后点击“继续”,就可以继续往下走了,然后构建就成功了,在 Stage View 的 Deploy 这个阶段可以看到如下的一些日志信息:

由上面白色一行可以看出当前打印出来了 QA,和选择是一致的,现在去 Kubernetes 集群中观察下部署的应用:

[root@VM-222-139-centos ~]# kubectl get deploy
NAME           READY   UP-TO-DATE   AVAILABLE   AGE
jenkins-demo   0/1     1            0           61s
jenkins2       1/1     1            1           26h
[root@VM-222-139-centos ~]# kubectl get pods
NAME                           READY   STATUS             RESTARTS   AGE
jenkins-demo-b47c7684c-mvpkm   0/1     CrashLoopBackOff   3          62s
jenkins2-7f6cb7d69c-2hnzg      1/1     Running            0          10h
[root@VM-222-139-centos ~]# kubectl logs jenkins-demo-b47c7684c-mvpkm
Hello, Kubernetes!I'm from Jenkins CI!
BRANCH_NAME:

可以看到应用已经正确的部署到了 Kubernetes 的集群环境中了。

Jenkinsfile

这里完成了一次手动的添加任务的构建过程,在实际的工作实践中,更多的是将 Pipeline 脚本写入到 Jenkinsfile 文件中,然后和代码一起提交到代码仓库中进行版本管理。现在将上面的 Pipeline 脚本拷贝到一个 Jenkinsfile 中,将该文件放入上面的 git 仓库中,但是要注意的是,现在既然已经在 git 仓库中了,是不是就不需要 git clone 这一步骤了,所以需要将第一步 Clone 操作中的 git clone 这一步去掉,可以参考:https://github.com/willemswang/jenkins-demo/Jenkinsfile

更改上面的 jenkins-demo 这个任务,点击 Configure -> 最下方的 Pipeline 区域 -> 将之前的 Pipeline Script 更改成 Pipeline Script from SCM,然后根据实际情况填写上对应的仓库配置,要注意 Jenkinsfile 脚本路径: