基于 Kubernetes 的 DevOps 平台
基于k8s集群部署gitlab、sonarQube、Jenkins等工具,并把上述工具集成到Jenkins中,以Django 项目为例,通过多分支流水线及Jenkinsfile实现项目代码提交到不同的仓库分支,实现自动代码扫描、 单元测试、docker容器构建、k8s服务的自动部署。
- DevOps、CI、CD介绍
- Jenkins、sonarQube、gitlab的快速部署
- Jenkins初体验
- 流水线入门及Jenkinsfile使用
- Jenkins与Kubernetes的集成
- sonarQube代码扫描与Jenkins的集成
- 实践Django项目的基于Jenkinsfile实现开发、测试环境的CI/CD
DevOps、CI、CD介绍
软件交付流程
一个软件从零开始到最终交付,大概包括以下几个阶段:规划、编码、构建、测试、发布、部署和维护,基于这些阶段,我们的软件交付模型大致经历了几个阶段:
瀑布式流程
前期需求确立之后,软件开发人员花费数周和数月编写代码,把所有需求一次性开发完,然后将代码交 给QA(质量保障)团队进行测试,然后将最终的发布版交给运维团队去部署。瀑布模型,简单来说, 就是等一个阶段所有工作完成之后,再进入下一个阶段。这种模式的问题也很明显,产品迭代周期长, 灵活性差。一个周期动辄几周几个月,对吧,适应不了当下产品需要快速迭代的场景。
敏捷开发
任务由大拆小,开发、测试协同工作,注重开发敏捷,不重视交付敏捷
DevOps
开发、测试、运维协同工作, 持续开发+持续交付。
我们是否可以认为Devops = 提倡开发、测试、运维协同工作来实现持续开发、持续交付的一种软件交付模式?
大家想一下为什么最初的开发模式没有直接进入DevOps的时代?各角色人员去沟通协作的时候都是手 动去做,交流靠嘴,靠人去指挥,很显然会出大问题。所以说不能认为DevOps就是一种交付模式,因 为解决不了沟通协作成本,这种模式就不具备可落地性。
那DevOps时代如何解决角色之间的成本问题?DevOps的核心就是自动化。自动化的能力靠什么来支 撑,工具和技术。
DevOps工具链
靠这些工具和技术,才实现了自动化流程,进而解决了协作成本,使得devops具备了可落地性。因此 我们可以大致给devops一个定义:
devops = 提倡开发、测试、运维协同工作来实现持续开发、持续交付的一种软件交付模式 + 基于工具 和技术支撑的自动化流程的落地实践。
因此devops不是某一个具体的技术,而是一种思想+自动化能力,来使得构建、测试、发布软件能够更 加地便捷、频繁和可靠的落地实践。本次课程核心内容就是要教会大家如何利用工具和技术来实现完整 的DevOps平台的建设。我们主要使用的工具有:
-
gitlab,代码仓库,企业内部使用最多的代码版本管理工具。
-
Jenkins, 一个可扩展的持续集成引擎,用于自动化各种任务,包括构建、测试和部署软件。 3. robotFramework, 基于Python的自动化测试框架
-
sonarqube,代码质量管理平台
-
Kubernetes
-
Docker
Kubernetes 环境中部署 jenkins
注意点
- 因为后面Jenkins会与kubernetes集群进行集成,会需要调用kubernetes集群的api,因此安装的 时候创建了ServiceAccount并赋予了cluster-admin的权限
- 默认部署到 jenkins=true 的节点;这里我做了修改,使用了专门的 spot 工作节点部署的 jenkins;
- 数据存储通过 hostpath 挂载到宿主机中;因为我使用的是 eks 所以这里直接使用 efs 实现的数据持久化
- 初始化容器用来设置目录权限;因为jenkins启动,默认使用的是 uid、gid = 1000 的用户所以需要初始化权限
jenkins/jenkins-all.yaml
############### 使用 storageClass 创建 pvc ###################
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: jenkins-data-pvc
namespace: monitoring
spec:
accessModes:
# eks 不支持 ReadWriteMany
# - ReadWriteMany
- ReadWriteOnce
# 指定 storageClass 的名字,这里使用 minikube 默认的 standard
storageClassName: sc-retain
resources:
requests:
# 因为 efs 的容量限制默认不生效。所以这里创建的 pvc 为10G,当10G满了,可以继续写数据
storage: 10Gi
###############创建一个ServiceAccount 名称为:jenkins-admin ###################
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: jenkins-admin
namespace: monitoring
labels:
name: jenkins
############### 绑定账户 jenkins-admin 为集群管理员角色,为了控制权限建议绑定自定义角色 ###################
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: jenkins-admin
labels:
name: jenkins
subjects:
- kind: ServiceAccount
name: jenkins-admin
namespace: monitoring
roleRef:
kind: ClusterRole
# cluster-admin 是 k8s 集群中默认的管理员角色,正式环境下,尽量自定义 role 角色
name: cluster-admin
apiGroup: rbac.authorization.k8s.io
############### 在 monitoring 命名空间创建 deployment ###################
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: jenkins
namespace: monitoring
spec:
replicas: 1
selector:
matchLabels:
app: jenkins
template:
metadata:
labels:
app: jenkins
spec:
nodeSelector:
# 绑定了特定的 nodegroup,
alpha.eksctl.io/nodegroup-name: ng2c4gSpotPrivate-cicd
terminationGracePeriodSeconds: 10
# 注意:k8s 1.21.x 中 serviceAccount 改名为 serviceAccountName
# 这里填写上面创建的 serviceAccount 的 name
serviceAccount: jenkins-admin
initContainers:
- name: fix-permissions
image: busybox
command: ["sh", "-c", "chown -R 1000:1000 /var/jenkins_home"]
securityContext:
privileged: true
volumeMounts:
- name: jenkinshome
mountPath: /var/jenkins_home
containers:
- name: jenkins
securityContext:
# 特权模式
privileged: true
# root 用户运行
runAsUser: 0
image: jenkins/jenkins:lts-jdk11
imagePullPolicy: IfNotPresent
env:
- name: JAVA_OPTS
value: -Duser.timezone=Asia/Shanghai
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
mountPath: /var/jenkins_home
volumes:
- name: jenkinshome
persistentVolumeClaim:
claimName: jenkins-data-pvc
############### 在 monitoring 命名空间创建 service ###################
---
apiVersion: v1
kind: Service
metadata:
name: jenkins
namespace: monitoring
labels:
app: jenkins
spec:
selector:
app: jenkins
type: ClusterIP
ports:
- name: web
port: 8080
targetPort: 8080
### anget service ,用于节点之间的通讯
---
apiVersion: v1
kind: Service
metadata:
name: jenkins-agent
namespace: monitoring
labels:
app: jenkins
spec:
selector:
app: jenkins
type: ClusterIP
ports:
- name: agent
port: 50000
targetPort: 50000
#⚠️: 这里没有创建 ingress ,因为使用的是名称空间已有的ingress,所以没有创建,
部署服务:
## 部署服务
kubectl apply -f jenkins-all.yaml
# 查看日志,第一次启动提示需要完成初始化设置,
kubectl -n monitoring logs -f jenkins-master-767df9b574-lgdr5
# 或者使用如下方式获取
kubectl -n monitoring exec jenkins-master-767df9b574-lgdr5 bash
cat /var/jenkins_home/secrets/initialAdminPassword
# 跳过选择安装推荐的插件环节,直接进入Jenkins。由于默认的插件地址安装非常慢,我们可以替换成 国内清华的源,进入 jenkins 工作目录,目录下面有一个 updates 的目录,下面有一个 default.json 文件,我们执行下面的命令替换插件地址:
cd /var/jenkins_home/updates
sed -i 's/http:\/\/updates.jenkins-ci.org\/download/https:\/\/mirrors.tuna.tsinghua.edu.cn\/jenkins/g' default.json
sed -i 's/http:\/\/www.google.com/https:\/\/www.baidu.com/g' default.json
# 重新启动pod,由于已经完成初始化,因此第二次启动会很快:
kubectl -n monitoring scale deploy jenkins-master --replicas=0
kubectl -n monitoring scale deploy jenkins-master --replicas=1
# 再次输入初始化密码,进入Jenkins主页面,选择右上角admin->configure->password重新设置管理员 密码,设置完后,会退出要求重新登录,使用admin/xxxxxx(新密码),登录即可。
安装汉化插件
Jenkins -> manage Jenkins -> Plugin Manager -> Avaliable,输入chinese自动搜索
选中后,选择[Download now and install after restart],等待下载完成,然后重启pod
kubectl -n monitoring scale deploy jenkins-master --replicas=0
kubectl -n monitoring scale deploy jenkins-master --replicas=1
启动后,界面默认变成中文。
Jenkins + gitlab + 钉钉
- 代码提交gitlab,自动触发Jenkins任务
- Jenkins任务完成后发送钉钉消息通知
准备gitlab
这里使用 docker 临时启动 一个 gitlab
docker run -d -p 8443:443 -p 8090:80 -p 8022:22 --name gitlab --restart always -v /opt/gitlab/config:/etc/gitlab -v /opt/gitlab/logs:/var/log/gitlab -v /opt/gitlab/data:/var/opt/gitlab gitlab/gitlab-ce:latest
修改gitlab root密码
方法一:
gitlab-ce 初装以后,把密码放在了一个临时文件中了 /etc/gitlab/initial_root_password 这个文件将在首次执行reconfigure后24小时自动删除
# 因为我们映射了 目录, 所以这里对应的目录为
# cat /opt/gitlab/config/initial_root_password | grep -i Password
Password: ARVVjpDcF5g/ZG2DScarmU9s9TKUT09HQd3qUj4jq/c=
方法2:直接登录 gitlab 容器修改密码
docker exec -it gitlab bash
<root@gitlab ~># cd /opt/gitlab/bin
<root@gitlab bin># gitlab-rails console
--------------------------------------------------------------------------------
Ruby: ruby 2.7.4p191 (2021-07-07 revision a21a3b7d23) [x86_64-linux]
GitLab: 14.4.1 (1a23d731c9f) FOSS
GitLab Shell: 13.21.1
PostgreSQL: 12.7
--------------------------------------------------------------------------------
Loading production environment (Rails 6.1.4.1)
irb(main):001:0> u=User.where(id:1).first
=> #<User id:1 @root>
irb(main):002:0> User.all
=> #<ActiveRecord::Relation [#<User id:1 @root>]>
irb(main):003:0> u.password='12345678'
=> "12345678"
irb(main):004:0> u.password_confirmation='12345678'
=> "12345678"
irb(main):005:0> u.save!
Enqueued ActionMailer::MailDeliveryJob (Job ID: 2e04113b-4441-4b96-b85d-b6d8f4adc582) to Sidekiq(mailers) with arguments: "DeviseMailer", "password_change", "deliver_now", {:args=>[#<GlobalID:0x00007f31cb855aa8 @uri=#<URI::GID gid://gitlab/User/1>>]}
=> true
irb(main):006:0> exit
再去登录就可以成功
推送项目 到 gitlab 新建的项目中
mkdir demo
cp -r myblog demo/
cd demo/myblog
git remote rename origin old-origin
git remote add origin http://152.136.62.143/root/myblog.git
git push -u origin --all
git push -u origin --tags
推送钉钉
- 配置机器人
- 试验发送消息
$ curl 'https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxxxx' \
-H 'Content-Type: application/json' \
-d '{"msgtype": "text",
"text": {
"content": "我就是我, 是不一样的烟火"
} }'
演示过程
流程示意图:
-
安装gitlab plugin 插件中心搜索并安装gitlab,直接安装即可
-
系统管理,配置Gitlab
其中的API Token,需要从下个步骤中获取 -
获取AccessToken
登录gitlab,选择user->profile->access tokens新建一个访问token;这里权限设置全部如果需要权限控制,自行取舍 -
jenkins 创建自由风格项目
-
gitlab connection 选择为刚创建的gitlab
-
源码管理选择Git,填项项目地址
-
新建一个 Credentials 认证,使用用户名密码 方式,配置gitlab的用户和密码
-
构建触发器选择 Build when a change is pushed to GitLab
-
生成一个Secret token
-
保存
-
-
到gitlab配置webhook
-
进入项目下settings->Integrations
-
Secret Token 填入在Jenkins端生成的token
-
Add webhook
- test push events会不通过,因为gitlab中无法解析自定义域名
-
-
配置host解析
由于我们的Jenkins域名是本地解析,因此需要在gitlab中添加解析,进入gitlab容器编辑hosts文 件,或者重新运行容器,在宿主机中的hosts中添加并挂载到容器的hosts目录。再次test push events,会由于安全设置,报错:Requests to the local network are not allowed
-
设置gitlab允许向本地网络发送webhook请求
访问 http://152.136.62.143/admin/application_settings/network ,展开Outbound requests Collapse,勾选第一项即可。再次test push events,成功。
-
配置free项目,增加构建步骤,执行shell,将发送钉钉消息的shell保存
-
提交代码到gitlab仓库,查看构建是否自动执行
Master-Slaves(agent)模式
多个任务都在master节点执行,对master节点的性能会造成一定影响,如何将任务分散到不同的节点,做成多slave的方式?
-
添加slave节点
-
系统管理 -> 节点管理 -> 新建节点
-
比如添加172.21.32.5,选择固定节点,保存
-
远程工作目录/opt/jenkins_jobs
-
标签为任务选择节点的依据,如172.21.32.5;进行任务分配时的一个依据
-
启动方式选择通过java web启动代理,代理是运行jar包,通过JNLP(是一种允许客户端启动 托管在远程Web服务器上的应用程序的协议 )启动连接到master节点服务中
-
-
执行java命令启动agent服务
## 登录172.21.32.5,下载agent.jar # 下载命令在 agnet 节点配置页面可以找到 $ wget http://jenkins.qq.com/jnlpJars/agent.jar ## 会提示找不到agent错误,因为没有配置地址解析,由于连接jenkins master会通过50000端 口,直接使用cluster-ip $ kubectl -n jenkins get svc #在master节点执行查询cluster-ip地址 NAME TYPE CLUSTER-IP EXTERNAL-IP jenkins ClusterIP 10.99.204.208 <none> 4h8m PORT(S) AGE 8080/TCP,50000/TCP ## 再次回到131节点 $ wget 10.99.204.208:8080/jnlpJars/agent.jar $ java -jar agent.jar -jnlpUrl http://10.99.204.208:8080/computer/172.21.32.5/slave-agent.jnlp -secret 4be4d164f861d2830835653567867a1e695b30c320d35eca2be9f5624f8712c8 -workDir "/opt/jenkins_jobs" ... INFO: Remoting server accepts the following protocols: [JNLP4-connect, Ping] Apr 01, 2020 7:03:51 PM hudson.remoting.jnlp.Main$CuiListener status INFO: Agent discovery successful Agent address: 10.99.204.208 Agent port: 50000 Identity: e4:46:3a:de:86:24:8e:15:09:13:3d:a7:4e:07:04:37 Apr 01, 2020 7:03:51 PM hudson.remoting.jnlp.Main$CuiListener status INFO: Handshaking Apr 01, 2020 7:03:51 PM hudson.remoting.jnlp.Main$CuiListener status INFO: Connecting to 10.99.204.208:50000 Apr 01, 2020 7:03:51 PM hudson.remoting.jnlp.Main$CuiListener status INFO: Trying protocol: JNLP4-connect Apr 01, 2020 7:04:02 PM hudson.remoting.jnlp.Main$CuiListener status INFO: Remote identity confirmed: e4:46:3a:de:86:24:8e:15:09:13:3d:a7:4e:07:04:37 Apr 01, 2020 7:04:03 PM hudson.remoting.jnlp.Main$CuiListener status INFO: Connected
-
查看Jenkins节点列表,新节点已经处于可用状态
-
测试使用新节点执行任务
-
配置free项目
-
限制项目的运行节点 ,标签表达式选择 172.21.32.5
-
立即构建
-
查看构建日志
Started by user admin Running as SYSTEM Building remotely on 172.21.32.5 in workspace /opt/jenkins_jobs/workspace/free-demo using credential gitlab-user Cloning the remote Git repository Cloning repository http://152.136.62.143/root/myblog.git > git init /opt/jenkins_jobs/workspace/free-demo # timeout=10 ...
-
Jenkins定制化容器
由于每次新部署Jenkins环境,均需要安装很多必要的插件,因此考虑把插件提前做到镜像中
# Dockerfile
FROM jenkinsci/blueocean
LABEL maintainer="Yongxin@devops.cn"
## 用最新的插件列表文件替换默认插件文件
COPY plugins.txt /usr/share/jenkins/ref/
## 执行插件安装
RUN /usr/local/bin/install-plugins.sh < /usr/share/jenkins/ref/plugins.txt
# plugins.txt
ace-editor:1.1
allure-jenkins-plugin:2.28.1
ant:1.10
antisamy-markup-formatter:1.6
apache-httpcomponents-client-4-api:4.5.10-1.0
authentication-tokens:1.3
...
# get_plugin.sh
# 获取线上 jenkins 插件;
admin:123456@localhost 需要替换成Jenkins的用户名、密码及访问地址
#!/usr/bin/env bash
curl -sSL "http://admin:123456@localhost:8080/pluginManager/api/xml?
depth=1&xpath=/*/*/shortName|/*/*/version&wrapper=plugins" | perl -pe 's/.*?
<shortName>([\w-]+).*?<version>([^<]+)()(<\/\w+>)+/\1:\2\n/g'|sed 's/ /:/' >
plugins.txt
## 执行构建,定制jenkins容器
$ docker build . -t 172.21.32.13:5000/jenkins:v20200414 -f Dockerfile $ docker push 172.21.32.13:5000/jenkins:v20200414
至此,我们可以使用定制化的镜像启动jenkins服务
## 删掉当前服务
$ kubectl delete -f jenkins-all.yaml
## 删掉已挂载的数据
$ rm -rf /var/jenkins_home
## 替换使用定制化镜像
$ sed -i 's#jenkinsci/blueocean#172.21.32.13:5000/jenkins:v20200404#g' jenkins- all.yaml
## 重新创建服务
$ kubectl create -f jenkins-all.yaml
本章小结
自由风格项目弊端:
任务的完成需要在Jenkins端维护大量的配置 可读性、可移植性很差,不够优雅
流水线入门
为什么叫做流水线,和工厂产品的生产线类似,pipeline是从源码到发布到线上环境。关于流水线,需 要知道的几个点:
- 重要的功能插件,帮助Jenkins定义了一套工作流框架;
- Pipeline 的实现方式是一套 Groovy DSL( 领域专用语言 ),所有的发布流程都可以表述为一段 Groovy 脚本;
- 将WebUI上需要定义的任务,以脚本代码的方式表述出来;
- 帮助jenkins实现持续集成CI(Continue Integration)和持续部署CD(Continue Deploy)的重要 手段;
流水线基础语法
两种语法类型:
- Scripted Pipeline,脚本式流水线,最初支持的类型
- Declarative Pipeline,声明式流水线,为Pipeline plugin在2.5版本之后新增的一种脚本类型,后 续Open Blue Ocean所支持的类型。与原先的Scripted Pipeline一样,都可以用来编写脚本。 Declarative Pipeline 是后续Open Blue Ocean所支持的类型,写法简单,支持内嵌Scripted Pipeline代码
为与 BlueOcean 脚本编辑器兼容,通常建议使用 Declarative Pipeline 的方式进行编写,从 jenkins 社区的动向来看,很明显这种语法结构也会是未来的趋势。
脚本示例
pipeline {
agent any
environment {
PROJECT = 'myblog'
}
options {
disableConcurrentBuilds()
retry(1)
skipStagesAfterUnstable()
timeout(time: 10, unit: 'MINUTES')
skipDefaultCheckout()
buildDiscarder(logRotator(numToKeepStr: '10', artifactNumToKeepStr: '1'))
}
tools {
/**
* Predefined environment variables MAVEN3 and JDK-1.8
*/
maven 'MAVEN3'
jdk 'JDK-1.8'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build') {
steps {
sh 'make'
}
}
stage('Test'){
steps {
sh 'make check'
junit 'reports/**/*.xml'
}
}
stage('Deploy') {
steps {
sh 'make publish'
}
}
}
post {
success {
echo 'Congratulations!'
}
failure {
echo 'Oh no!'
}
always {
echo 'I will always say Hello again!'
}
}
}
脚本解释:
-
checkout 步骤为检查代码; scm 是一个特殊变量,指示 checkout 步骤克隆触发此 Pipeline 运行的 特定修订
-
agent:指明使用哪个agent节点来执行任务,定义于pipeline顶层或者stage内部
-
any,可以使用任意可用的agent来执行
-
label,在提供了标签的 Jenkins 环境中可用的代理上执行流水线或阶段。 例如:
agent { label 'my-defined-label' }
,最常见的使用方式 -
none,当在 pipeline 块的顶部没有全局代理, 该参数将会被分配到整个流水线的运行中 并且每个 stage 部分都需要包含他自己的 agent 部分。比如: agent none
-
docker, 使用给定的容器执行流水线或阶段。 在指定的节点中,通过运行容器来执行任务
agent { docker { image 'maven:3-alpine' label 'my-defined-label' args '-v /tmp:/tmp' } }
-
-
options: 允许从流水线内部配置特定于流水线的选项。
-
buildDiscarder , 为最近的流水线运行的特定数量保存组件和控制台输出。例如:
options { buildDiscarder(logRotator(numToKeepStr: '1')) }
-
disableConcurrentBuilds ,不允许同时执行流水线。 可被用来防止同时访问共享资源等。 例 如:
options { disableConcurrentBuilds() }
-
timeout ,设置流水线运行的超时时间, 在此之后,Jenkins将中止流水线。例如:
options { timeout(time: 1, unit: 'HOURS') }
-
retry,在失败时, 重新尝试整个流水线的指定次数。 For example:
options { retry(3) }
-
-
tools:定义需要安装的工具,且会自动加入到PATH
-
environment: 指令制定一个 键-值对序列,该序列将被定义为所有步骤的环境变量
-
stages: 包含一系列一个或多个 stage指令, stages 部分是流水线描述的大部分"work" 的位置。 建议 stages 至少包含一个 stage 指令用于连续交付过程的每个离散部分,比如构建, 测试, 和部 署。
pipeline {
agent any
stages {
stage('Example') {
steps {
echo 'Hello World'
}
}
}
}
-
steps: 在给定的 stage 指令中执行的定义了一系列的一个或多个steps。
-
post: 定义一个或多个steps ,这些阶段根据流水线或阶段的完成情况而运行 post 支持以下 post-condition 块中的其中之一: always , changed , failure , success , unstable , 和 aborted 。
- always, 无论流水线或阶段的完成状态如何,都允许在 post 部分运行该步骤
- changed, 当前流水线或阶段的完成状态与它之前的运行不同时,才允许在 post 部分运行该 步骤
- failure, 当前流水线或阶段的完成状态为"failure",才允许在 post 部分运行该步骤, 通常 web UI是红色
- success, 当前流水线或阶段的完成状态为"success",才允许在 post 部分运行该步骤, 通常 web UI是蓝色或绿色
- unstable, 当前流水线或阶段的完成状态为"unstable",才允许在 post 部分运行该步骤, 通 常由于测试失败,代码违规等造成。通常web UI是黄色
- aborted, 只有当前流水线或阶段的完成状态为"aborted",才允许在 post 部分运行该步 骤, 通常由于流水线被手动的aborted。通常web UI是灰色
创建pipeline示意:
jenkins/pipelines/p1.yaml
pipeline {
agent any
stages {
stage('printenv') {
steps {
echo 'Hello World'
sh 'printenv'
}
}
stage('check') {
steps {
checkout([$class: 'GitSCM', branches: [[name: '*/master']],
doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [],
userRemoteConfigs: [[credentialsId: 'gitlab-user', url:
'http://152.136.62.143/root/myblog.git']]])
}
}
stage('build-image') {
steps {
sh 'docker build . -t myblog:latest'
}
}
}
}
同样的,我们可以配置触发器,使用webhook的方式接收项目的push事件,
- 构建触发器选择 Build when a change is pushed to GitLab.
- 生成 Secret token
- 配置gitlab,创建webhook,发送test push events测试
思考:
- 每个项目都把大量的pipeline脚本写在Jenkins端,对于谁去维护及维护成本是一个问题
- 没法做版本控制
Jenkinsflie
Jenkins Pipeline 提供了一套可扩展的工具,用于将“简单到复杂”的交付流程实现为“持续交付即代码”。 Jenkins Pipeline 的定义通常被写入到一个文本文件(称为 Jenkinsfile )中,该文件可以被放入项 目的源代码控制库中。
1:使用Jenkinsfile管理pipeline
- 在项目中新建Jenkinsfile文件,拷贝已有script内容
- 配置pipeline任务,流水线定义为Pipeline Script from SCM
- 执行push 代码测试
Jenkinsfile:
jenkins/pipelines/p2.yaml
pipeline {
agent { label '10.23.1.33'}
stages {
stage('printenv') {
steps {
echo 'Hello World'
sh 'printenv'
}
}
stage('check') {
steps {
checkout([$class: 'GitSCM', branches: [[name: '*/master']],
doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [],
userRemoteConfigs: [[credentialsId: 'gitlab-user', url:
'http://152.136.62.143/root/myblog.git']]])
}
}
stage('build-image') {
steps {
retry(2) { sh 'docker build . -t myblog:latest'}
}
}
}
}
Blue Ocean:
我们需要知道的几点:
- 是一个插件, 旨在为Pipeline提供丰富的体验 ;
- 连续交付(CD)Pipeline的复杂可视化,允许快速和直观地了解Pipeline的状态;
- 目前支持的类型仅针对于Pipeline,尚不能替代Jenkins 经典版UI
2:丰富流水线内容
-
优化代码检出阶段
由于目前已经配置了使用git仓库地址,且使用SCM来检测项目,因此代码检出阶段完全没有必要 再去指定一次
-
构建镜像的 tag 使用 git 的 commit id
-
增加post阶段的消息通知,丰富通知内容
jenkins/pipelines/p3.yaml
pipeline {
agent { label '10.23.1.33'}
stages {
stage('printenv') {
steps {
echo 'Hello World'
sh 'printenv'
}
}
stage('check') {
steps {
checkout scm
}
}
stage('build-image') {
steps {
retry(2) { sh 'docker build . -t myblog:${GIT_COMMIT}'}
}
}
}
post {
success {
echo 'Congratulations!'
sh """
curl 'https://oapi.dingtalk.com/robot/send?access_token=8ca19c49673df02df3c1c3b9b2602f223f3aabaac004b5800eb4b12e89b22a52' \
-H 'Content-Type: application/json' \
-d '{"msgtype": "text",
"text": {
"content": "😄👍构建成功👍😄\n 关键字:luffy\n 项目名称: ${JOB_BASE_NAME}\n Commit Id: ${GIT_COMMIT}\n 构建地址:${RUN_DISPLAY_URL}"
}
}'
"""
}
failure {
echo 'Oh no!'
sh """
curl 'https://oapi.dingtalk.com/robot/send?access_token=8ca19c49673df02df3c1c3b9b2602f223f3aabaac004b5800eb4b12e89b22a52' \
-H 'Content-Type: application/json' \
-d '{"msgtype": "text",
"text": {
"content": "😖❌构建失败❌😖\n 关键字:luffy\n 项目名称: ${JOB_BASE_NAME}\n Commit Id: ${GIT_COMMIT}\n 构建地址:${RUN_DISPLAY_URL}"
}
}'
"""
}
always {
echo 'I will always say Hello again!'
}
}
}
3:使用k8s部署服务
- 新建deploy目录,将k8s所需的文件放到deploy目录中
- 将镜像地址改成模板,在pipeline中使用新构建的镜像进行替换
- 执行kubectl apply -f deploy应用更改,需要配置kubectl认证
jenkins/pipelines/p4.yaml
pipeline {
agent { label '172.21.32.5'}
environment {
IMAGE_REPO = "172.21.32.13:5000/myblog"
}
stages {
stage('printenv') {
steps {
echo 'Hello World'
sh 'printenv'
}
}
stage('check') {
steps {
checkout scm
}
}
stage('build-image') {
steps {
retry(2) { sh 'docker build . -t ${IMAGE_REPO}:${GIT_COMMIT}'}
}
}
stage('push-image') {
steps {
# push 重试2次
retry(2) { sh 'docker push ${IMAGE_REPO}:${GIT_COMMIT}'}
}
}
stage('deploy') {
steps {
sh "sed -i 's#{{IMAGE_URL}}#${IMAGE_REPO}:${GIT_COMMIT}#g' deploy/*"
# 超时1分钟
timeout(time: 1, unit: 'MINUTES') {
sh "kubectl apply -f deploy/"
}
}
}
}
post {
success {
echo 'Congratulations!'
sh """
curl 'https://oapi.dingtalk.com/robot/send?access_token=8ca19c49673df02df3c1c3b9b2602f223f3aabaac004b5800eb4b12e89b22a52' \
-H 'Content-Type: application/json' \
-d '{"msgtype": "text",
"text": {
"content": "😄👍构建成功👍😄\n 关键字:myblog\n 项目名称: ${JOB_BASE_NAME}\n Commit Id: ${GIT_COMMIT}\n 构建地址:${RUN_DISPLAY_URL}"
}
}'
"""
}
failure {
echo 'Oh no!'
sh """
curl 'https://oapi.dingtalk.com/robot/send?access_token=8ca19c49673df02df3c1c3b9b2602f223f3aabaac004b5800eb4b12e89b22a52' \
-H 'Content-Type: application/json' \
-d '{"msgtype": "text",
"text": {
"content": "😖❌构建失败❌😖\n 关键字:luffy\n 项目名称: ${JOB_BASE_NAME}\n Commit Id: ${GIT_COMMIT}\n 构建地址:${RUN_DISPLAY_URL}"
}
}'
"""
}
always {
echo 'I will always say Hello again!'
}
}
}
4:使用凭据管理敏感信息
上述Jenkinsfile中存在的问题是敏感信息使用明文,暴漏在代码中,如何管理流水线中的敏感信息(包 含账号密码),之前我们在对接gitlab的时候,需要账号密码,已经使用过凭据来管理这类敏感信息, 同样的,我们可以使用凭据来存储钉钉的token信息,那么,创建好凭据后,如何在Jenkinsfile中获取 已有凭据的内容?
Jenkins 的声明式流水线语法有一个 credentials() 辅助方法(在 environment 指令中使用),它 支持 secret 文本,带密码的用户名,以及 secret 文件凭据。
下面的流水线代码片段展示了如何创建一个使用带密码的用户名凭据的环境变量的流水线。
在该示例中,带密码的用户名凭据被分配了环境变量,用来使你的组织或团队以一个公用账户访问
Bitbucket 仓库;这些凭据已在 Jenkins 中配置了凭据 ID jenkins-bitbucket-common-creds 。 当在 environment 指令中设置凭据环境变量时:
environment {
BITBUCKET_COMMON_CREDS = credentials('jenkins-bitbucket-common-creds')
}
这实际设置了下面的三个环境变量:
- BITBUCKET_COMMON_CREDS - 包含一个以冒号分隔的用户名和密码,格式为 username:password 。
- BITBUCKET_COMMON_CREDS_USR - 附加的一个仅包含用户名部分的变量。
- BITBUCKET_COMMON_CREDS_PSW - 附加的一个仅包含密码部分的变量。
pipeline {
agent {
// 此处定义 agent 的细节
}
environment {
//顶层流水线块中使用的 environment 指令将适用于流水线中的所有步骤。
BITBUCKET_COMMON_CREDS = credentials('jenkins-bitbucket-common-creds')
}
stages {
stage('Example stage 1') {
//在一个 stage 中定义的 environment 指令只会将给定的环境变量应用于 stage 中
environment {
BITBUCKET_COMMON_CREDS = credentials('another-credential-id')
}
steps {
//
}
}
stage('Example stage 2') {
steps {
//
}
}
}
}
因此对Jenkinsfile做改造:
jenkins/pipelines/p5.yaml
pipeline {
agent { label '172.21.32.5'}
environment {
IMAGE_REPO = "172.21.32.13:5000/myblog"
DINGTALK_CREDS = credentials('dingTalk')
}
stages {
stage('printenv') {
steps {
echo 'Hello World'
sh 'printenv'
}
}
stage('check') {
steps {
checkout scm
}
}
stage('build-image') {
steps {
retry(2) { sh 'docker build . -t ${IMAGE_REPO}:${GIT_COMMIT}'}
}
}
stage('push-image') {
steps {
retry(2) { sh 'docker push ${IMAGE_REPO}:${GIT_COMMIT}'}
}
}
stage('deploy') {
steps {
sh "sed -i 's#{{IMAGE_URL}}#${IMAGE_REPO}:${GIT_COMMIT}#g' deploy/*"
timeout(time: 1, unit: 'MINUTES') {
sh "kubectl apply -f deploy/"
}
}
}
}
post {
success {
echo 'Congratulations!'
sh """
curl 'https://oapi.dingtalk.com/robot/send?access_token=${DINGTALK_CREDS_PSW}' \
-H 'Content-Type: application/json' \
-d '{"msgtype": "text",
"text": {
"content": "😄👍构建成功👍😄\n 关键字:luffy\n 项目名称: ${JOB_BASE_NAME}\n Commit Id: ${GIT_COMMIT}\n 构建地址:${RUN_DISPLAY_URL}"
}
}'
"""
}
failure {
echo 'Oh no!'
sh """
curl 'https://oapi.dingtalk.com/robot/send?access_token=${DINGTALK_CREDS_PSW}' \
-H 'Content-Type: application/json' \
-d '{"msgtype": "text",
"text": {
"content": "😖❌构建失败❌😖\n 关键字:luffy\n 项目名称: ${JOB_BASE_NAME}\n Commit Id: ${GIT_COMMIT}\n 构建地址:${RUN_DISPLAY_URL}"
}
}'
"""
}
always {
echo 'I will always say Hello again!'
}
}
}
本章小结
上面我们已经通过Jenkinsfile完成了最简单的项目的构建和部署,那么我们来思考目前的方式:
- 目前都是在项目的单一分支下进行操作,企业内一般会使用feature、develop、release、master 等多个分支来管理整个代码提交流程,如何根据不同的分支来做构建?
- 构建视图中如何区分不同的分支?
- 如何不配置webhook的方式实现构建?
- 如何根据不同的分支选择发布到不同的环境(开发、测试、生产)?
多分支流水线
我们简化一下流程,假如使用develop分支作为开发分支,master分支作为集成测试分支,看一下如何 使用多分支流水线来管理。
演示1:多分支流水线的使用
-
提交develop分支:
git checkout -b develop git push --set-upstream origin develop
-
登录gitlab,删除gitlab已有webhook
-
Jenkins端创建多分支流水线项目
- 增加git分支源
- 发现标签
- 根据名称过滤,develop|master|v.*
- 高级克隆,设置浅克隆
保存后,会自动检索项目中所有存在Jenkinsfile文件的分支和标签,若匹配我们设置的过滤正则表达 式,则会添加到多分支的构建视图中。所有添加到视图中的分支和标签,会默认执行一次构建任务。
演示2:美化消息通知内容
jenkins/pipelines/p6.yaml
pipeline {
agent { label '172.21.32.5'}
environment {
IMAGE_REPO = "172.21.32.13:5000/demo/myblog"
DINGTALK_CREDS = credentials('dingTalk')
TAB_STR = "\n \n "
}
stages {
stage('printenv') {
steps {
script{
sh "git log --oneline -n 1 > gitlog.file"
env.GIT_LOG = readFile("gitlog.file").trim()
}
sh 'printenv'
}
}
stage('checkout') {
steps {
checkout scm
script{
env.BUILD_TASKS = env.STAGE_NAME + "√..." + env.TAB_STR
}
}
}
stage('build-image') {
steps {
retry(2) { sh 'docker build . -t ${IMAGE_REPO}:${GIT_COMMIT}'}
script{
env.BUILD_TASKS += env.STAGE_NAME + "√..." + env.TAB_STR
}
}
}
stage('push-image') {
steps {
retry(2) { sh 'docker push ${IMAGE_REPO}:${GIT_COMMIT}'}
script{
env.BUILD_TASKS += env.STAGE_NAME + "√..." + env.TAB_STR
}
}
}
stage('deploy') {
steps {
sh "sed -i 's#{{IMAGE_URL}}#${IMAGE_REPO}:${GIT_COMMIT}#g' deploy/*"
timeout(time: 1, unit: 'MINUTES') {
sh "kubectl apply -f deploy/"
}
script{
env.BUILD_TASKS += env.STAGE_NAME + "√..." + env.TAB_STR
}
}
}
}
post {
success {
echo 'Congratulations!'
sh """
curl 'https://oapi.dingtalk.com/robot/send?access_token=${DINGTALK_CREDS_PSW}' \
-H 'Content-Type: application/json' \
-d '{
"msgtype": "markdown",
"markdown": {
"title":"myblog",
"text": "😄👍 构建成功 👍😄 \n**项目名称**:luffy \n**Git log**: ${GIT_LOG} \n**构建分支**: ${GIT_BRANCH} \n**构建地址**:${RUN_DISPLAY_URL} \n**构建任务**:${BUILD_TASKS}"
}
}'
"""
}
failure {
echo 'Oh no!'
sh """
curl 'https://oapi.dingtalk.com/robot/send?access_token=${DINGTALK_CREDS_PSW}' \
-H 'Content-Type: application/json' \
-d '{
"msgtype": "markdown",
"markdown": {
"title":"myblog",
"text": "😖❌ 构建失败 ❌😖 \n**项目名称**:luffy \n**Git log**: ${GIT_LOG} \n**构建分支**: ${GIT_BRANCH} \n**构建地址**:${RUN_DISPLAY_URL} \n**构建任务**:${BUILD_TASKS}"
}
}'
"""
}
always {
echo 'I will always say Hello again!'
}
}
}
演示3:通知gitlab构建状态
Jenkins端做了构建,可以通过gitlab通过的api将构建状态通知过去,作为开发人员发起Merge Request或者合并Merge Request的依据之一。
注意一定要指宾 gitLabConnection (gitlab),不然没法认证到 Gitlab。端
jenkins/pipelines/p7.yaml
pipeline {
agent { label '172.21.32.5'}
options {
buildDiscarder(logRotator(numToKeepStr: '10'))
disableConcurrentBuilds()
timeout(time: 20, unit: 'MINUTES')
gitLabConnection('gitlab')
}
environment {
IMAGE_REPO = "172.21.32.13:5000/demo/myblog"
DINGTALK_CREDS = credentials('dingTalk')
TAB_STR = "\n \n "
}
stages {
stage('printenv') {
steps {
script{
sh "git log --oneline -n 1 > gitlog.file"
env.GIT_LOG = readFile("gitlog.file").trim()
}
sh 'printenv'
}
}
stage('checkout') {
steps {
checkout scm
updateGitlabCommitStatus(name: env.STAGE_NAME, state: 'success')
script{
env.BUILD_TASKS = env.STAGE_NAME + "√..." + env.TAB_STR
}
}
}
stage('build-image') {
steps {
retry(2) { sh 'docker build . -t ${IMAGE_REPO}:${GIT_COMMIT}'}
updateGitlabCommitStatus(name: env.STAGE_NAME, state: 'success')
script{
env.BUILD_TASKS += env.STAGE_NAME + "√..." + env.TAB_STR
}
}
}
stage('push-image') {
steps {
retry(2) { sh 'docker push ${IMAGE_REPO}:${GIT_COMMIT}'}
updateGitlabCommitStatus(name: env.STAGE_NAME, state: 'success')
script{
env.BUILD_TASKS += env.STAGE_NAME + "√..." + env.TAB_STR
}
}
}
stage('deploy') {
steps {
sh "sed -i 's#{{IMAGE_URL}}#${IMAGE_REPO}:${GIT_COMMIT}#g' deploy/*"
timeout(time: 1, unit: 'MINUTES') {
sh "kubectl apply -f deploy/"
}
updateGitlabCommitStatus(name: env.STAGE_NAME, state: 'success')
script{
env.BUILD_TASKS += env.STAGE_NAME + "√..." + env.TAB_STR
}
}
}
}
post {
success {
echo 'Congratulations!'
sh """
curl 'https://oapi.dingtalk.com/robot/send?access_token=${DINGTALK_CREDS_PSW}' \
-H 'Content-Type: application/json' \
-d '{
"msgtype": "markdown",
"markdown": {
"title":"myblog",
"text": "😄👍 构建成功 👍😄 \n**项目名称**:luffy \n**Git log**: ${GIT_LOG} \n**构建分支**: ${BRANCH_NAME} \n**构建地址**:${RUN_DISPLAY_URL} \n**构建任务**:${BUILD_TASKS}"
}
}'
"""
}
failure {
echo 'Oh no!'
sh """
curl 'https://oapi.dingtalk.com/robot/send?access_token=${DINGTALK_CREDS_PSW}' \
-H 'Content-Type: application/json' \
-d '{
"msgtype": "markdown",
"markdown": {
"title":"myblog",
"text": "😖❌ 构建失败 ❌😖 \n**项目名称**:luffy \n**Git log**: ${GIT_LOG} \n**构建分支**: ${BRANCH_NAME} \n**构建地址**:${RUN_DISPLAY_URL} \n**构建任务**:${BUILD_TASKS}"
}
}'
"""
}
always {
echo 'I will always say Hello again!'
}
}
}
我们可以访问gitlab,然后找到commit记录,查看同步状态
提交merge request,也可以查看到相关的任务状态,可以作为项目owner合并代码的依据之一:
本章小节
优势:
- 根据分支展示, 视图人性化
- 自动检测各分支的变更
思考:
-
Jenkins的slave端,没有任务的时候处于闲置状态,slave节点多的话造成资源浪费
-
是否可以利用kubernetes的Pod来启动slave,动态slave pod来执行构建任务
k8s pod slave 集成 Jenkinsfile
- 对接 kubernetes 集群
- 使用 kubernetes 的 Pod-Template 来作为动态的 agent 执行 Jenkins 任务
- 如何制作 agent 容器实现不同类型的业务的集成
- 集成代码扫描、docker镜像自动构建、k8s服务部署、自动化测试
k8s pod slave 集成 Jenkinsfile
- 对接 kubernetes 集群
- 使用 kubernetes 的 Pod-Template 来作为动态的 agent 执行 Jenkins 任务
- 如何制作 agent 容器实现不同类型的业务的集成
- 集成代码扫描、docker镜像自动构建、k8s服务部署、自动化测试
jenkins 集成 Kubernetes
插件安装及配置 (普通版本)jenkins 在 eks 集群中部署时
-
[系统管理] -> [插件管理] -> [搜索kubernetes]->直接安装 若安装失败,请先更新 bouncycastle API Plugin并重新启动Jenkins
-
[系统管理] -> [系统配置] -> [Add a new cloud]
-
配置地址信息
-
Kubernetes 地址: https://kubernetes.default(或者https://172.21.32.13:6443) ;k8s api server 的地址,kubeconfig 中也有
-
Kubernetes 命名空间:jenkins ;命名空间是 jankins slave 启动时所在的 ns
-
服务证书不用写(我们在安装Jenkins的时候已经指定过serviceAccount),均使用默认
-
连接测试,成功会提示:Connection test successful J
-
jenkins地址:http://jenkins:8080(jenkins svc 地址)
-
-
配置Pod Template
- 名称:jnlp-slave
- 命名空间:jenkins
- Jenkins 通道 :jenkins:50000
- 标签列表:jnlp-slave,作为agent的label选择用
- 连接 Jenkins 的超时时间(秒) :300,设置连接jenkins超时时间
- 节点选择器:agent=true
- 工作空间卷:选择hostpath,设置/opt/jenkins_jobs/,注意需要设置chown -R 1000:1000 /opt/jenkins_jobs/权限,否则Pod没有权限
jenkins 在 eks 集群之外部署时,连接 eks
eks 之外的 jenkins 连接到 eks ,这个过程还是比较麻烦的。它涉及到 aws 的认证 + eks 的认证;
过程大概分为以下步骤:
- 首先需要在 eks 所在 的 aws 账号创建 iam 用户
- 授权iam用户对eks有操作权限,最小的权限为可读权限;
- 绑定 iam 用户到 eks 中的用户
- eks 用户 绑定 role 来分配权限;
- jenkins 机器配置 aws cli
- aws cli 更新 kubeconfig
- 将kubeconfig 导入jenkins,并测试 jenkins 连接 eks
- 配置 jenkins anget 端口(系统管理->全局安全配置->代理->指定端口(50000),可以这样配置。),然后jenkins kubenetes 中配置jenkins 连接地址为:http://<jenkins master 域名地址>:8080(实际测试用其他端口失败,包括nginx代理的8080,正在查找错误原因)
- 接下来就可以配置一个测试项目,测试 jenkins pipeline 构建了;
详情请参考链接
eks 线上 jenkins pipeline 配置
- 该项目中我们在一个pod中包含了多个容器
- jnlp 或者
jenkins/jnlp-slave:4.0.1-1
: jnlp 容器只是负责和 jenkins master 建立连接; - maven:3.8.1-jdk-8:负责打包
- docker:负责构建镜像,推送镜像
- awscli:负责获取 ecr 登录密钥;
- jnlp 或者
- 我们在pod中绑定了 serviceAccount:jenkins-agentd-pod-service-account,它提供 ecr 所必需的权限
- 我们指定了工作目录,workingDir: '/home/jenkins/agent',
- 指定了 runAsUser :0;因为默认 jenkins-agent 启动权限为1000,不然还得修改挂载目录的权限,这样就不用修改目录权限了
- 我们共享了 jenkins-agent-pvc:/root/.m2/repository 目录挂载,该文件中存有 maven 所依赖的包下载,挂载出来避免重复下载
- 我们配置了 maven-config:/root/.m2;将maven 的配置以 configmap 方式挂载出来,方便后期管理;
- 该任务中并没有配置 k8s 的部署,后期会加入;
// 之前的项目较老。所以一些配置使用的比较原始
podTemplate( serviceAccount:'jenkins-agentd-pod-service-account',
containers: [
containerTemplate(name: 'jnlp', image: 'jenkins/inbound-agent:latest', workingDir: '/home/jenkins/agent', privileged: 'true', runAsUser: '0'),
containerTemplate(name: 'maven', image: 'maven:3.8.1-jdk-8', ttyEnabled: true, command: 'cat', workingDir: '/home/jenkins/agent', privileged: 'true', runAsUser: '0'),
containerTemplate(name: 'docker', image: 'docker', ttyEnabled: true, command: 'cat', workingDir: '/home/jenkins/agent', privileged: 'true', runAsUser: '0'),
containerTemplate(name: 'awscli', image: 'amazon/aws-cli', ttyEnabled: true, command: 'cat', workingDir: '/home/jenkins/agent', privileged: 'true', runAsUser: '0'),
],
volumes: [
// # hostPathVolume(hostPath: '/tmp/maven/repository', mountPath:'/root/.m2/repository'),
persistentVolumeClaim(claimName: 'jenkins-agent-pvc', mountPath: '/root/.m2/repository'),
hostPathVolume(hostPath: '/var/run/docker.sock', mountPath:'/var/run/docker.sock'),
hostPathVolume(hostPath: '/etc/docker/daemon.json', mountPath:'/etc/docker/daemon.json'),
configMapVolume(configMapName: 'maven-config', mountPath:'/root/.m2')
],
)
{
node(POD_LABEL) {
stage('拉取 git仓库 代码') {
// 取消 ssl 检查
sh 'git config --global http.sslverify false'
sh 'git config --global https.sslverify false'
git credentialsId: '6df8b3dc-bb50-4464-8cc6-ae58b9148f66', url: 'https://gitea.nqspace.com/hanqunfeng/lx-novel.git'
}
stage('maven 打包') {
container('maven') {
stage('构建 Maven 项目') {
sh 'mvn -version'
sh 'mvn clean package -Dmaven.test.skip=true'
}
}
}
stage('构建docker镜像 grap_novel_data') {
container('awscli') {
stage('获取ecr登陆密钥') {
sh '''
pwd
aws ecr get-login-password --region us-west-2 > dockerLogin
cp dockerLogin novel-web-parent/grap_novel_data/target/
cp dockerLogin novel-web-parent/novel-boss/target/
cp dockerLogin novel-web-parent/novel-boss/target/
'''
}
}
container('docker') {
stage('build docker') {
sh 'pwd'
dir('novel-web-parent/grap_novel_data/target') {
sh '''
pwd
sh docker-push.sh
'''
}
}
}
}
stage('构建docker镜像 novel-boss') {
container('docker') {
stage('build docker') {
sh 'pwd'
dir('novel-web-parent/novel-boss/target') {
sh '''
pwd
sh docker-push.sh
'''
}
}
}
}
stage('构建docker镜像 novel-api') {
container('docker') {
stage('build docker') {
sh 'pwd'
dir('novel-web-parent/novel-api/target') {
sh '''
pwd
sh docker-push.sh
'''
}
}
}
}
}
}
Pod-Template 中容器镜像的制作
制作一个tools镜像,集成常用的工具,来完成常见的构建任务,需要注意的几
点:
- 使用alpine基础镜像,自身体积比较小
- 替换国内安装源
- 为了使用docker,安装了docker
- 为了克隆代码,安装git
- 为了后续做python的测试等任务,安装python环境
- 为了后面集成robotframework,安装了chromium-chromedriver等包
- 为了在容器中调用 kubectl 的命令,拷贝了kubectl的二进制文件
- 为了认证 kubectl,需要在容器内部生成 .kube 目录及 config 文件
$ mkdir tools;
$ cp `which kubectl` .
$ cp ~/.kube/config .
$ cat requirements.txt
robotframework
robotframework-seleniumlibrary
robotframework-databaselibrary
robotframework-requests
Dockerfile
jenkins/custom-images/tools/Dockerfile
FROM alpine
LABEL maintainer="inspur_lyx@hotmail.com"
USER root
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g'
/etc/apk/repositories && \
apk update && \
apk add --no-cache openrc docker git curl tar gcc g++ make \
bash shadow openjdk8 python python-dev py-pip openssl-dev libffi-dev \
libstdc++ harfbuzz nss freetype ttf-freefont chromium chromium-chromedriver
&& \
mkdir -p /root/.kube && \
usermod -a -G docker root
COPY requirements.txt /
COPY config /root/.kube/
RUN pip install -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host
mirrors.aliyun.com -r /requirements.txt
RUN rm -rf /var/cache/apk/* && \
rm -rf ~/.cache/pip
#-----------------安装 kubectl--------------------# COPY kubectl /usr/local/bin/
RUN chmod +x /usr/local/bin/kubectl
# ------------------------------------------------#
执行镜像构建并推送到仓库中
$ docker build . -t 172.21.32.13:5000/devops/tools:v1
$ docker push 172.21.32.13:5000/devops/tools:v1
我们可以直接使用该镜像做测试:
## 启动临时镜像做测试
$ docker run --rm -ti 172.21.32.13:5000/devops/tools:v1 bash # / git clone http://xxxxxx.git
# / kubectl get no
# / python3
#/ docker
## 重新挂载docker的sock文件
docker run -v /var/run/docker.sock:/var/run/docker.sock --rm -ti 172.21.32.13:5000/devops/tools:v1 bash
更新Jenkins中的 PodTemplate,添加tools镜像,注意同时要先添加名为jnlp的container,因为我们是 使用自定义的PodTemplate覆盖掉默认的模板:
在卷栏目,添加卷,Host Path Volume,不然在容器中使用docker会提示docker服务未启动
实践通过Jenkinsfile实现demo项目自动发布到kubenetes环境
tools容器做好后,我们需要对Jenkinsfile做如下调整:
jenkins/pipelines/p8.yaml
pipeline {
agent { label 'jnlp-slave'}
options {
buildDiscarder(logRotator(numToKeepStr: '10'))
disableConcurrentBuilds()
timeout(time: 20, unit: 'MINUTES')
gitLabConnection('gitlab')
}
environment {
IMAGE_REPO = "172.21.32.13:5000/myblog"
DINGTALK_CREDS = credentials('dingTalk')
TAB_STR = "\n \n "
}
stages {
stage('printenv') {
steps {
script{
sh "git log --oneline -n 1 > gitlog.file"
env.GIT_LOG = readFile("gitlog.file").trim()
}
sh 'printenv'
}
}
stage('checkout') {
steps {
container('tools') {
checkout scm
}
updateGitlabCommitStatus(name: env.STAGE_NAME, state: 'success')
script{
env.BUILD_TASKS = env.STAGE_NAME + "√..." + env.TAB_STR
}
}
}
stage('build-image') {
steps {
container('tools') {
retry(2) { sh 'docker build . -t ${IMAGE_REPO}:${GIT_COMMIT}'}
}
updateGitlabCommitStatus(name: env.STAGE_NAME, state: 'success')
script{
env.BUILD_TASKS += env.STAGE_NAME + "√..." + env.TAB_STR
}
}
}
stage('push-image') {
steps {
container('tools') {
retry(2) { sh 'docker push ${IMAGE_REPO}:${GIT_COMMIT}'}
}
updateGitlabCommitStatus(name: env.STAGE_NAME, state: 'success')
script{
env.BUILD_TASKS += env.STAGE_NAME + "√..." + env.TAB_STR
}
}
}
stage('deploy') {
steps {
container('tools') {
sh "sed -i 's#{{IMAGE_URL}}#${IMAGE_REPO}:${GIT_COMMIT}#g' deploy/*"
timeout(time: 1, unit: 'MINUTES') {
sh "kubectl apply -f deploy/"
}
}
updateGitlabCommitStatus(name: env.STAGE_NAME, state: 'success')
script{
env.BUILD_TASKS += env.STAGE_NAME + "√..." + env.TAB_STR
}
}
}
}
post {
success {
echo 'Congratulations!'
sh """
curl 'https://oapi.dingtalk.com/robot/send?access_token=${DINGTALK_CREDS_PSW}' \
-H 'Content-Type: application/json' \
-d '{
"msgtype": "markdown",
"markdown": {
"title":"myblog",
"text": "😄👍 构建成功 👍😄 \n**项目名称**:luffy \n**Git log**: ${GIT_LOG} \n**构建分支**: ${BRANCH_NAME} \n**构建地址**:${RUN_DISPLAY_URL} \n**构建任务**:${BUILD_TASKS}"
}
}'
"""
}
failure {
echo 'Oh no!'
sh """
curl 'https://oapi.dingtalk.com/robot/send?access_token=${DINGTALK_CREDS_PSW}' \
-H 'Content-Type: application/json' \
-d '{
"msgtype": "markdown",
"markdown": {
"title":"myblog",
"text": "😖❌ 构建失败 ❌😖 \n**项目名称**:luffy \n**Git log**: ${GIT_LOG} \n**构建分支**: ${BRANCH_NAME} \n**构建地址**:${RUN_DISPLAY_URL} \n**构建任务**:${BUILD_TASKS}"
}
}'
"""
}
always {
echo 'I will always say Hello again!'
}
}
}
集成sonarQube实现代码扫描
sonarqube架构简介
- SonarQube Scanner 扫描仪在本地执行代码扫描任务
- 执行完后,将分析报告被发送到SonarQube服务器进行处理
- SonarQube服务器处理和存储分析报告导致SonarQube数据库,并显示结果在UI中。
sonarqube on kubernetes环境搭建
- 资源文件准备
sonar/secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: sonar
namespace: jenkins
type: Opaque
data:
POSTGRES_USER: cm9vdA== # root
POSTGRES_PASSWORD: MTIzNDU2 # 123456
sonar/postgres.yaml
apiVersion: v1
kind: Service
metadata:
name: sonar-postgres
labels:
app: sonar-postgres
namespace: jenkins
spec:
ports:
- name: server
port: 5432
targetPort: 5432
protocol: TCP
selector:
app: sonar-postgres
---
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: jenkins
name: sonar-postgres
labels:
app: sonar-postgres
spec:
replicas: 1
selector:
matchLabels:
app: sonar-postgres
template:
metadata:
labels:
app: sonar-postgres
spec:
nodeSelector:
sonar: "true"
tolerations:
- operator: "Exists"
containers:
- name: postgres
image: 172.21.32.13:5000/postgres:11.4
imagePullPolicy: "IfNotPresent"
ports:
- containerPort: 5432
env:
- name: POSTGRES_DB #PostgreSQL 数据库名称
value: "sonar"
- name: POSTGRES_USER #PostgreSQL 用户名
valueFrom:
secretKeyRef:
name: sonar
key: POSTGRES_USER
- name: POSTGRES_PASSWORD #PostgreSQL 密码
valueFrom:
secretKeyRef:
name: sonar
key: POSTGRES_PASSWORD
resources:
limits:
cpu: 1000m
memory: 2048Mi
requests:
cpu: 500m
memory: 1024Mi
volumeMounts:
- mountPath: /var/lib/postgresql/data
name: postgredb
volumes:
- name: postgredb
hostPath:
path: /var/lib/postgres/
type: Directory
sonar/sonar.yaml
apiVersion: v1
kind: Service
metadata:
name: sonarqube
namespace: jenkins
labels:
app: sonarqube
spec:
ports:
- name: sonarqube
port: 9000
targetPort: 9000
protocol: TCP
selector:
app: sonarqube
---
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: jenkins
name: sonarqube
labels:
app: sonarqube
spec:
replicas: 1
selector:
matchLabels:
app: sonarqube
template:
metadata:
labels:
app: sonarqube
spec:
nodeSelector:
sonar: "true"
initContainers:
- command:
- /sbin/sysctl
- -w
- vm.max_map_count=262144
image: alpine:3.6
imagePullPolicy: IfNotPresent
name: elasticsearch-logging-init
resources: {}
securityContext:
privileged: true
containers:
- name: sonarqube
image: 172.21.32.13:5000/sonarqube:7.9-community
ports:
- containerPort: 9000
env:
- name: SONARQUBE_JDBC_USERNAME
valueFrom:
secretKeyRef:
name: sonar
key: POSTGRES_USER
- name: SONARQUBE_JDBC_PASSWORD
valueFrom:
secretKeyRef:
name: sonar
key: POSTGRES_PASSWORD
- name: SONARQUBE_JDBC_URL
value: "jdbc:postgresql://sonar-postgres:5432/sonar"
livenessProbe:
httpGet:
path: /sessions/new
port: 9000
initialDelaySeconds: 60
periodSeconds: 30
readinessProbe:
httpGet:
path: /sessions/new
port: 9000
initialDelaySeconds: 60
periodSeconds: 30
failureThreshold: 6
resources:
limits:
cpu: 2000m
memory: 4096Mi
requests:
cpu: 300m
memory: 512Mi
volumes:
- name: sonarqube-data
hostPath:
path: /opt/sonarqube/data
- name: sonarqube-logs
hostPath:
path: /opt/sonarqube/logs
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: sonarqube
namespace: jenkins
spec:
rules:
- host: sonar.qq.com
http:
paths:
- backend:
serviceName: sonarqube
servicePort: 9000
path: /
status:
loadBalancer: {}
## 创建secret
$ kubectl create -f secret.yaml
## 打算将sonarqube部署在哪个节点,打上label,sonar=true $ kubectl label node k8s-master sonar=true
## 到sonar=true的节点创建postgres目录
$ mkdir /var/lib/postgres/
# 创建postgres数据库
$ kubectl create -f postgres.yaml
## 创建sonarqube服务器
$ kubectl create -f sonar.yaml
## 配置本地hosts解析 152.136.62.143 sonar.qq.com
## 访问sonarqube,初始用户名密码为 admin/admin http://sonar.qq.com
- sonar-scanner的安装
下载地址: https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-c li-4.2.0.1873-linux.zip。该地址比较慢,可以在网盘下载(https://pan.baidu.com/s/1SiEhWyHikTiKl5lEMX1tJg 提取码: tqb9)。
-
演示sonar代码扫描功能
-
在项目根目录中准备配置文件 sonar-project.properties
sonar.projectKey=myblog sonar.projectName=myblog # if you want disabled the DTD verification for a proxy problem for example, true by default sonar.coverage.dtdVerification=false # JUnit like test report, default value is test.xml sonar.sources=.
-
配置sonarqube服务器地址
由于sonar-scanner需要将扫描结果上报给sonarqube服务器做质量分析,因此我们需要在
sonar-scanner中配置sonarqube的服务器地址:
在宿主机中测试,可以先用cluster-ip,先查看一下sonarqube的cluster-ip:
$ kubectl -n jenkins get svc|grep sonarqube sonarqube ClusterIP 10.105.94.31 <none> 9000/TCP $ cat sonar-scanner/conf/sonar-scanner.properties #----- Default SonarQube server #sonar.host.url=http://localhost:9000 sonar.host.url=http://10.105.94.31:9000 #----- Default source code encoding #sonar.sourceEncoding=UTF-8
-
执行扫描
## 在项目的根目录下执行 $ /opt/sonar-scanner-4.0.0.1744-linux/bin/sonar-scanner -X
-
sonarqube界面查看结果
登录sonarqube界面查看结果,Quality Gates说明
-
插件安装及配置
-
集成到tools容器中
由于我们的代码拉取、构建任务均是在tools容器中进行,因此我们需要把scanner集成到我们的 tools容器中,又因为scanner是一个cli客户端,因此我们直接把包解压好,拷贝到tools容器内 部,配置一下PATH路径即可,注意两点:
-
由于是在k8s集群中利用Pod去执行扫描任务,因此可以直接调用http://sonarqube:9000连 接服务端
-
由于tools已经集成了java环境,因此可以直接剔除scanner自带的jre
-
删掉sonar-scanner/jre目录
-
修改sonar-scanner/bin/sonar-scanner
use_embedded_jre=false
$ cd tools $ cp -r /opt/sonar-scanner-4.0.0.1744-linux/ sonar-scanner ## sonar配置,由于我们是在Pod中使用,因此可以直接配置: sonar.host.url=http://sonarqube:9000 $ cat sonar-scanner/conf/sonar-scanner.properties #----- Default SonarQube server sonar.host.url=http://sonarqube:9000 #----- Default source code encoding #sonar.sourceEncoding=UTF-8 $ rm -rf sonar-scanner/jre $ vi sonar-scanner/bin/sonar-scanner ... use_embedded_jre=false ...
Dockerfile
jenkins/custom-images/tools/Dockerfile2
FROM alpine LABEL maintainer="inspur_lyx@hotmail.com" USER root RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories && \ apk update && \ apk add --no-cache openrc docker git curl tar gcc g++ make \ bash shadow openjdk8 python python-dev py-pip openssl-dev libffi-dev \ libstdc++ harfbuzz nss freetype ttf-freefont chromium chromium-chromedriver && \ mkdir -p /root/.kube && \ usermod -a -G docker root COPY requirements.txt / COPY config /root/.kube/ RUN pip install -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com -r /requirements.txt RUN rm -rf /var/cache/apk/* && \ rm -rf ~/.cache/pip #-----------------安装 kubectl--------------------# COPY kubectl /usr/local/bin/ RUN chmod +x /usr/local/bin/kubectl # ------------------------------------------------# #---------------安装 sonar-scanner-----------------# COPY sonar-scanner /usr/lib/sonar-scanner RUN ln -s /usr/lib/sonar-scanner/bin/sonar-scanner /usr/local/bin/sonar-scanner && chmod +x /usr/local/bin/sonar-scanner ENV SONAR_RUNNER_HOME=/usr/lib/sonar-scanner # ------------------------------------------------#
重新构建镜像,并推送到仓库:
$ docker build . -t 172.21.32.13:5000/devops/tools:v2 $ docker push 172.21.32.13:5000/devops/tools:v2
-
-
-
修改Jenkins PodTemplate 为了在新的构建任务中可以拉取v2版本的tools镜像,需要更新PodTemplate
-
安装并配置sonar插件
由于sonarqube的扫描的结果需要进行Quality Gates的检测,那么我们在容器中执行完代码扫描 任务后,如何知道本次扫描是否通过了Quality Gates,那么就需要借助于sonarqube实现的 jenkins的插件。
-
安装插件
插件中心搜索sonarqube,直接安装
-
配置插件
系统管理->系统配置-> SonarQube servers ->Add SonarQube
- Name:sonarqube
- Server URL:http://sonarqube:9000
- Server authentication token
- 登录sonarqube -> My Account -> Security -> Generate Token
- 登录Jenkins,添加全局凭据,类型为Secret text
-
如何在jenkinsfile中使用
我们在 https://jenkins.io/doc/pipeline/steps/sonar/ 官方介绍中可以看到:
stage('build && SonarQube analysis') { steps { withSonarQubeEnv('sonarqube') { sh 'sonar-scanner -X' } } } stage("Quality Gate") { steps { timeout(time: 1, unit: 'HOURS') { // Parameter indicates whether to set pipeline to UNSTABLE if Quality Gate fails // true = set pipeline to UNSTABLE, false = don't } } }
-
Jenkinsfile集成sonarqube演示
jenkins/pipelines/p9.yaml
pipeline {
agent { label 'jnlp-slave'}
options {
buildDiscarder(logRotator(numToKeepStr: '10'))
disableConcurrentBuilds()
timeout(time: 20, unit: 'MINUTES')
gitLabConnection('gitlab')
}
environment {
IMAGE_REPO = "172.21.32.13:5000/demo/myblog"
DINGTALK_CREDS = credentials('dingTalk')
TAB_STR = "\n \n "
}
stages {
stage('git-log') {
steps {
script{
sh "git log --oneline -n 1 > gitlog.file"
env.GIT_LOG = readFile("gitlog.file").trim()
}
sh 'printenv'
}
}
stage('checkout') {
steps {
container('tools') {
checkout scm
}
updateGitlabCommitStatus(name: env.STAGE_NAME, state: 'success')
script{
env.BUILD_TASKS = env.STAGE_NAME + "√..." + env.TAB_STR
}
}
}
stage('CI'){
failFast true
parallel {
stage('Unit Test') {
steps {
echo "Unit Test Stage Skip..."
}
}
stage('Code Scan') {
steps {
container('tools') {
withSonarQubeEnv('sonarqube') {
sh 'sonar-scanner -X'
sleep 3
}
script {
timeout(1) {
def qg = waitForQualityGate('sonarqube')
if (qg.status != 'OK') {
error "未通过Sonarqube的代码质量阈检查,请及时修改!failure: ${qg.status}"
}
}
}
}
}
}
}
}
stage('build-image') {
steps {
container('tools') {
retry(2) { sh 'docker build . -t ${IMAGE_REPO}:${GIT_COMMIT}'}
}
updateGitlabCommitStatus(name: env.STAGE_NAME, state: 'success')
script{
env.BUILD_TASKS += env.STAGE_NAME + "√..." + env.TAB_STR
}
}
}
stage('push-image') {
steps {
container('tools') {
retry(2) { sh 'docker push ${IMAGE_REPO}:${GIT_COMMIT}'}
}
updateGitlabCommitStatus(name: env.STAGE_NAME, state: 'success')
script{
env.BUILD_TASKS += env.STAGE_NAME + "√..." + env.TAB_STR
}
}
}
stage('deploy') {
steps {
container('tools') {
sh "sed -i 's#{{IMAGE_URL}}#${IMAGE_REPO}:${GIT_COMMIT}#g' deploy/*"
timeout(time: 1, unit: 'MINUTES') {
sh "kubectl apply -f deploy/"
}
}
updateGitlabCommitStatus(name: env.STAGE_NAME, state: 'success')
script{
env.BUILD_TASKS += env.STAGE_NAME + "√..." + env.TAB_STR
}
}
}
}
post {
success {
echo 'Congratulations!'
sh """
curl 'https://oapi.dingtalk.com/robot/send?access_token=${DINGTALK_CREDS_PSW}' \
-H 'Content-Type: application/json' \
-d '{
"msgtype": "markdown",
"markdown": {
"title":"myblog",
"text": "😄👍 构建成功 👍😄 \n**项目名称**:luffy \n**Git log**: ${GIT_LOG} \n**构建分支**: ${BRANCH_NAME} \n**构建地址**:${RUN_DISPLAY_URL} \n**构建任务**:${BUILD_TASKS}"
}
}'
"""
}
failure {
echo 'Oh no!'
sh """
curl 'https://oapi.dingtalk.com/robot/send?access_token=${DINGTALK_CREDS_PSW}' \
-H 'Content-Type: application/json' \
-d '{
"msgtype": "markdown",
"markdown": {
"title":"myblog",
"text": "😖❌ 构建失败 ❌😖 \n**项目名称**:luffy \n**Git log**: ${GIT_LOG} \n**构建分支**: ${BRANCH_NAME} \n**构建地址**:${RUN_DISPLAY_URL} \n**构建任务**:${BUILD_TASKS}"
}
}'
"""
}
always {
echo 'I will always say Hello again!'
}
}
}
集成RobotFramework实现验收测试
链接地址;使用谷歌浏览器打开,有目录;
代码地址:cicd: cicd 配置 (gitee.com)
基于sharedLibrary进行CI/CD流程的优化
代码地址:[cicd: cicd 配置 (gitee.com)](
本文来自博客园, 作者:Star-Hitian, 转载请注明原文链接:https://www.cnblogs.com/Star-Haitian/p/16528758.html