写点什么

基于阿里云容器的 CI/CD 落地实践

用户头像
LorraineLiu
关注
发布于: 2020 年 10 月 27 日
基于阿里云容器的CI/CD落地实践

背景

大家好,笔者所在的团队当前面临落地公司业务数字化转型的重大任务。我们面临的主要研发挑战是如何快速得迭代出不断新增的开发需求,由于没有太多历史包袱,团队选择的技术栈也是相对成熟与流行的,比如我负责的 Devops 主要是基于阿里云 ACK 容器以及 Jenkins2.0+进行搭建实施。

阿里云 ACK 容器也是基于 K8S1.16.9 封装的云服务,我们选择的是托管版本,即 master 节点托管于阿里云,我们只负责 worker 节点集群的运维与管理。这样做的好处是使团队力量尽可能得集中在业务层面,基础设施层面的运维工作尽量服务化。选择 Jenkins2.0 有诸多好处,比如 Jenkins 是经典的 CI 实施工具平台,开发测试多数熟悉这种使用方式,无需再次学习。2.0 版本以后,引入了声明式的 Jenkinsfile 语法,这种流水线编排格式与基于声明式 API 的 K8S 能够更天然地集成,并且 Jenkins 生态圈的 plugin 十分丰富,基本可以通过配置方式满足团队构建多语言多项目 CICD 任务的使用场景。


核心问题

目前实施 CICD 的业务,基本都要解决以下核心问题。


  • 交付物是什么?

  • 部署环境有哪些?

  • 环境配置信息是什么?

  • 如何自动化地执行构建,部署任务?

  • 如果构建,部署任务执行失败,或者部署环境的应用运行失败,如何感知故障的发生?

交付物定义与结构

交付物对象是在整个 CICD 流程中需要最先确定的实体,是开发跟 devops 之间交互的对接物。开发与 devops 一同参与交付物实体的定义,再由 devops 针对定义提供交付物的通用模版。


  • 交付物定义

团队对于交付物的定义是基于 dockerfile 生成的微服务镜像。

devops 提供了基于阿里云容器镜像服务的私有仓库来存放,如图:


开发基于 dockerfile 提供镜像打包定义,我们以 java 应用为例,镜像内容主要包括基于 openjdk 基础镜像的工作目录定以及指定启动 cmd,示例代码如下:

FROM openjdk:11.0.8WORKDIR /home/demoCOPY target/index-0.0.1-SNAPSHOT.jar app.jarENTRYPOINT ["java","-jar","app.jar"]
复制代码
  • 交付物结构

每个应用部署交付物都归档在一个独立的镜像仓库,镜像仓库命名规范为项目名称.应用服务名称,镜像 tag 的命名规范里应包含每次构建物里所包含的代码变更内容,以 latest commit id 来表示,以及构建发生的时间信息,以 yyyymmddd-hhmm 格式表示时间戳,示例如图:


交付物镜像应是包含了除了环境配置信息以外的应用部署的全部信息的实体。以 java 应用为例,该交付物是基于 openjdk 的 jar 包的镜像实体。环境配置信息可以由环境变量传递并改写。


部署环境与配置

  • 部署环境

CICD 理念解决的核心问题之一是在如何在多环境下部署同一交付物。典型的部署环境主要有 4 种:

  • dev 开发环境

交付物生成的环境。在开发环境内,交付物第一次生成,需要经过必要的单元测试通过率以及代码安全性检查等步骤,该环境的交付物是可提测的部署物。

  • test 测试环境

交付物开始集成测试以及回归测试的环境。该环境的交付物是可上生产的部署物。

  • stage 预发布环境

预发布环境的使用场景会依据需求而有所不同。有些公司会将预发布环境用作 demo 环境作为 poc 功能展示;有些公司会在预发布环境中进行流量压力测试,确保上生产之前的服务负载与资源配置相匹配;有些公司做蓝绿发布或者 A/B 测试时,则会利用 stage 环境来做流量切换。无论 stage 环境如何被使用,stage 部署环境都需与生产部署环境配置保持一致。

  • prod 生产环境

