我们隔一流的软件生产工艺还有多远?在距离15000公里外,Amazon一年可以进行5000万次部署,在这一边某电商平台的研发部门里,让他们引以为傲的是他们正在进行“敏捷”开发模式,并对外号称他们是以每周为迭代来进行升级。时间是定在周四(因为这样如果出现问题,周五还可以修复,不用周六加班),但是周四的晚上,开发/测试/还有产品的负责人是妥妥的要留下来了奋斗到天明了,如果运气好的话还可以在 12点之前回家。运气这种事情,总是不太好说。
这是我们曾经经历的痛, 由于缺少自动化测试以及完整的上线发布流程,每次一上线,总能折腾个4到5小时。所以后来在我们自己实现这套新零售的saas系统的时候,从一开始我就在思考如何避免这个问题。我们采用了微服务架构,由30多个微服务组成, 部署在K8S上, 测试环境与生产环境都是借助于gitlab ci来完成的,并且同时可以支持腾讯和阿里云的k8s容器服务。这个花了3天调研和实施出来的持续集成方案在时间收益上已经给我们带来了超过 20倍的回报 。
微服务
微服务存在着多个服务独立部署的情况,在没有k8s之前需要自己实现一套完整的持续部署工具,这个复杂度及成本可以说80%的公司都承受不起。但是微服务部署在 k8s上之后一切就变的简单的多了。
单个服务在k8s中的部署可以用 kubectl set image的语句在ci job中实现
kubectl --record deployment.apps/nginx-deployment set image deployment.v1.apps/nginx-deployment nginx=nginx:1.9.1
如果是30多个甚至更多的服务,我们把所有的资源定义文件存放在一个单独的代码仓库中进行维护和版本管理会更合适一些。在没有helm之前,我们可能通过 kubectl apply -f ./ 的方式基于整个文件夹来创建和更新 k8s资源。即使这样,我们在不同测试环境与生产环境由于不同的配置需要有两个文件夹来保存资源定义文件。
Helm
helm可以将多个k8s的资源定义文件打包在一起进行整体的部署、更新,就像一个应用程序一样。
这种场景就特别适合微服务,我们可以将所有的服务以及中间件、数据库、证书等资源定义在一个helm package下来进行部署和更新。
一个helm package的构成主要由模板和参数来构成,模板就是 k8s的资源定义文件,在模板中可以引用外部的参数,我们一组微服务的应用可以用同一个包只需要替换不同的参数即可。
我们以一个简单的单体应用来举例,它包括一个api, 一个mysql数据库,以及一个ingress配置将api就暴露在k8s集群之外。在本地安装好helm client之后可以使用helm create [name]来创建一个package。
helm create Helm
在templates中新建api-deploy.yaml, ingress.yaml, mysql-svc.yaml,这些文件的内容和我们平台创建k8s资源的定义文件是一样的,只不过我们在这里可以使用helm提供的参数。
比如命名空间我们就可以这样来使用
kind: Service apiVersion: v1 metadata: name: {{ .Values.image.api.name }} namespace: {{ .Values.namespace }} spec: {{ if .Values.image.api.nodePort }}type: NodePort{{ end }} ports: - port: 80 targetPort: 80 {{ if .Values.image.api.nodePort }}nodePort: {{ .Values.image.api.nodePort }}{{ end }} selector: name: {{ .Values.image.api.name }}
在模板中还支持条件判断,比如在测试环境我们可以配置nodePort来暴露服务,而生产环境中则不支持。
在values.yaml中我们定义以下内容供测试环境使用,在生产中可以将namespace改成生产环境对应的命名空间名以及移除nodePort节点即可。
namespace: hunterpro image: api: name: hunterpro-api version: 1.0.0.2991 nodePort: 31013
之后我们可以使用helm install -n [name] ./helm 来将这个helm package部署到k8s集群中 。 ./helm为包定义目录。helm 所连接的k8s集群为kubectl的配置。
Helm package Chartmuseum
一个helm的包存放在一个文件夹内,同时我们还可以使用 helm pakcage将它打包成一个文件来方便与其它人共享。同时也可以像上传docker我镜像一样上传helm package,并且同样可以在仓库上保存不同的版本。Chartmuseum就是一个开源的 helm package仓库服务。
我们可以使用docker将它快速安装
docker run --rm -it \ -p 8080:8080 \ -v $(pwd)/charts:/charts \ -e DEBUG=true \ -e STORAGE=local \ -e STORAGE_LOCAL_ROOTDIR=/charts \ chartmuseum/chartmuseum:v0.8.1
以上我们就可以拿到一个远程 helm package repository的地址,只需要将它加入本地的repo list中即可
helm repo add [name] [url]
官方还提供了helm push插件,让我们可以轻松地将本地的helm package推送到远程的仓库
$ helm push mychart/ [repo name] Pushing mychart-0.3.2.tgz to [repo name]... Done.
完整实践
有了对 helm以及helm package repository的初步了解,我们就可以进入到我们整个ci方案了。
1. 开发提交代码到dev分支
2. 触发项目dev构建流水线 gitlab ci开始进行代码的构建,使用项目内dockerfile来构建 docker镜像
3. 镜像构建成功之后推送到镜像仓库 (我们根据不同的分支会把镜像分别推到阿里或者镜像云)
4. 触发buildscript dev构建流水线 (传送参数 :当前版本以及当前更新服务名称)
5. build script 下载最新代码并将helm package的参数内的对应服务的镜像版本更新至当前版本
6. 提交更改之后的代码
7. helm package 并且helm push将最新的包推送到远程镜像
8. 用最新的 helm package来更新开发的集群
我们使用gitlab ci来做持续集成,如果不了解gitlab ci可以查看这篇之前的文章。
GitLab CI 自动部署netcore web api 到Docker
.Net & Docker(二)5分钟快速用Docker部署你自己的GitLab
当我们的gitlab runner配置好之后,由于涉及到k8s我们还需要做以下事情。
- 安装kubectl 并配置连接到集群
- 安装helm client (helm client会直接使用kubectl 的连接操作对应集群)
- 安装 helm push 插件
- 由于我们在流水线执行过程中更新代码并推送用到了python脚本,所以我们需要安装python3.6
安装kubectl
与k8s相关的学习最大的问题是它有比较多新的概念,刚开始学习会比较难。而第二大问题就是那堵墙,在很多情况下会让我们直接想放弃。在安装kubectl 的时候,如果是 centos我们可以使用阿里的镜像:
cat <<EOF > /etc/yum.repos.d/kubernetes.repo [kubernetes] name=Kubernetes baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64/ enabled=1 gpgcheck=1 repo_gpgcheck=1 gpgkey=https://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg https://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg EOF
然后再安装kubectl
yum install -y kubectl
安装helm
centos 可以使用snap来安装并添加阿里的稳定仓库源来进初始化,默认使用谷哥的稳定源会失败 。
sudo snap install helm --classic helm init --upgrade -i registry.cn-hangzhou.aliyuncs.com/google_containers/tiller:v2.14.1 --stable-repo-url https://kubernetes.oss-cn-hangzhou.aliyuncs.com/charts https://yq.aliyun.com/articles/159601
安装python和 PyYAML库
我们上面提到我们要通过 buildscript的构建流水线去更新helm pakcage内某个服务的镜像版本
image: auctionapi: name: auction-api nodePort: 31100 version: 1.0.2532 deliveryapi: name: delivery-api nodePort: 31049 version: 1.0.2542 fundapi: name: fund-api nodePort: 31034 version: 1.0.2538 gatewaymp: name: gateway-mp-api nodePort: 31039 version: 1.0.2544
比如我们的 values.yaml 参数配置是这样的。当我们提交gateway-mp-api 之后,当前的版本号会升级主为 1.0.2545 。gateway-mp-api的构建流水线会将版本号为 1.0.2545的镜像推到镜像仓库,接下来我们要做的就是通过 helm upgrade 来更新我们在k8s中的服务。
gitlab ci中可以通过 web hook 的方式触发另一个仓库的ci,所以我们在这里每一个 api推送完镜像之后都会触发 buildscript的更新,并传入参数:当前服务名称(对应values.yaml中的 服务key) 和最新版本号。
然后借助一段python脚本来更新values.yaml, 下面的代码用到了python的 yaml库,可以比较方便的操作yaml格式的文件。
import sys import yaml with open("values.yaml") as f: value = yaml.load(f) value['image'][sys.argv[1]]['version'] = sys.argv[2] with open("values.yaml","w") as f: yaml.dump(value, f)
buildscript 的.gitlab-ci.yaml文件 script
script: - helm init --client-only --stable-repo-url https://kubernetes.oss-cn-hangzhou.aliyuncs.com/charts - helm repo add $helmRepoName $helmRepoUrl - cd BuildScript/dev-wotui/wotui-dev - python3.6 update.py $image $version - git add . - git commit -m "upgrade to $version" || true - git push origin wotui/develop - cd ../ - helm push ./wotui-dev $helmRepoName -v $version - helm repo update - helm upgrade $helmReleaseName $helmPackageName
所完成的步骤包括:
- 初始化helm 仅客户端
- 添加远程 helm 仓库
- 更改 helm chart values.yaml 对应的服务镜像版本
- 提交buildscript 代码
- 推送 helm package
- 更新微服务
服务项目的构建流水线
每一个服务自己的gitlab-ci文件中只需要完成构建自己的镜像并推送到镜像仓库,之后再用curl发起buildscript 项目的构建并传入对应参数即可。
stages: - image - deploy variables: versionNo: $MAIN_VERSION.$SECONDARY_VERSION.$CI_PIPELINE_ID registry: registry-vpc.cn-shanghai.aliyuncs.com/ registryUser: ******** registryPwd: ******** repository: namespace/delivery-api docker image: stage: image tags: - wotui only: - wotui/develop script: - docker build -t "$registry$repository:$versionNo" ./Collectin.Delivery.API - docker login -u $registryUser -p $registryPwd $registry - docker push "$registry$repository:$versionNo" deploy: stage: deploy variables: GIT_STRATEGY: none tags: - wotui only: - wotui/develop script: - curl -X POST -F token=${PUBLISH_TRIGGER_TOKEN} -F ref=wotui/develop -F "variables[version]=$versionNo" -F "variables[image]=deliveryapi" http://code.collectin.cn/api/v4/projects/50/trigger/pipelin
以上,当你提交代码到服务的分支,接下来就是视频中全自动的编译,打包,部署流程。
关于微服务与K8S
只要找到了合适的方法,微服务就没有那么复杂 。即使不是粒度非常细的微服务,哪怕是粗粒度的服务,同样可以借用于K8S来进行运维管理。 在现在它可以节省大量的运维操作时间,在未来它也可以很好地适应业务快速扩展。 随着企业对于开发人员的要求不断增高,如果你希望在未来的两到三年晋升成为架构师,那么微服务架构和K8S是你不得不掌握的必要技能。
Jesse腾飞