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

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
容器服务 Serverless 版 ACK Serverless,317元额度 多规格
容器服务 Serverless 版 ACK Serverless,952元额度 多规格
简介: 团队面对的业务架构是不断增长的微服务集群,基于此,对于CICD的自动化要求以及发布频率不断提出了新的挑战。

作者个人介绍
刘晨 Lorraine

坐标Fintech,精通持续集成与发布,曾具有全平台100+应用持续部署持续发布实战经验,现在立志于成为K8S玩家。

image.png

背景

大家好,笔者所在的团队当前面临落地公司业务数字化转型的重大任务。我们面临的主要研发挑战是如何快速得迭代出不断新增的开发需求,由于没有太多历史包袱,团队选择的技术栈也是相对成熟与流行的,比如我负责的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提供了基于阿里云容器镜像服务的私有仓库来存放,如图:
image.png
开发基于dockerfile提供镜像打包定义,我们以java应用为例,镜像内容主要包括基于openjdk基础镜像的工作目录定以及指定启动cmd,示例代码如下:

FROM openjdk:11.0.8
WORKDIR /home/demo
COPY target/index-0.0.1-SNAPSHOT.jar app.jar
ENTRYPOINT ["java","-jar","app.jar"]

交付物结构

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

image.png
交付物镜像应是包含了除了环境配置信息以外的应用部署的全部信息的实体。以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任务监控两方面来进行。

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

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

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

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

首先在Jenkins/configuration/Extended E-mail Notification配置SMTP邮件服务信息,我们使用的是阿里云邮件推送服务, 确保填写了正确的smtp server, smtp port,如果有smtp username/passwrod,也需要正确填写。
image.png
其次在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_name

def revision
def 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。

image.png

基于阿里云容器服务的部署环境属于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]

image.png
部署物helm包的目录结构如下:
image.png
根目录下的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_name
def revision

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

安全问题

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

相关实践学习
通过容器镜像仓库与容器服务快速部署spring-hello应用
本教程主要讲述如何将本地Java代码程序上传并在云端以容器化的构建、传输和运行。
Kubernetes极速入门
Kubernetes(K8S)是Google在2014年发布的一个开源项目,用于自动化容器化应用程序的部署、扩展和管理。Kubernetes通常结合docker容器工作,并且整合多个运行着docker容器的主机集群。 本课程从Kubernetes的简介、功能、架构,集群的概念、工具及部署等各个方面进行了详细的讲解及展示,通过对本课程的学习,可以对Kubernetes有一个较为全面的认识,并初步掌握Kubernetes相关的安装部署及使用技巧。本课程由黑马程序员提供。 &nbsp; 相关的阿里云产品:容器服务 ACK 容器服务 Kubernetes 版(简称 ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情:&nbsp;https://www.aliyun.com/product/kubernetes
相关文章
|
Devops 开发工具 git
【devops】二、Code阶段工具——容器部署Gitlab
【devops】二、Code阶段工具——容器部署Gitlab
124 0
|
Cloud Native 关系型数据库 程序员
使用Docker部署自动化CI/CD平台Drone
[Drone](https://drone.io) 是一个现代化的持续集成平台,能够使用强大的云原生管道引擎自动化他们的构建、测试和发布工作流程,让我们不再关注程序如何发布而是如何去实现,去更好的实现。
使用Docker部署自动化CI/CD平台Drone
|
19天前
|
jenkins 测试技术 持续交付
Docker最佳实践:构建高效的CI/CD流水线
【10月更文挑战第17天】在现代软件开发实践中,持续集成(Continuous Integration, CI)和持续部署(Continuous Deployment, CD)已成为提高开发效率和软件质量的重要手段。Docker作为一种容器技术,为构建一致且隔离的开发环境提供了强有力的支撑。本文将探讨如何利用Docker来优化CI/CD流程,包括构建环境的标准化、镜像管理以及与CI/CD工具(如Jenkins、GitLab CI)的集成。
40 5
|
3月前
|
测试技术 持续交付 开发者
使用Docker构建CI/CD流程:从理论到实践
【8月更文挑战第2天】使用Docker构建CI/CD流程,可以显著提高软件开发的效率和质量。通过容器化技术,开发者可以确保环境的一致性,快速部署和测试应用,并减少人为错误。结合合适的CI/CD工具和最佳实践,可以进一步加速软件交付过程,提高用户满意度。希望本文能为开发者在构建基于Docker的CI/CD流程时提供有价值的参考。
|
6月前
|
运维 Kubernetes PHP
构建高效自动化运维体系:基于容器技术的CI/CD实践深入理解PHP中的命名空间
【5月更文挑战第27天】在现代软件交付过程中,持续集成(CI)与持续部署(CD)已成为提升开发效率、保障产品质量的重要手段。本文旨在探讨如何利用容器技术实现CI/CD的自动化流程,从而构建一个高效的自动化运维体系。通过分析容器技术的核心优势和CI/CD流程的关键要素,我们提出了一种结合Docker、Kubernetes等工具的实践方案,并详细阐述了从代码提交到最终部署的全过程自动化实现方法。 【5月更文挑战第27天】在现代PHP开发中,命名空间是一个不可或缺的功能,它解决了代码库增长时可能出现的类名和函数名冲突问题。本文将深入探讨PHP命名空间的核心概念、实现原理及其在实际项目中的应用,帮助
|
存储 前端开发 jenkins
实践:部署Jenkins服务并开发MERN应用的CI/CD构建管道
为了解决这个问题,我们可以创建一个 CI/CD流水线。因此,每当您添加功能或修复错误时,都会触发此管道。这会自动执行从测试到部署的所有步骤。
280 0
|
运维 监控 安全
Serverless CI/CD实践案例
CI/CD 是一种通过在应用开发阶段引入自动化来频繁向客户交付应用的方法。CI/CD 的核心概念是持续集成、持续交付和持续部署。作为一个面向开发和运营团队的解决方案,CI/CD 主要针对在集成新代码时所引发的问题。具体而言,CI/CD 可让持续自动化和持续监控贯穿于应用的整个生命周期(从集成和测试阶段,到交付和部署)。这些关联的事务通常被统称为“CI/CD 管道”,由开发和运维团队以敏捷方式协同支持。
237 0
|
jenkins Java 应用服务中间件
CI/CD + docker 综合实战
本篇记录了一些与Harbor、Jenkins的相关操作。
180 0
CI/CD + docker 综合实战
|
Kubernetes 监控 Java
基于阿里云容器的 CI/CD 落地实践
团队面对的业务架构是不断增长的微服务集群,基于此,对于CICD的自动化要求以及发布频率不断提出了新的挑战。
7816 0
基于阿里云容器的 CI/CD 落地实践
下一篇
无影云桌面