生产发布环境是交付物最终运行环境,对用户提供服务。


  • 环境配置

环境配置信息是 CICD 过程中,独立与交付物,但依赖于部署环境的一系列环境变量信息。基于 K8S 集群部署时,实现的主要方式是 ConfigMap 以及 Secret 资源对象。


构建自动化

我们有了 Dockerfile 来描述交付物的定义,简单来说只要 docker build 然后 docker push 到私有镜像仓库就完成了构建。基于 Jenkins 搭建自动化的构建任务就是将这个过程自动化。后文实施部分会详细讲述。


  • 构建任务自动化主要考虑的问题有

  • 当 main/master branch 有代码提交 commit 生成时,如何自动触发构建任务执行?

  • 不同语言应用,比如 java 应用或者前端应用的构建流程是怎样的?包含哪些步骤?

  • 生成的交付物镜像如何成功放至到阿里云容器镜像服务的私有仓库里?

  • 交付物传输过程如何确保网络传输的安全性与可靠性?


部署自动化

我们的部署环境是在阿里云容器集群中,当前每个部署环境通过命名空间来进行资源隔离,后续还会做资源隔离升级。目前对于在 K8S 集群中的应用部署,主流的解决方案有 Helm 和 Kustomize,我们最后选择了 HelmV3 来实施。Helm 与 Kustomize 之间的选择是另一个有意思的话题,不在本章展开。基于 Helm 的部署自动化简单来说就是构建一个能够运行 helm Install 指令的 jenkins 任务。后文实施部分会详细讲述。


  • 部署自动化主要考虑的问题有

  • 如何获取到交付物?

  • 部署的是哪个交付物?要部署的目标环境是哪个?

  • 如何定义部署成功?

  • 部署失败了,如何感知到故障?如何快速定位到问题?

  • 如何实现回滚操作?


事件监控与告警机制

CICD 的目标是尽可能的自动化全流程,降低人为参与的程度。当自动化程度越高,对于自动任务的故障发现与告警就越有必要。我们的服务部署在阿里云容器集群中,基于 Jenkins2.0 来搭建自动化 CI/CD 的任务,构建部署依赖与 Jenkins 任务执行是否成功,以及集群中的资源按照 Helm 的定义是否如期更新运行。CI/CD 相关的事件监控围绕着集群资源事件监控以及 Jenkins 任务监控两方面来进行。

  • 阿里云容器集群事件监控与告警

  • 阿里云服务提供了事件监控与告警服务,可以直接配置使用,如图所示,该示例为阿里云集群服务/运维管理/事件列表提供的事件监控仪表盘服务。我们可以轻松得到整个集群资源运行的情况。

  • 进入“告警配置”服务就能配置基本事件的告警,集群初始化了很多基本事件的告警配置,比如 Pod/Node OOM;Pod 启动失败,或者资源不足,无法调度等。可以依据业务需求,定制化告警事项。笔者目前采用的是使用默认配置的告警模版,对发生频次做了一定容忍,通过邮件方式进行告警。


  • Jenkins 任务失败自动发送邮件告警

  •  E-mail Notification Plugin 可以帮助实现 Jenkins 服务对于任务完成自动发送邮件的功能。

  • 首先在 Jenkins/configuration/Extended E-mail Notification 配置 SMTP 邮件服务信息,我们使用的是阿里云邮件推送服务, 确保填写了正确的 smtp server, smtp port,如果有 smtp username/passwrod,也需要正确填写。


  • 其次在 Jenkinsfile 里声明使用 email extension plugin,对于构建任务失败自动发送邮件的代码块如下:

emailext (                to: 'XXX@example.com',                subject: "FAILED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]'",                body: """<p>FAILED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]':</p>                <p>Check console output at &QUOT;<a href='${env.BUILD_URL}'>${env.JOB_NAME} [${env.BUILD_NUMBER}]</a>&QUOT;</p>""",                recipientProviders: [[$class: 'DevelopersRecipientProvider']]            )
复制代码


