前言
想要让CI/CD系统变得功能强大、高效运行、任务执行成功率高,需考虑以下3大要素;
1.CI/CD任务承载和执行工具
随着DevOps文化盛行,各种承载和执行CI/CD任务的工具的也纷纷涌现,例如
- JenKins
- GitLabCI/CD
- ArgoWorkflows
- ArgoCD
2.CI/CD制品仓库
制品库在CI/CD系统中充当中间存储的枢纽,将CI/CD系统中每一步骤的输出成果保存起来 ,为下一步提供标准化接口。
制品仓库可以保存
- 代码
- 开发依赖
- 构建依赖(系统软件包、语言依赖库)
- 构建成果(二进制文件、JAR包、前端静态资源、Helm Chart、容器镜像)
等CI/CD任务执行时需要输入/输出的数据,其中JFrog Artifactory就是1个通用的制品仓库;
3.流水线任务步骤编排机制
流水线编排是指在软件交付过程中,将从代码提交到应用上线的各个步骤(如代码拉取、构建、测试、部署等)通过1个自动化流程串联起来,并按照一定顺序或逻辑执行,以实现高效、稳定、可控的交付过程。
自动化串联流程:是当前步骤执行完毕,上传输出(成果制品)到制品库后,通知一下next步骤,你该执行了....去X制品库下载我的输出x制品,制品ID是X-x;
任务步骤拆分
如上图所示:1个CI/CD流水线任务能被拆分为多个步骤(如代码拉取、构建、测试、部署等)
- ArgoWorkflow的Template
可以让流水线更加易于灵活扩展,提高任务的可复用性;
- ArgoWorkflow TemplateRef
任务步骤触发机制配置
以上我们划分好了任务的步骤,可这些步骤如何启动?触发器的核心作用就是用来启动工作流;
在ArgoWorkflows中,1个Workflow任务的多个步骤运行在不同的Pod中,它们之间如何通信;
- GitLab/MinIO/Harbor/Artifactory的WebHook功能
- 事件触发
- curl/RPC网络请求触发
任务步骤输入输出配置
当步骤触发之后首先应该知道自己的输入是什么,执行完任务逻辑,还要知道自己的输出是什么?
流水线任务就像1根管道,每个步骤都需要明确自己的输出和输出,任务步骤如何知道自己的输入和输出呢?
人工配置下载和上传
在制品比较少的情况下可以在当前步骤,手动配置输出和输出
前端UI选择制品
在代码库、流水线、制品库、制品比较多的情况下,用户需要通过前端UI选择制品,就需要把关联关系存储到数据库中,以OAM应用交付管理模式为例:
创建应用时: 在数据库中建立应用、组件、运维特征的关联关系
创建代码库时:在数据库中建立应用、组件、代码库的关联关系
创建流水线时:在数据库代码库、流水线的关联关系
CI流水线执行: 从代码库下载源码构建为制品,把制品上传到关联的制品库
CD流水线执行:从当前组件关联的制品库中,选择下载的不同制品ID,发布到dev/test/ontest/prod环境
总结
与微服务架构思想类似,流水线编排系统即强调任务拆分隔离,也强调系统化管理分离的流水线,也要确保协同运作。
每个企业实现CI/CD方式都不太一样,需要根据当前发展阶段、业务体量、技术栈、部署环境,打造适合自身的CI/CD系统;
一、DevOps
DevOps是1个更宏观的文化和理念,强调开发与运维之间的紧密协作和自动化工具的广泛应用;(微服务-理论概念)
CI/CD是DevOps实践中的技术实现部分;(SpringCloud-技术实现)
1.传统应用发布模式面临的挑战
1.1.传统应用发布模式
传统应用发布模式下开发、运维、测试间的职责划分;
开发团队
在开发环境中完成软件开发和单元测试,测试通过后提交代码到代码版本管理仓库;
运维团队
把应用部署到测试环境,供QA团队测试,测试通过后部署到生成环境;
测试团队
进行测试,测试通过后通知运维部署人员发布代码到生产环境;
1.2.传统应用发布模式面临的挑战
Bug发现不及时
很多错误在项目早起可能就已经存在,到最后代码集成的时候才能发现Bug;
人工低级错误发生
应用交付中的关键操作全部需要手工操作,易产生失误;
团队协同合作效率低
开发、运维、测试团队的工作相互依赖,需要等待其他团队工作完成后才能进行本团队工作;
开发运维对立
开发追求产品快速迭代,而运维追求系统稳定,目标产生对立;
2.引入CI/CD
由于传统软件交付模式存在以上诸多问题,所以引入CI/CD
2.1.CI持续集成
开发计划(plan)、编码(code)、构建(build)、测试(test)反复过程通过自动化方式实现称为持续集成(CI)
输出:可部署的构建产物(如 Docker 镜像、JAR包)。
2.2.CD-持续部署和持续交付
持续交付可以通过工作流程实现完全自动化,也可以在关键点通过手动步骤实现部分自动化(Manual)。
2.3.CD-持续部署
采用持续部署时,系统会在未经开发人员明确批准的情况下自动将修订部署到生产环境中,从而实现整个软件发布流程的自动化(AUTO)。
3.引用CI/CD产生的效益
加快产品研发过程
快速获得用户反馈
更早发现软件Bug
减少了人工操作风险,构建和发布过程更加稳定
提高研发、运维、测试相关部门的协同效率
研发人员更加专注于业务而不是软件部署
CI/CD可以根据公司业务规模迭代进行
二、GitLab
GitLab除了可以保存代码还具有以下功能
- 代码审查
- 问题跟踪
- 动态订阅
- 项目wiki
- 多角色项目管理
- 代码在线diff和预览
- CI/CD工具集成
- ContainerRegistry,该功能可以实现docker镜像的仓库功能,将gitlab上的代码仓的代码通过docker构建后并推入到容器仓库中,好处就是无需再额外部署一套docker仓库。
1.安装GitLab依赖
安装GitLab依赖,并设置开机启动
yum -y install curl policycoreutils openssh-server openssh-clents libsemanage-static libsemanage-devel
yum install curlpolicycoreutils openssh-server openssh-clients
systemctl enablesshd
systemctl enable sshd
systemctl start sshd
yum install postfix
systemctl enable postfix
systemctl start postfix
firewall-cmd --permanent --add-service=http
systemctl reload firewalld
2.安装GitLab服务
2.1.yum仓库配置
配置yum仓库
[root@hecs-83208 ~]# curl -fsSL https://packages.gitlab.cn/repository/raw/scripts/setup.sh | /bin/bash ==> Detected OS centos ==> Add yum repo file to /etc/yum.repos.d/gitlab-jh.repo [gitlab-jh] name=JiHu GitLab baseurl=https://packages.gitlab.cn/repository/el/$releasever/ gpgcheck=0 gpgkey=https://packages.gitlab.cn/repository/raw/gpg/public.gpg.key priority=1 enabled=1 ==> Generate yum cache for gitlab-jh ==> Successfully added gitlab-jh repo. To install JiHu GitLab, run "sudo yum/dnf install gitlab-jh".
2.2.安装GitLab
EXTERNAL_URL="http://114.115.128.169" yum -y install gitlab-jh
3.配置GitLab服务
3.1.访问URL设置
vim /etc/gitlab/gitlab.rb
EXTERNAL_URL="http://114.115.128.169"
3.2.存储路径设置
vim /etc/gitlab/gitlab.rb
"default" => { "path" => "/data/git-data", "failure_count_threshold" => 10, "failure_wait_time" => 30, "failure_reset_time" => 1800, "failure_timeout" => 30 } })
3.3.GitLab初始化密码
查看GitLab的初始化密码
[root@hecs-83208 gitlab]# pwd /etc/gitlab [root@hecs-83208 gitlab]# cat initial_root_password
3.4.设置root用户密码
设置新的root密码
4.管理GitLab服务
gitlab-ctl start #启动所有gitlab组件 gitlab-ctl stop #停止所有gitlab组件 gitlab-ctl restart #重启所有gitlab组件 gitlab-ctl status #查看所有gitlab组件的运行状态 gitlab-ctl reconfigure #刷新gitlab配置通常和gitlab-ctl restart组合使用,否则配置不生效
5.使用GitLab
5.1.GitLab账户添加SSHKey
$ cd ~/.ssh/ $ git config --global user.name "root" $ git config --global user.email "root@le.com" $ ssh-keygen -t rsa -C "root@le.com" $ ls id_rsa id_rsa.pub known_hosts $ cat id_rsa.pub $ git clone git@114.115.128.169:root/java-project.git Cloning into 'java-project'... remote: Enumerating objects: 3, done. remote: Counting objects: 100% (3/3), done. remote: Compressing objects: 100% (2/2), done. remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 Receiving objects: 100% (3/3), done.
GitLab
6.GitLab设置系统钩子
当GitLab的某1个仓库发生Push、Merge..事件时,调用Jenkins的WebHool,触发Jenkins的自动构建;
6.1.GitLab添加系统钩子
http://192.168.56.20:8081/buildByToken/build?job=build-maven-java-project&token=123.com
6.2.GitLab允许出站请求
只有GitLab允许出站请求之后,GitLab才可以去请求Jenkins的WebHook;
6.3.测试系统钩子的可用性
GitLab会向Jenkins的WebHook发起HTTP请求;
7.Python调用GitlabAPI
使用Python调用GitLab的API
from public import RecordLoggre import gitlab class GitlabApi: def __init__(self): self.GitlabUrl = 'https://git.apuscn.com:8443' # jenkins用户 private_token = '2icW6gSLdrX9ZGoPLAEs' self.ApiVersion = '4' self.GitClent = gitlab.Gitlab(self.GitlabUrl, private_token=private_token) # 记录日志 self.record_log = RecordLoggre() class ProjectApi(GitlabApi): def __init__(self, project_url): super(ProjectApi, self).__init__() self.project_url = project_url self.project = self.get_project() def get_project(self): """通过项目url获取项目实例""" project_name = self.project_url.split('/')[-1].split('.')[0] project_list = self.GitClent.projects.list(search=project_name) fit_fileter = [] for item in project_list: if (self.project_url == item.ssh_url_to_repo) or ( self.project_url == item.http_url_to_repo) or ( self.project_url == item.web_url) or ( f'git@{self.project_url}' == item.ssh_url_to_repo): fit_fileter.append(item) if len(fit_fileter) == 1: return fit_fileter[0] else: self.record_log.error(f'获取gitlab项目数量为{len(fit_fileter)},{str(fit_fileter)}') return None def get_branch(self, branch): """获取分支信息""" branch_data = {} try: branch_data = self.project.branches.get(branch) except Exception as Error: self.record_log.error(str(Error), exc_info=True) return branch_data def get_tag(self, tag): """获取标签信息""" tag_data = {} try: tag_data = self.project.tags.get(id=tag) except Exception as Error: self.record_log.error(str(Error), exc_info=True) return tag_data def get_version(self, version): """通过version获取分支或tag的信息""" try: data = self.project.branches.get(version) except Exception: try: data = self.project.tags.get(id=version) except Exception: data = None return data def list_branch(self): """列出项目的所有分支""" try: branch_list = self.project.branches.list(per_page=1000) for item in branch_list: yield item except Exception as Error: raise Exception(f'{self.project_url}获取分支失败,{str(Error)}') def list_tags(self): """列出项目的所有标签""" try: branch_list = self.project.tags.list(per_page=1000) for item in branch_list: yield item except Exception as Error: raise Exception(f'{self.project_url}获取标签失败,{str(Error)}') def find_merge_branch(self, tag): """查找tag从那个分支合并来的代码""" branch_commit_id = [] try: tag_obj = self.get_tag(tag) parent_ids = tag_obj.commit.get('parent_ids') for item in self.list_branch(): commit_id = item.commit.get('id') branch_name = item.name if commit_id in parent_ids: branch_commit_id.append((commit_id, branch_name)) except Exception as Error: self.record_log.error(f'{str(Error)}', exc_info=True) else: if len(branch_commit_id) == 1: return branch_commit_id[0] else: raise Exception(f'{self.project_url}获取合并前的分支失败,{str(branch_commit_id)}') if __name__ == '__main__': a = ProjectApi('git@git.apuscn.com:sa/ci-cd.git') print(a.find_merge_branch('v2.0.2')) # a.project.commits.get('e3d5a71b') # print(a.get_version('dev')) # print(dir(branch_data)) # print(branch_data.commit.get('id'))
三、Jenkins自动构建Maven项目
Jenkins支持master/agent模式,agent听从Master的任务调度,分摊Matser之上的的CICD工作量,使持续继承和部署的效率更高;
Jekins不能直接去Gitlab拉取代码,需要在Jekins上配置Gitlab的秘钥。
1.Jenkins服务器准备
Jenkins可以通过安装采用插件的方式,完成对Python、Java等不同类型的项目的CI、CD;
1.1.启动Jenkins
java -Dhudson.model.DownloadService.noSignatureCheck=true -jar jenkins.war --httpPort=8081
yum -y install java-1.8.0-openjdk*
Jenkins的插件的文件后缀名有两种格式 .jpi和.hpi,本质是被编译打包压缩之后的.clss文件;
jpi中的j就是Jenkins , hpi中的h是Hudson,Jenkins项目的前身是Hudson。
Publish Over SSH插件
Build Authorization Token Root插件
允许GitLab或者其他第三方程序免登录,通过Token参数调用Jenkins的WebHook;
buildByToken/build?job=NAME&token=SECRET
http://192.168.56.20:8081/buildByToken/build?job=build-maven-java-project&token=123.com
1.4.1.安装Git
yum -i install git
1.4.2.安装Maven
配置仓库路径和阿里镜像源,不在赘述
1.5.构建触发器
构建触发器是可以被外部程序调用的触发器,一旦触发器被触发就Jenkins就可以开始自动构建项目;
Jenkins支持以下几种方式进行自动构建
A、快照依赖构建/Build whenever a SNAPSHOT dependency is built
当依赖的快照被构建时执行本job
B、触发远程构建(例如,使用脚本)。远程调用本job的restapi时执行本jobjob依赖构建/Build after other projects are built
当依赖的job被构建时执行本job
C、定时构建/Build periodically。使用cron表达式定时构建本iob
D、向GitHub提交代码时触发Jenkins自动构建/GitHub hook trigger for GITScm pollingo Github-WebHook出发时构建本iob
E、定期检查代码变更/PollSCM
使用cron表达式定时检查代码变更,变更后构建本iob!0
1.5.1.webHook
通常由GitLab触发,一旦该URL被访问,Jenkins就会自动构建;
http://192.168.56.20:8081/job/build-maven-java-project/build?token=123.com
2.1.Maven配置
2.2.Git配置
2.3.指定分支
设置Jenkins去GitLab哪1个仓库的哪1个分支去拉取代码;
2.4.指定项目的根pom文件
设置Maven打包时去项目的什么路径下找到.pom文件,每次打包之前一定清理maven缓存,否则不更新!
Maven的Assembly插件的主要作用是:允许用户将项目输出与它的依赖项、模块、站点文档、和其他文件一起组装成1个可分发的归档文件(tar/zip)。
在Java项目Pom文件中指定源码打包之后是归档为jar/war/tar包,也指定了target目录;
Maven仅仅是通过读取Pom文件内容进行项目打包的打包工具。
/var/lib/jenkins/tools/maven/bin/mvn
-B
-f /var/lib/jenkins/workspace/common-service/sg-2/xxl-job-admin__runner/pom.xml
clean install -Dmaven.test.skip=true
-Pprod
实际执行的命令如所示。
2.5.Pre Step设置
2.5.1./root/clean.sh脚本开发
如果clean.sh的语法正确,但执行老报错,原因是脚本从Windos平台Copy到Unix平台中,Shell脚本的格式发生了肉眼不可见的变化;
直接使用Vim编辑即可!
#!/bin/bash echo "cenling........" #删除历史数据 rm -rf ./jar #获取传入的参数 appName=$1 echo "参数是$1" #获取正在运行的jar包的Pid pid=`ps -ef | grep $1 | grep 'java -jar' | awk '{printf $2}' ` echo $pid #如果Pid为空提示 if [ -z $pid ]; then echo "$appName not started" else kill -9 $pid && echo "$appName stoping" fi
2.5.2.设置执行/root/clean.sh脚本
2.6.配置部署服务器
把Jenkins服务器调用Maven编译打包之后,Jar包应该传输到哪1台服务器上去部署?
在Configure System菜单下
2.7.Post Step设置
当Jar包传输到部署服务器之后就是启动jar包!
启动jar包不能使用 java -jar直接启动,因为该命令没有正确退出,会延迟Jenkins的构建时间;
nohup java -jar /root/jar/CI-CD*.jar --server.port=8001 > mylog.log 2>&1 &
2.8.构建成功效果
切记观察Transferred file的数量,确定Jar包传输到了部署服务器上;
3.Python调用JenkinAPI
python-jenkins==1.7.0
在Python使用python-jenkins这个第三方包,来实现Python和Jenkins的交互。
from public.utils import record_loggre from config import OpsConfig from public.hash_ring import HashRing import jenkins import time class JenkinsApi: def __init__(self): self.jenkins_server = OpsConfig.Jenkins.server self.user_name = OpsConfig.Jenkins.user self.password = OpsConfig.Jenkins.password self.server_list = self.jenkins_server.split(',') # 记录日志 self.record_log = record_loggre() def select_server(self, key): ring = HashRing(self.server_list) return jenkins.Jenkins(ring.get_node(key), username=self.user_name, password=self.password) def server_iter(self): for item in self.server_list: yield jenkins.Jenkins(item, username=self.user_name, password=self.password) def request(self, client, action, *args, **kwargs): """根据传来的客户端和动作发起请求""" method = getattr(client, action, None) if method: retry, sign = 0, False while True: try: result = method(*args, **kwargs) except Exception as Error: self.record_log.error(f'{str(Error)},重试{retry}次', exc_info=True) retry += 1 if retry >= 3: result = str(Error) break time.sleep(3) else: sign = True break return sign, result else: return False, None def request_iter(self, action, *args, **kwargs): """请求所有服务器,使所有服务器保持一致""" for client in self.server_iter(): self.request(client, action, *args, **kwargs) class JenkinsBuild(JenkinsApi): """Jenkins 构建相关""" def __init__(self, job_name): super(JenkinsBuild, self).__init__() self.job_name = job_name self.Jenkins = self.select_server(self.job_name) def get_build_console(self, build_number): """获取构建任务的日志输出""" sign, build_console = self.request(self.Jenkins, 'get_build_console_output', self.job_name, build_number) return sign, build_console def get_build_status(self, build_number): """获取构建状态""" sign, build_status = self.request(self.Jenkins, 'get_build_info', self.job_name, build_number) if sign: building, result = build_status.get('building'), build_status.get('result') else: building, result = None, None return building, result def last_test_build(self): """获取动作为test最后一次构建""" job_info = self.Jenkins.get_job_info(self.job_name) job_builds, build_number = job_info.get('builds'), None for item in job_builds: build_number = item.get('number') build_info = self.Jenkins.get_build_info(self.job_name, build_number) parameters = build_info.get('actions')[0].get('parameters') if parameters[0].get('value') == 'test': break return build_number def get_job_config(self, name): config = self.Jenkins.get_job_config(name) print(config) # self.Jenkins.create_folder('App-Dev-test/ali-cn-zj1') # a = self.Jenkins.get_job_config('App-Dev-test/ali-cn-zj1') # print(a) if __name__ == '__main__': JenkinsBuild('App-Dev/user-growth/hermes-admin1').get_job_config('App-Dev/user-growth/hermes-admin1') JenkinsApi().select_server('App-Dev/user-growth/hermes22-admin12') a = JenkinsBuild('App-Dev/ali-cn-zjk/middle-platform-ad_adx-bss-service').get_build_status(7) print(a) # func = getattr(JenkinsApi, 'request', None) # print(func)
四、GitLabCI/CD
JenKins的功能都是通过插件完成的,但是JenKins的插件升级维护的成本很高;
大规模使用JenKins之后,发现Jenkins更新完插件后,经常会导致服务重启失败,非常影响CI/CD系统运行的稳定性;
GitLab不仅仅是1个代码库,GitLab还自带了1个端到端一站式CI和CD解决方案(GitLab CI/CD);
1.GitLab CI/CD的优势
- 开源免费:无论是GitLab社区版本还是GitLab企业版都有CI/CD功能;
- 易于学习:具有详细的入门文档。
- 无缝集成:GitLab CI/CD是GitLab的一部分,支持从计划到部署,具有出色的用户体验。
- 可扩展::测试可以在单独的计算机上分布式运行,可以根据需要添加任意数量的计算机。
- 构建效率更快:每个构建可以拆分为多个作业,这些作业可以在多台计算机上并行运行。
- 针对持续交付进行了优化:多个阶段,手动部署,环境和变量。
2.GitLab CI/CD组件
2.1..gitlab-ci.yml
在代码仓库/项目的根目录下,定义了CI/CD任务执行的步骤;
每1个代码分支中包含1个.gitlab-ci.yml文件,可以避免配置漂移;
2.2.GitLabServer
GitLab CI/CD属于GitLab的一部分,GitLab Server会解析.gitlab-ci.yml文件,将任务下发到Runner
提供一个不错的用户界面,可以实时查看Job列表和Job执行日志
2.3.GitLabRunners
GitLabRunners是GitLabServer的1个代理,GitLabServer根据gitlab-ci.yaml中的runner配置将CI/CD任务下发到具体的Runner执行;
Runner可以单独部署,并通过API与GitLabCI/CD一起使用。
总结
为了运行测试,至少需要1个GitLab实例 + 1个GitLabRunner .
3.GitLabCI/CD工作原理
将代码托管到Git存储库。
在项目根目录创建ci文件.gitlab-ci.yml,在文件中指定构建,测试和部暑脚本。
GitLab将检测到它并使用名为GitLabRunner的工具运行脚本。
脚本被分组为作业(Job),它们共同组成了1个管道。