早在学习初期查看学习路线的时候, 就看到过CI/CD , 不过开始没有什么开发的经验 , 并不能体会到CI/CD的含义以及作用。

前段时间曾总结过一篇GithubAction与阿里云镜像仓库自动化实现Docker镜像构建与推送的文章 , 通过Github Action以及阿里云的Docker镜像仓库来进行Docker镜像的构建以及推送 , 接着在部署的时候只需要从镜像仓库中拉取镜像然后运行容器即可。

这样做一方面可以降低风险(只需要推送代码到指定分支就可以自动的去构建项目) , 也使得项目部署更加方便 (直接在服务器运行shell即可, 省去了临时去构建doker镜像的讨厌的操作) , 但是还有一点问题就是仍然需要人手动的去构建容器完整的CI/CD流程远非如此


今天在项目开发合并分支的时候, 项目构建的时候突然出现错误(后来发现是server顶不住了) , 不过通过项目组的大佬提前配置好的CI/CD流水线非常方便地来进行项目构建以及发布, 省去了大量的不必要的体力劳动。

计划顺着这个机会,从头来学习CI/CD的知识 , 并且通过GithubAction构建一个完整的CI/CD流水线

之前的GithubAction 配置在 adorabled4/hxBI , 可以先从这个看起, 一步步来理解

What is CI/CD?

CI/CD 是 Continuous Intergration/Continuous Deploy 的简称,翻译过来就是持续集成/持续部署

其中CD 也会被解释为持续交付(Continuous Delivery)。

简单来说 CI/CD 是一种通过在应用开发阶段引入自动化来频繁向客户交付应用的方法。

仔细看下面这张图

一项技术或者是一种产品的提出(or 发展) , 必然会对应着相应的场景以及一定的需求 , 那么CI/CD也有对应的他需要解决的问题

Was vernünftig ist, das ist wirklich; und was wirklich ist, das ist vernünftig.

----Hegel, Grundlinien der Philosophie des Rechts, Frankfurt am Main 1972, S. 11

CI

大师 Martin Fowler 对持续集成是这样定义的:持续集成是一种软件开发实践,即团队开发成员经常集成他们的工作,通常每个成员每天至少集成一次,也就意味着每天可能会发生多次集成。每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽快地发现集成错误。许多团队发现这个过程可以大大减少集成的问题,让团队能够更快的开发内聚的软件。

CD

CD(Continuous Delivery, 中文意思持续交付)是在持续集成的基础上,将集成后的代码部署到更贴近真实运行环境(类生产环境)中。

比如,我们完成单元测试后,可以把代码部署到连接数据库的Staging环境中更多的测试。

如果代码没有问题,可以继续手动部署到生产环境。

Jenkins

Jenkins是一个开源的、提供友好操作界面的持续集成(CI)工具,起源于Hudson(Hudson是商用的),主要用于持续、自动的构建/测试软件项目、监控外部任务的运行(这个比较抽象,暂且写上,不做解释)。

Jenkins用Java语言编写,可在Tomcat等流行的servlet容器中运行,也可独立运行。通常与版本管理工具(SCM)、构建工具结合使用。常用的版本控制工具有SVN、GIT,构建工具有Maven、Ant、Gradle。

通过GithubAction搭建CI/CD流水线

这里的主要目标是通过GithubAction + Jenkins + Docker 来进行

理想的步骤大概是:

  1. 编写代码
  2. 单元测试
  3. 提交代码(dev)
  4. 合并分支(比如dev -> test)

安装Docker&&Jenkins

安装Docker

Docker 分为 CE 和 EE 两大版本。CE 即社区版(免费,支持周期 7 个月),EE 即企业版,强调安全,付费使用,支持周期 24 个月。

Docker CE 分为 stable testnightly 三个更新频道。

官方网站上有各种环境下的 安装指南,这里主要介绍 Docker CE 在 CentOS上的安装。

通过命令启动docker

1
2
3
systemctl start docker  # 启动docker服务
systemctl stop docker # 停止docker服务
systemctl restart docker # 重启docker服务

然后输入命令,可以查看docker版本:

1
docker -v

如图:

1
2
3
4
5
[root@localhost ~]# docker -v
Docker version 20.10.21, build baeda1f
[root@localhost ~]# ps -ef | grep docker
root 35192 1 0 09:30 ? 00:00:00 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
root 35337 34160 0 09:30 pts/0 00:00:00 grep --color=auto docker

docker官方镜像仓库网速较差,我们需要设置国内镜像服务:

参考阿里云的镜像加速文档:https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors

编辑完文件之后可以通过cat 命令查看是否编辑成功