实施

基于 Dockerfile/Jenkinsfile 构建 CI 流水线

  • 交付物由 Dockerfile 定义,每个应用服务的根目录下都应该有一个 Dockerfile 文件,定义了该服务的构建过程。本节里我们举 java 微服务来说明,这也是我们后端微服务的构建流水线。

  • Java 微服务

  • Java 应用的构建逻辑定义在 Jenkinsfile 里,基于 maven 实现。Dockerfile 模版只定义了如何调用构建好的 jar 包,即部署指令。这样做的主要目的是尽可能缩小镜像,降低网络数据传输的负载。

  • 声明式 Jenkinsfile 的好处是可以通过代码方式定义与管理流水线逻辑。基于代码就拥有了版本管理的能力。

  • 构建 Pipeline 主要包含了以下步骤:

  • scm 获取项目源代码

  • 基于 maven 的构建,生成可部署的 jar 包

  • 基于 maven 的单元测试

  • 基于 SonarQube 的代码检查

  • 基于 kaniko 的云原生方式,生成 image,并推送至阿里云私有镜像仓库

  • 构建 Pipeline 主流程之外的后置步骤是构建任务执行完成后的 downStream 任务,链接的是通用部署任务,这种结构解耦了构建与部署逻辑,可以使不同的构建任务复用同一个部署任务。


