作者 | 倚天码农
责编 | 刘静
出品 | CSDN 博客
要想理解持续集成和持续部署,先要了解它的部分组成,以及各个组成部分之间的关系。下面这张图是我见过的最简洁、清晰的持续部署和集成的关系图。 图片来源(https://www.sonatype.com/products-overview) 持续部署 如图所示,开发的流程是这样的: 程序员从源码库(Source Control)中下载源代码,编写程序,完成后提交代码到源码库,持续集成(Continuous Integration)工具从源码库中下载源代码,编译源代码,然后提交到运行库(Repository),然后持续交付(Continuous Delivery)工具从运行库(Repository)中下载代码,生成发布版本,并发布到不同的运行环境(例如DEV,QA,UAT, PROD)。 图中,左边的部分是持续集成,它主要跟开发和程序员有关;右边的部分是持续部署,它主要跟测试和运维有关。持续交付(Continuous Delivery)又叫持续部署(Continuous Deployment),它们如果细分的话还是有一点区别的,但我们这里不分得那么细,统称为持续部署。本文侧重讲解持续部署。 持续集成和部署有下面几个主要参与者:源代码库:负责存储源代码,常用的有Git和SVN;
持续集成与部署工具:负责自动编译和打包以及把可运行程序存储到可运行库。比较流行的有Jenkins,GitLab,Travis CI,CircleCI 等;
库管理器(Repository Manager):也就是图中的Repository,我们又叫运行库,负责管理程序组件。最常用的是Nexus。它是一个私有库,它的作用是管理程序组件。
管理第三方库:应用程序常常要用到很多第三方库,并且不同的技术栈需要的库不同,它们经常是存放在第三方公共库里,管理起来不是很方便。一般公司会建立一个私有管理库,来集中统一管理各种第三方软件,例如它既可以做为Maven库(Java),也可以做为镜像库(Docker),还可以做为NPM库(JavaScript),来保证公司软件的规范性;
管理内部程序的交付:所有公司在各种环境(例如DEV,QA,UAT, PROD)发布的程序都由它来管理,并赋予统一的版本号,这样任何交付都有据可查,同时便利于程序回滚。
下载源码:从源代码库(例如github)中下载源代码;
编译代码:编译语言都需要有这一步;
测试:对程序进行测试;
生成镜像:这里包含两个步骤,一个是创建镜像,另一个是存储镜像到镜像库;
部署镜像:把生成的镜像部署到容器上。
下载源码:从github下载源代码到Jenkins的运行环境;
测试:这一步暂时没有实际内容;
生成镜像:创建镜像,并上传到Docker hub;
部署镜像:将生成的镜像部署到k8s。
FROM jenkins/jenkins:lts
USER root
ENV DOCKERVERSION=19.03.4
RUN curl -fsSLO https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKERVERSION}.tgz \
&& tar xzvf docker-${DOCKERVERSION}.tgz --strip 1 \
-C /usr/local/bin docker/docker \
&& rm docker-${DOCKERVERSION}.tgz
RUN curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl \
&& chmod x ./kubectl \
&& mv ./kubectl /usr/local/bin/kubectl
上面的镜像在“jenkins/jenkins:lts”的基础上又安装了Docker和kubectl,这样就支持这两个软件了。镜像里使用的是docker的19.03.4版本。这里装的只是“Docker CLI”,没有Docker引擎。用的时候还是要把虚拟机的卷挂载到容器上,使用虚机的Docker引擎。因此最好保证容器里的Docker版本和虚机的Docker版本一致。 使用如下命令查看Docker版本:
vagrant@ubuntu-xenial:/$ docker version
详细情况请参见 Configure a CI/CD pipeline with Jenkins on Kubernetes 准备工作已经完成,现在要正式创建Jenkins项目: Jenkins脚本: 项目的创建是在Jenkins的主页上来完成,它的名字是“jenkins-k8sdemo”,它的最主要部分是脚本代码,它也跟Go程序存放在相同的源码库中,文件的名字也是“jenkins-k8sdemo”。项目的脚本页面如下图所示。
如果你不熟悉安装和创建Jenkins项目,请参阅在k8s上安装Jenkins及常见问题
下面就是jenkins-k8sdemo脚本文件:def POD_LABEL = "k8sdemopod-${UUID.randomUUID().toString()}"
podTemplate(label: POD_LABEL, cloud: 'kubernetes', containers: [
containerTemplate(name: 'modified-jenkins', image: 'jfeng45/modified-jenkins:1.0', ttyEnabled: true, command: 'cat')
],
volumes: [
hostPathVolume(mountPath: '/var/run/docker.sock', hostPath: '/var/run/docker.sock')
]) {
node(POD_LABEL) {
def kubBackendDirectory = "/script/kubernetes/backend"
stage('Checkout') {
container('modified-jenkins') {
sh 'echo get source from github'
git 'https://github.com/jfeng45/k8sdemo'
}
}
stage('Build image') {
def imageName = "jfeng45/jenkins-k8sdemo:${env.BUILD_NUMBER}"
def dockerDirectory = "${kubBackendDirectory}/docker/Dockerfile-k8sdemo-backend"
container('modified-jenkins') {
withCredentials([[$class: 'UsernamePasswordMultiBinding',
credentialsId: 'dockerhub',
usernameVariable: 'DOCKER_HUB_USER',
passwordVariable: 'DOCKER_HUB_PASSWORD']]) {
sh """
docker login -u ${DOCKER_HUB_USER} -p ${DOCKER_HUB_PASSWORD}
docker build -f ${WORKSPACE}${dockerDirectory} -t ${imageName} .
docker push ${imageName}
"""
}
}
}
stage('Deploy') {
container('modified-jenkins') {
sh "kubectl apply -f ${WORKSPACE}${kubBackendDirectory}/backend-deployment.yaml"
sh "kubectl apply -f ${WORKSPACE}${kubBackendDirectory}/backend-service.yaml"
}
}
}
}
我们逐段看一下代码: 设定容器镜像:
podTemplate(label: POD_LABEL, cloud: 'kubernetes', containers: [
containerTemplate(name: 'modified-jenkins', image: 'jfeng45/modified-jenkins:1.0', ttyEnabled: true, command: 'cat')
],
volumes: [
hostPathVolume(mountPath: '/var/run/docker.sock', hostPath: '/var/run/docker.sock')
])
这里设定Jenkins子节点Pod的容器镜像,用的是“jfeng45/modified-jenkins:1.0”,也就是我们在上个步骤创建的。所有的脚本里的步骤(stage)都用的是这个镜像。“volumes:”用来挂载卷到Jenkins容器中,这样Jenkins子节点就可以使用虚机的Docker引擎。 关于Jenkins脚本命令和设置挂载卷请参阅jenkinsci/kubernetes-plugin 创建镜像: 下面的代码生成Go程序的Docker镜像文件,这里我们没有用Docker插件,而是直接调用Docker命令,它的好处后面会讲到。它引用了我们前面设置的“Docker hub”的凭证去访问Docker库。在脚本里,我们先登录到“Docker hub”,然后使用上一步从GitHub下载的源代码来创建镜像,最后上传镜像到“Docker hub”。其中“WORKSPACE”是Jenkins预定义变量,从GitHub下载的源代码就存放在“ {WORKSPACE}”是Jenkins预定义变量,从GitHub下载的源代码就存放在“WORKSPACE”是Jenkins预定义变量,从GitHub下载的源代码就存放在“{WORKSPACE}”里。
stage('Build image') {
def imageName = "jfeng45/jenkins-k8sdemo:${env.BUILD_NUMBER}"
def dockerDirectory = "${kubBackendDirectory}/docker/Dockerfile-k8sdemo-backend"
container('modified-jenkins') {
withCredentials([[$class: 'UsernamePasswordMultiBinding',
credentialsId: 'dockerhub',
usernameVariable: 'DOCKER_HUB_USER',
passwordVariable: 'DOCKER_HUB_PASSWORD']]) {
sh """
docker login -u ${DOCKER_HUB_USER} -p ${DOCKER_HUB_PASSWORD}
docker build -f ${WORKSPACE}${dockerDirectory} -t ${imageName} .
docker push ${imageName}
"""
}
}
}
如果你想了解Jenkins命令详情,请参阅 Set Up a Jenkins CI/CD Pipeline with Kubernetes 我们这里并没有重新生成Go程序的镜像文件,而是复用了以前就有的k8s创建Go程序的镜像文件,Go程序的镜像文件路径是“\script\kubernetes\backend\docker\Dockerfile-k8sdemo-backend”。 它的代码如下。后面还会讲到这样做的好处。
# vagrant@ubuntu-xenial:~/app/k8sdemo/script/kubernetes/backend$
# docker build -t k8sdemo-backend .
FROM golang:latest as builder
# Set the Current Working Directory inside the container
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
WORKDIR /app/cmd
# Build the Go app
#RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main.exe
RUN go build -o main.exe
######## Start a new stage from scratch #######
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
RUN mkdir /lib64 && ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2
# Copy the Pre-built binary file from the previous stage
COPY --from=builder /app/cmd/main.exe .
# Command to run the executable
# CMD exec /bin/bash -c "trap : TERM INT; sleep infinity & wait"
CMD
关于Go镜像文件详情,请参阅创建优化的Go镜像文件以及踩过的坑
部署镜像: 下面部署Go程序到k8s上,这里也没有用kubectl插件,而是直接用kubectl命令调用已经存在的k8s的部署和服务配置文件(文件里会引用生成的Go镜像),它的好处后面也会讲到。 stage('Deploy') {
container('modified-jenkins') {
sh "kubectl apply -f ${WORKSPACE}${kubBackendDirectory}/backend-deployment.yaml"
sh "kubectl apply -f ${WORKSPACE}${kubBackendDirectory}/backend-service.yaml"
}
}
关于k8s的部署和服务配置文件详情,请参阅把应用程序迁移到k8s需要修改什么?
为什么没用Declarative? 用脚本来写Pipeline有两种方法,“Scripted Pipleline”和“Declarative Pipleline”,这里用的是第一种方法。“Declarative Pipleline”是新的方法,之所以没用它,是因为开始用的是Declarative模式但没调出来,然后就改用“Scripted Pipleline”,结果成功了。后来才发现设置Declarative的方法,特别是如何挂载卷,但看了一下,比起“Scripted Pipleline”要复杂不少,就偷了一下懒,没有再改。 如果你想知道怎样在Declarative模式下设置挂载卷,请参阅 Jenkins Pipeline Kubernetes Agent shared Volumes 自动执行项目: 现在的Jenkins中的项目需要手动启动,如果你需要自动启动项目的话就要创建webhook,GitHub和dockerhub都支持webhook,在它们的页面上都有设置选项。“webhook”是一个反向调用的URL,每当有新的代码或镜像提交时,GitHub和dockerhub都会调用这个URL,URL被设置成Jenkins的项目地址,这样相关的项目就会自动启动。 检验结果: 现在Jenkins的项目就完全配置好了,需要运行项目,检验结果。启动项目后, 查看“Console Output”,下面是部分输出(全部输出太长,请看附录),说明部署成功。。。。
kubectl apply -f /home/jenkins/workspace/test1/script/kubernetes/backend/backend-deployment.yaml
deployment.apps/k8sdemo-backend-deployment created
[Pipeline] sh kubectl apply -f /home/jenkins/workspace/test1/script/kubernetes/backend/backend-service.yaml
service/k8sdemo-backend-service created
[Pipeline] }
[Pipeline] // container
[Pipeline] }
[Pipeline] // stage
[Pipeline] }
[Pipeline] // node
[Pipeline] }
[Pipeline] // podTemplate
[Pipeline] End of Pipeline
Finished: SUCCESSstage('Deploy') {
container('modified-jenkins') {
sh "kubectl apply -f ${WORKSPACE}${kubBackendDirectory}/backend-deployment.yaml"
sh "kubectl apply -f ${WORKSPACE}${kubBackendDirectory}/backend-service.yaml"
}
}
查看运行结果:
获得Pod名字:vagrant@ubuntu-xenial:/home$ kubectl get pod
NAME READY STATUS RESTARTS AGE
envar-demo 1/1 Running 15 32d
k8sdemo-backend-deployment-6b99dc6b8c-8kxt9 1/1 Running 0 50s
k8sdemo-database-deployment-578fc88c88-mm6x8 1/1 Running 9 20d
k8sdemo-jenkins-deployment-675dd574cb-r57sb 1/1 Running 0 2d23h
登录Pod并运行程序:
vagrant@ubuntu-xenial:/home$ kubectl exec -ti k8sdemo-backend-deployment-6b99dc6b8c-8kxt9 -- /bin/sh
~ # ./main.exe
DEBU[0000] connect to database
DEBU[0000] dataSourceName:dbuser:dbuser@tcp(k8sdemo-database-service:3306)/service_config?charset=utf8
DEBU[0000] FindAll()
DEBU[0000] created=2019-10-21
DEBU[0000] find user:{1 Tony IT 2019-10-21}
DEBU[0000] find user list:[{1 Tony IT 2019-10-21}]
DEBU[0000] user lst:[{1 Tony IT 2019-10-21}]
结果正确。
Jenkins原理 实例部分已经结束,下面来探讨最佳实践。在这之前,先要搞清楚Jenkins的原理。 可执行命令 我一直有一个问题就是那些命令是Jenkins可以通过shell执行的?Jenkins和Docker、k8s不同,后者有自己的一套命令,只要把它们学会了就行了。而Jenkins是通过与别的系统集成来工作的,因此它的可执行命令与其他系统有关,这导致了你很难知道那些命令是可以执行的,那些不行。你需要弄懂它的原理,才能得到答案。当Jenkins执行脚本时,主节点会自动生成一个子节点(Docker容器),所有的Jenkins命令都是在这个容器里执行的。所以能执行的命令与容器密切相关。一般来讲,你可以通过shell来运行Linux命令。那下面的问题就来了:为什么我不能用Bash?
因为你使用的子节点的容器可能使用的是精简版的Linux,例如Alpine,它是没有Bash的。为什么我不能运行Docker命令或Kubectl?
app = docker.build("jfeng45/jenkins-k8sdemo")
但创建Docker镜像文件命令有许多参数选项,例如,你的镜像文件名不是Dockerfile,并且目录不是在项目根目录下,应如何写呢?这在以前的版本是不支持的,后来的版本支持了,但毕竟不太方便,还要学新的命令。最好的办法是能直接使用Docker命令,这样就完美的解决了上面说的三个问题。答案就在前面讲的Jenkins原理里,其实绝大多数插件都是不需要的,你只要自己创建一个Jenkins子节点容器,并安装相应的软件就能圆满解决。 下面是使用插件的脚本和不使用的对比,不使用的看起来更长,那时因为使用插件的脚本和Jenkins里的凭证设置有更好的集成,而不使用的脚本没有。但除了这个小缺点,其他方面不使用的脚本都要远远优于使用插件的。 使用插件的脚本(用插件命令):
stage('Create Docker images') {
container('docker') {
app = docker.build("jfeng45/codedemo", "-f ${WORKSPACE}/script/kubernetes/backend/docker/Dockerfile-k8sdemo-test .")
docker.withRegistry('', 'dockerhub') {
// Push image and tag it with our build number for versioning purposes.
app.push("${env.BUILD_NUMBER}")
}
}
}
不使用插件的脚本(直接用Docker命令):
stage('Create a d ocker image') {
def imageName = "jfeng45/codedemo:${env.BUILD_NUMBER}"
def dockerDirectory = "${kubBackendDirectory}/docker/Dockerfile-k8sdemo-backend"
container('modified-jenkins') {
withCredentials([[$class: 'UsernamePasswordMultiBinding',
credentialsId: 'dockerhub',
usernameVariable: 'DOCKER_HUB_USER',
passwordVariable: 'DOCKER_HUB_PASSWORD']]) {
sh """
docker login -u ${DOCKER_HUB_USER} -p ${DOCKER_HUB_PASSWORD}
docker build -f ${WORKSPACE}${dockerDirectory} -t ${imageName} .
docker push ${imageName}
"""
}
}
}
尽量多使用k8s和Dcoker 例如我们要创建一个应用程序的镜像,我们可以写一个Docker文件,并在Jenkins脚本里调用这个Docker文件来创建,也可以写一个Jenkins脚本,在脚本里来创建镜像。比较好的方法是前者。因为Docker和k8s都是事实上的标准,移植起来很方便。 Jenkins脚本的代码越少越好 如果你认同前面两个原则,那么这一条就是顺理成章的,原因也和上面是一样的。 常见问题 1.变量要放在双引号里 Jenkins的脚本即可以使用单引号也可以使用双引号,但如果你在引号里引用了变量,那么就要使用双引号。 正确的命令:
sh "kubectl apply -f ${WORKSPACE}${kubBackendDirectory}/backend-deployment.yaml"
错误的命令:
sh 'kubectl apply -f ${WORKSPACE}${kubBackendDirectory}/backend-deployment.yaml'
2.docker not found 如果Jenkins的容器里没有Docker,但你又调用了Docker命令,那么“Console Output”里就会有如下错误:
docker inspect -f . k8sdemo-backend:latest
/var/jenkins_home/workspace/k8sdec@2@tmp/durable-01e26997/script.sh: 1: /var/jenkins_home/workspace/k8sdec@2@tmp/durable-01e26997/script.sh: docker: not found
3.Jenkins宕机了 在调试Jenkins时,我新创建了一个镜像文件并上传到“Docker hub”之后就发现Jenkins宕机了。检查了Pod,发现了问题,k8s找不到Jenkins的镜像文件了(镜像文件从磁盘上消失了)。因为Jenkins的部署文件的设置是“imagePullPolicy: Never”,所以一旦镜像没有了,它不会自动重新下载。后来找到了原因,Vagrant的默认磁盘大小是10G,如果空间不够,它会自动从磁盘上删除其他镜像文件,腾出空间,结果就把Jenkins的镜像文件给删了,解决方案是扩充Vagrant的磁盘大小。 下面是修改之后的Vagrantfile,把磁盘空间改成了16G。
Vagrant.configure(2) do |config|
。。。
config.vm.box = "ubuntu/xenial64"
config.disksize.size = '16GB'
。。。
end
详情请见 How can I increase disk size on a Vagrant VM? 源码 完整源码的github链接 下面是项目中与本文有关的部分:
不堆砌术语,不罗列架构,不迷信权威,不盲从流行,坚持独立思考 版权声明:本文为CSDN博主「 倚天码农 」的原创文章。 原文链接:https://blog.csdn.net/weixin_38748858/article/details/102967540
更多精彩推荐
☞滴滴 AI Labs 负责人叶杰平因个人原因即将离职!CTO 张博接任
☞TIOBE 9 月编程语言:C++ 突起、Java 流行度下降
☞被劝退的学渣,逆袭成高级语言之父,改变编程方式却说“不喜欢写代码”!
☞5年5亿美金,华为昇腾如何构建全行业AI生态?
☞该买哪家二手手机呢?程序员爬取京东告诉你!
☞总计2171个BTC被盗,这个钱包漏洞的受害者越来越多
点分享点点赞点在看