1
2
3
4
[root@localhost ~]# cat /etc/docker/daemon.json
{
"registry-mirrors": ["https://uvot3b1n.mirror.aliyuncs.com"]
}

Docker安装Jenkins

这里为了方便使用Docker来进行安装

更多容器部署命令参考 docker常用容器部署命令总结

拉取镜像 docker pull jenkins/jenkins

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[root@localhost ~]# docker pull jenkins/jenkins
Using default tag: latest
latest: Pulling from jenkins/jenkins
012c0b3e998c: Pull complete
08d5bc5026df: Pull complete
ced1909dae03: Pull complete
15e27b0be22f: Pull complete
3f68c4dffe66: Pull complete
e5ff73647592: Pull complete
a96a1d982b84: Pull complete
b24a00d8fb00: Pull complete
4099a34af478: Pull complete
daa8ff635634: Pull complete
086249825169: Pull complete
15a814ee2c77: Pull complete
12b571a61f13: Pull complete
Digest: sha256:498d2976f199fde8d26889356e702b6246c5482b8642d2f35395f2f29029dea0
Status: Downloaded newer image for jenkins/jenkins:latest
docker.io/jenkins/jenkins:latest
[root@localhost ~]# docker images | grep jenkins
jenkins/jenkins latest e600c78922cf 2 days ago 478MB

创建挂载文件

1
mkdir -p /mydata/jenkins/jenkins_home

给予权限 chmod 755 mydata/jenkins/jenkins_home

创建jenkins容器

1
2
3
4
5
6
7
8
9
docker run \
-d \
-uroot \
-p 11000:8080 \
-p 50000:50000 \
--name jenkins \
-v /mydata/jenkins/jenkins_home:/var/jenkins_home \
-v /etc/localtime:/etc/localtime \
jenkins/jenkins;

关于命令参数

参数 含义
-d 后台运行容器,并返回容器ID
-uroot 使用 root 身份进入容器,推荐加上,避免容器内执行某些命令时报权限错误
-p 11000:8080 将容器内8080端口映射至宿主机11000端口,这个是访问jenkins的端口
-p 50000:50000 将容器内50000端口映射至宿主机50000端口
–name jenkins 设置容器名称为jenkins
-v /home/jenkins_home:/var/jenkins_home :/var/jenkins_home 目录为容器jenkins工作目录,我们将硬盘上的一个目录挂载到这个位置,方便后续更新镜像后继续使用原来的工作目录
-v /etc/localtime:/etc/localtime 让容器使用和服务器同样的时间设置
jenkins/jenkins 镜像的名称,这里也可以写镜像ID

接着我们docker logs jenkins查看运行日志

保存好password

接着访问Jenkinshttp://${server_ip}:{port}

选择 安装推荐的插件

因为网络原因,需要将插件源设置为国内,

这样才可以安装插件。

进入宿主机目录 /mydata/jenkins/jenkins_home/,编辑文件 hudson.model.UpdateCenter.xml

修改 https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json

接着docker restart jenkins重启容器

安装推荐插件即可

创建账户

到这里我们完成了基本的Jenkins安装操作

image-20230923184116418

编写Demo项目

这里重点不在于代码编写, 因此编写的代码结构非常简单 !

接口代码

service

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class ReturnService {

public String buildHelloResult() {
return "<h1>Hello!This is DemoApplication</h1>";
}

public String buildHiResult() {
return "<h1>Hi!This is DemoApplication</h1>";
}

}

controller

1
2
3
4
5
6
7
8
9
10
11
12
@RequestMapping
@RestController
public class HelloWorldController {

@Resource
ReturnService service;

@GetMapping("/")
public String mockMethod() {
return service.buildHelloResult();
}
}

单元测试

这里的代码非常简单 , 测试的目的在于强调单元测试的重要性。

不写单测一时爽, 项目上线火葬场

  • 简单断言
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.dhx.demo.service;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;
@SpringBootTest
public class ReturnServiceTest {
@Resource
ReturnService service;

@Test
public void Test() {
assert service.buildHelloResult().contains("Hello");
assert service.buildHiResult().contains("Hi");
}
}

编写Dockerfile

如果你不熟悉Dockerfile的部署操作 , 建议先阅读 Docker部署Springboot+React项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
FROM openjdk:8-jdk-alpine
LABEL maintainer="adorabled4 <dhx2648466390@163.com>"

# 设置时区
RUN apk add --no-cache tzdata && \
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
echo "Asia/Shanghai" > /etc/timezone && \
apk del tzdata

# 创建工作目录
RUN mkdir -p /app/demo