def branch_namedef revisiondef registryIp = "registry-vpc.cn-shanghai-finance-1.aliyuncs.com"def app = "XXX"
pipeline { agent{ node{ label 'slave-java' // Jenkins Slave Pod Template } } stages { stage ('Checkout') { steps { script { def repo = checkout scm revision = sh(script: 'git log -1 --format=\'%h.%ad\' --date=format:%Y%m%d-%H%M | cat', returnStdout: true).trim() branch_name = repo.GIT_BRANCH.take(20).replaceAll('/', '_') if (branch_name != 'master') { revision += "-${branch_name}" } sh "echo 'Building revision: ${revision}'" // 获取代码并生成镜像tag(latest-commit-id+timestanp+branch) } } } stage('Compile') { steps { container("maven") { sh 'mvn -B -DskipTests clean package' // 基于maven的构建步骤 } } } //unit test 测试部署 stage('Unit Test') { steps { container("maven") { sh 'mvn test org.jacoco:jacoco-maven-plugin:0.7.3.201502191951:prepare-agent install -Dmaven.test.failure.ignore=true' } } } // 上传Jacoco检测结果 stage('JacocoPublisher') { steps { jacoco() } } stage('Build Artifact') { steps { container("maven") { sh 'chmod +x ./jenkins/scripts/deliver.sh' sh './jenkins/scripts/deliver.sh' } } } stage('SonarQube Analysis'){ environment { scannerHome = tool 'SonarQubeScanner' } steps { withSonarQubeEnv('sonar_server') { sh "${scannerHome}/bin/sonar-scanner" } } } // 添加stage, 运行容器镜像构建和推送命令 stage('Image Build and Publish for Dev Branch'){ when { not { branch 'master' } } steps{ container("kaniko") { sh "kaniko -f `pwd`/Dockerfile -c `pwd` --destination=${registryIp}/xxxx/xxx.${app}:${revision} --skip-tls-verify" } } } // 添加stage, 运行容器镜像构建和推送命令 stage('Image Build and Publish for Master Branch'){ when { branch 'master' } steps{ container("kaniko") { sh "kaniko -f `pwd`/Dockerfile -c `pwd` --destination=${registryIp}/xxxx/xxx.${app} --destination=${registryIp}/xxxx/xxx.${app}:${revision} --skip-tls-verify" } } } } post { always { echo 'This will always run' } success { script { build job: '../xxx.app.deploy/master', parameters: [string(name: 'App', value: String.valueOf(app)), string(name: 'Env', value: 'dev-show'), string(name: 'Tag', value: String.valueOf(revision))] } } failure { emailext ( to: 'XXX@example.com', subject: "FAILED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]'", body: """<p>FAILED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]':</p> <p>Check console output at &QUOT;<a href='${env.BUILD_URL}'>${env.JOB_NAME} [${env.BUILD_NUMBER}]</a>&QUOT;</p>""", recipientProviders: [[$class: 'DevelopersRecipientProvider']] ) } unstable { echo 'This will run only if the run was marked as unstable' } changed { echo 'This will run only if the state of the Pipeline has changed' echo 'For example, if the Pipeline was previously failing but is now successful' } }}
复制代码
  • 构建任务一般是根据主分支提交的代码自动触发的任务。为了实现 autoTrigger,我们需要使用 webHook 连接 SCM 服务和 Jenkins 服务,我们使用的 SCM 服务是 Bitbucket,通过使用hook插件实现了 autoTrigger。


  • 基于阿里云容器服务的部署环境属于 VPC 内,即我们使用的容器镜像服务是 VPC 内的私有镜像服务,VPC 外部无法访问。K8S 集群也属于该 VPC 内的资源,故集群内部的 CI Jenkins Slave Pod 以及目标部署 Pod 服务都在该 VPC 的内网内,与私有化的容器镜像服务直接局域网连接,确保了数据传输的安全性与可靠性。


基于 Helm/Jenkinsfile 构建 CD 流水线

  • 在 K8S 上部署一个应用,传统方式一般利用 kubectl 创建一系列的资源对象(deployment,configMap,secret,serviceAccount, service,ingress 等)。在 CICD 部署流程中,涉及同一部署物部署到多个环境内,即部署发生多次。举个例子,部署在开发和测试环境的两个 Deployment 对象,结构基本一致,只有少数属性的赋值依据环境而有所不同。CICD 软件实践里有一个重要理念是部署可以重复,确保在各个环境的部署的动作是一致的,以避免在发布流水线中引入差异。Helm 是一种适应上述需求的模版式解决方案。Helm 的介绍不在本章展开,有兴趣学习的读者可以参考。https://whmzsu.github.io/helm-doc-zh-cn/

  • helm 提供了 chart 包(一种可以封装 K8S 资源对象为模版文件的集合)和 values.yaml(属性参数的集合)结构,基于 Go Template 语法,解耦了 manifest 里属性值与 K8S 资源对象模版结构。使得一组对应用部署所需资源对象的创建可以通过模版定义加赋值的方式,复用到多个环境,重复部署,解决了 K8S 资源对象管理的问题。

  • 下来以 Java 微服务的 helm/Chart 包举例来说明我们是如何生成应用部署物的

  • helm 包初始化可以使用 helm create Name [flags]


部署物 helm 包的目录结构如下:

  • 根目录下的 values.yaml 是所有环境公共的属性集合,比如关于创建 serviceAccount,rbac 相关的属性信息,dev/test/stage/production.yaml 是与部署环境有关的属性集合。

  • 执行 helm install 命令来部署当前 Helm 包模版,例如


 helm upgrade index ./helm/ -i -nbss-dev -f ./helm/values.yaml -f ./helm/dev.yaml --set 'image.tag=latest' --set 'image.repo=bss.index' --set 'ingress.hosts.paths={/index}'
复制代码
  • 前文提到了我们使用声明式 Jenkinsfile 语法构建部署流水线逻辑,与传统 groovy 不一样的是,我们可以通过声明 pipeline,agent/node,stages/step 等对象,直接将部署流程的定义声明。以部署基于 helm 的 jar 包应用为例,部署物为在阿里云私有镜像仓库的镜像文件,上文例子所示。部署流程包括两个步骤:1. 获取 helm 代码 2. 执行 helm install,具体如下:


def branch_namedef revisiondef registryIp = "registry-vpc.cn-shanghai-finance-1.aliyuncs.com"
pipeline { agent{ node{ label 'slave-java' // Jenkins Slave Pod Template } } parameters { choice(name: 'App', choices: ['111', '222', '333','444'], description: '选择部署应用') choice(name: 'Env', choices: ['dev', 'stage', 'test',], description: '选择部署环境') string(name: 'Tag', defaultValue: 'latest', description: '请输入将要部署的构建物镜像Tag') } stages { stage ('CheckoutHelm') { steps { script { def repo = checkout scm revision = sh(script: 'git log -1 --format=\'%h.%ad\' --date=format:%Y%m%d-%H%M | cat', returnStdout: true).trim() branch_name = repo.GIT_BRANCH.take(20).replaceAll('/', '_') if (branch_name != 'master') { revision += "-${branch_name}" } } } }
stage ('Deploy') { steps { container('helm-kubectl') { sh "chmod +x ./helm/setRevision.sh" sh "./helm/setRevision.sh ${revision}" sh "helm upgrade -i ${params.App} ./helm/ -nxxx-${params.Env} -f ./helm/${params.Env}.yaml --set-file appConfig=./appConfig/${params.Env}/${params.App}.yml --set image.tag=${params.Tag} --set image.repo=bss.${params.App} --set ingress.hosts.paths={/${params.App}}" } } } } post { always { echo 'This will always run' } success { echo 'This will run only if successful' } failure { emailext ( to: 'XXX@example.com', subject: "FAILED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]'", body: """<p>FAILED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]':</p> <p>Check console output at &QUOT;<a href='${env.BUILD_URL}'>${env.JOB_NAME} [${env.BUILD_NUMBER}]</a>&QUOT;</p>""", recipientProviders: [[$class: 'DevelopersRecipientProvider']] ) } unstable { echo 'This will run only if the run was marked as unstable' } changed { echo 'This will run only if the state of the Pipeline has changed' echo 'For example, if the Pipeline was previously failing but is now successful' } }}
复制代码


经验总结与展望

流水线自助化改进

  • 笔者团队面对的业务架构是不断增长的微服务集群。基于此,对于 CICD 的自动化要求以及发布频率不断提出了新的挑战。原先我们的做法是每个微服务拥有独立的 helm 部署包,但是由于快速增加的微服务数量,对于快速注册新服务到现有 CICD 流水线中,产生了开发快速构建部署服务的需求。

  • 面临的问题主要是 helm 部署模版是以应用部署所需的资源对象构建为单元,在不同部署环境做配置。每个应用都会创建一套 helm 部署包以及的独立的构建和部署任务。当越来越多的微服务部署需求不断增加时,构建 helm 包以及相应的构建部署任务的配置工作量就会成为 CICD 流程的效率瓶颈。

  • 解决方案是抽象一个公共 helm 部署包模版,将各个应用部署时定制化的属性信息集合从原有 helm 包结构中再抽象出来,以 AppConfig File 方式独立成一层配置信息,例如:


  • 这里的模版抽象实现主要是依赖 Helm 命令里-f 传入 values.yaml 时,可以同时-f 多个属性集合文件,并且位于后面的-f 的文件可以覆盖前面的属性参数值。如果定制化配置信息文件不是 Go template 可以直接使用的格式,可以考虑使用 flag --set-file 直接将 Config 文件里的内容写入某个上层的属性参数。

安全问题

  • 将开发,测试环境都搬到公有云上,访问安全是一个不可忽略的问题。我们的交付物,部署流水线以及目标部署集群都基于一个 VPC 下,从网络传输角度,数据交互在一个局域网内,是相对安全的公有云网络环境。除此之外,为了保证开发环境,测试环境完全的私有化,在微服务应用部署 ingress 资源对应的 slb 上我们也设置了相应的访问控制,只能对公司内部的用户开放网络访问。后续对于公网访问的服务以及外部部署环境和 CICD 服务之间的数据传输会考虑使用阿里云提供的 KMS 等数据传输保密服务来保证。


发布于: 2020 年 10 月 27 日阅读数: 491
用户头像

LorraineLiu

关注

坐标Fintech,立志于成为K8S玩家。 2019.04.20 加入

我的博客即将同步至 OSCHINA 社区,这是我的 OSCHINA ID:刘晨Lorraine

评论

发布
暂无评论
基于阿里云容器的CI/CD落地实践