利用Jenkins完成一次容器镜像的构建

前言

在2022年的当下, 微服务以及容器技术仍然是互联网行业的主流方向. 在真实的生产环境中, 要如何优雅地实现从代码到容器镜像这一步骤, 也俨然成了众多运维人员讨论得最多的话题.

在本文, 笔者将会基于一个简单的golang demo, 通过Jenkins工具实现一个完整的镜像构建流程.

笔者会假设读者已经有一定的相关基础(一个能用的jenkins服务, 以及docker运行环境, 还有代码仓库和镜像仓库), 只会着重介绍关于jenkins任务的一些细节.

整体思路

基于容器化的特点, 在设计CI/CD流程的时候, 简单来说我们需要考虑两个问题:

  • 构建镜像

  • 启动镜像

在本文, 我们先来解决第一个问题, 即镜像的构建, 也就是大家常说的持续集成(CI: Continuous integration).

在笔者所接触到的工作中, 镜像构建大概是这样一个流程, 这也是接下来我们要模拟的场景:

jenkins-ci

  1. 开发组组长在代码仓库中完成代码的合并, 打上标签(git tag)

  2. 在Jenkins网页中, 进入相应的项目页面, 填写必要的参数, 启动镜像构建任务. 而为了启动这个任务, 必要的参数有:

    • 代码仓库地址

    • 代码的版本信息(git tag)

    • 镜像版本标签

  3. 把构建好的镜像, 上传到镜像仓库, 必要的参数有:

    • 镜像仓库的地址

    • 镜像仓库的登录验证信息

    • 镜像版本标签

至此, 我们便完成了从代码到镜像的过程. 下面尝试着完成细节的设计工作.

流程设计

在上一篇文章: 设计一个golang项目的容器构建流程, 我们做了一个简单的golang demo, 并且已经写好了一个Dockerfile, 接下来我们就直接使用这个代码仓库去设计CI流程.

github地址: https://github.com/RondoChen/docker-go-demo.git

先从shell开始

就像只需要三个步骤就可以把大象塞进冰箱一样, 我们构建镜像也只需要三个步骤:

  1. git clone

  2. docker build

  3. docker push

具体到shell脚本, 大概只需要这几行(随便写写):

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash

git clone https://github.com/RondoChen/docker-go-demo.git

cd docker-go-demo

docker build -f ./Dockerfile.multistage -t rondochen/go-demo-app:1004 .

# 需要提前设置好镜像仓库的登录设置
docker login xxxxxxxxxxxxxxxx

docker push rondochen/go-demo-app:1004

引入Jenkins Pipeline

当我们要设计一个jenkins任务的时候, 拿着上面的shell脚本, 再添加一些参数变量的设置, 其实也差不多能凑合用了. 但是为了标准化(听上去厉害一点), 我在这里会使用jenkins的流水线(pipeline).

为了方便理解, 这里是没有花里胡哨的插件的版本, 这里可以清晰看出来, 还是用的三步走策略:

值得一提的就是, 我这里使用的镜像仓库是dockerhub, 为了能完成docker push, 需要在jenkins的系统设置中提前把自己的dockerhub登录信息填好.

jenkins-credentials

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

pipeline {
agent any
stages {
stage('pull code') {
steps {
sh '! test -d /tmp/docker-go-demo && (cd /tmp && git clone https://github.com/RondoChen/docker-go-demo.git) || (cd /tmp/docker-go-demo && git pull)'
}
}
stage('docker build') {
steps {
sh 'cd /tmp/docker-go-demo && docker build --tag rondochen/go-demo-jenkins-pipeline:20221004 -f Dockerfile.multistage .'
}
}
stage('docker push') {
steps {
withCredentials([usernamePassword(credentialsId: 'rondochen-dockerhub', passwordVariable: 'password', usernameVariable: 'username')]) {
sh """
docker login -u $username -p '$password' https://index.docker.io/v1/
docker push rondochen/go-demo-jenkins-pipeline:20221004
docker logout
"""
}
}
}
}
}

打磨

很明显, 上面的方案只是验证了最基本的流程, 我们还需要继续进行深度定制:

  1. 使任务带参数运行. 回顾上面的分析, 笔者设置了GIT_TAG, GIT_URL, IMAGE_TAG三个参数.

  2. 调用jenkins插件, 简化jenkins file. 在这里, 笔者的jenkins系统安装了git和docker插件.

经过打磨的jenkinsfile如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

pipeline {
agent any
environment {
registryCredential = 'rondochen-dockerhub'
}
stages {
stage('pull code') {
steps {
checkout([$class: 'GitSCM',
branches: [[name: '${GIT_TAG}']],
doGenerateSubmoduleConfigurations: false,
userRemoteConfigs: [[url: '${GIT_URL}']]
])
}
}
stage('docker build') {
steps {
script {
dockerfile = 'Dockerfile.multistage'
customImage = docker.build("${REPO_NAME}:${IMAGE_TAG}","-f ${dockerfile} .")
}
}
}
stage('docker push') {
steps {
script {
docker.withRegistry( '', registryCredential ) {
customImage.push()
}
}
}
}
}
}

当需要运行这个流水线任务的时候, 界面如下:

jenkins-pipeline

当运行结束之后, 就可以在镜像仓库中看到结果:

dockerhub

结语

终于是趁着国庆长假把搁置了好久的文章写完. 按照以往的习惯, 像这样的内容可能会分开好几篇文章配合很多操作上的细节慢慢写完, 但工作越来越忙, 最终只能草草了事. 被忽略掉的内容也不外乎是jenkins服务的搭建, 创建流水线时候的录屏操作, 各位读者如果已经到了要参考我的文章去设计CI/CD流程的阶段, 想必也早就领会这些基本操作, 这样一想倒也释然了.

结合笔者工作中遇到的讨论来看, 在设计CI/CD流程的时候, 最难的还是要在众多不同的项目中抽丝剥茧, 挖掘里面的共性, 进而进行大量的标准化约束.

以这个jenkins流水线为例子, 不难看出, 就算是换了别的代码别的项目, 只要代码仓库里面有一个能用的dockerfile, 我就可以复用这个jenkins任务.

而通过规范这个CI流程, 也顺路规范了运维和开发人员的工作边界, 即: 开发者对dockerfile负责, 运维对jenkinsfile负责, 这样在工作上也自然可以省下很多不必要的讨论.

如果后面再有时间, 会继续把CD部分讲完, 拭目以待.

拾遗

笔者使用了jenkins插件List Git Branches Parameter, 让jenkins自动获取代码仓库的分支和标签. 在撰文的时候, 发现这个插件现在存在安全漏洞(https://plugins.jenkins.io/list-git-branches-parameter/), 并且多年没有维护了, 大家在使用的时候应当考虑这个问题.

作为替代方案, 可以考虑使用groovy script:

1
2
3
4
def gettags = ("git ls-remote -t -h https://github.com/RondoChen/docker-go-demo.git").execute()
return gettags.text.readLines().collect {
it.split()[1].replaceAll('refs/heads/', '').replaceAll('refs/tags/', '').replaceAll("\\^\\{\\}", '')
}

groovy-script

扩展阅读

Jenkins官方文档