# 将所有jar文件添加到对应模块的目录中 => 需要注意的是 , TODO 构建的时候是以dockerfile所在的目录开始的
# 把 jar 复制到当前的目录
COPY target/demo.jar .

# 暴露端口号
EXPOSE 8080

# 运行所有jar文件
CMD ["sh", "-c", "java -jar demo.jar --spring.profiles.active=prod"]

这里主要需要修改pom.xml , 设置生成的jar包的名称是项目的名称。

在准备好基本的项目代码之后 , 我们把代码托管到github , 这里我的是https://github.com/adorabled4/demo

Jenkins搭建

接下来就开始准备Jenkins的相关配置。

环境配置

首先是进行环境相关的配置 , 这里由于我们使用docker来搭建jenkins , 部分的环境以及配置好了

1
2
3
4
5
6
root@54eec85aa964:~/.ssh# java -version
openjdk version "11.0.20.1" 2023-08-24
OpenJDK Runtime Environment Temurin-11.0.20.1+1 (build 11.0.20.1+1)
OpenJDK 64-Bit Server VM Temurin-11.0.20.1+1 (build 11.0.20.1+1, mixed mode)
root@54eec85aa964:~/.ssh# mvn -v
bash: mvn: command not found

创建item

接着在Jenkins中配置相关的信息

General
  • Github 项目url

丢弃旧的构建: 服务器资源是有限的,有时候保存了太多的历史构建,会导致Jenkins速度变慢,并且服务器硬盘资源也会被占满。当然下方的"保持构建天数" 和 保持构建的最大个数是可以自定义的,需要根据实际情况确定一个合理的值。

这里我们需要使用Git来进行版本控制 , 下面的 配置秘钥 请仔细配置

配置秘钥

关于秘钥的工作流程

比如有两个用户Alice和Bob,Alice想把一段明文通过双钥加密的技术发送给Bob,Bob有一对公钥和私钥,那么加密解密的过程如下:

  1. Bob将他的公开密钥传送给Alice
  2. Alice用Bob的公开密钥加密她的消息,然后传送给Bob
  3. Bob用他的私人密钥解密Alice的消息

一个公钥对应一个私钥

密钥对中,让大家都知道的是公钥,不告诉大家,只有自己知道的,是私钥

如果用其中一个密钥加密数据,则只有对应的那个密钥才可以解密

如果用其中一个密钥可以进行解密数据,则该数据必然是对应的那个密钥进行的加密

使用Docker部署jenkins如果直接拉取的话是没有权限的, 因此我们需要进行相关的权限配置。

  1. 删除/data/jenkins/workspace/project-name后重新构建

  2. 通过git取消代理设置

    1
    2
    git config --global --unset http.proxy
    git config --global --unset https.proxy

上面的两种方法偶尔还是会存在问题 , 最终的解决办法是通过配置公钥与私钥

首先我们进入容器docker exec -it jenkins /bin/bash

接着ssh-keygen 生成公钥与私钥

注意把邮箱替换为你的Github中使用的邮箱 ssh-keygen -t rsa -b 4096 -C dhx2648466390@163.com

秘钥的地址为/root/.ssh

1
2
3
4
root@54eec85aa964:~/.ssh# pwd
/root/.ssh
root@54eec85aa964:~/.ssh# ls
id_rsa id_rsa.pub

接下来我们在Github中配置公钥 , 在Jenkins中配置私钥

使用cat命令,查看公钥

1
cat /root/.ssh/id_rsa.pub

GitHub配置生成好的公钥。 下面的链接是配置公钥的路径!

1
https://github.com/settings/ssh/new

cat /root/.ssh/id_rsa 查看私钥

这里出现了ssh: connect to host github.com port 22: Network is unreachable报错

这里是访问不了github.com的22端口 , 那么我们尝试更换端口

尝试在~/.ssh/config文件 中添加如下的内容

1
2
3
Host github.com
Hostname ssh.github.com
Port 443

这里容器中没有vim vi命令 , 需要我们手动进行安装

1
2
3
4
5
6
#先更新apt,如果root用户 sudo 就不需要加
root@96ea9752bc25:/# apt-get update
#下载,中途会有一个y/n 请输入y
root@96ea9752bc25:/# apt-get install vim
#安装完成,会有版本信息,退出信息界面请安ESC 然后:q! 回车
root@96ea9752bc25:/# vi -v

接着我们

1
2
cd ~/.ssh
vim config

然后粘贴

1
2
3
Host github.com
Hostname ssh.github.com
Port 443

到文件中即可。

源码管理
  • Git -> 配置

  • 指定代码分支

Github hook

构建触发器选择Github hook。

image-20230924112404547

Payload的格式为http://jenkins_username:jenkins_password@ip/github-webhook/

参考