在DevOps工作流中,CICD持续集成持续部署是重要的一环。GitLab CI/CD 是 GitLab 的一部分,因此不需要单独安装。
如果你已经在使用 GitLab 进行代码管理,GitLab CI/CD 无缝集成,直接在同一个平台上管理代码、审查合并请求、自动化测试和部署。
这种集成度简化了开发流程,使用起来非常方便。

前提条件:已经在使用Gitlab做代码管理、机器已安装Docker环境
目标:Docker搭建gitlab-runner,用gitgitlab-ci.yml脚本、Maven打包Springboot微服务、上传到Docker镜像仓库、远程部署到K8s集群

docker安装gitlab-runner

在Gitlab中Runner可以注册到 项目(Project)、群组(Group) 或 所有项目(All projects),这里注册到最大的,所有项目和组都可以用这个Runner

还需要查看当前在用的Gitlab的安装版本,选择一致的版本。避免gitlab-runner版本跨度过大导致出现问题。

1.先注册生成得到一个配置文件config.toml

1
2
// 注意替换成自己的版本号,然后回车,开始注册runner到gitlab流程
docker run --rm -it -v $(pwd)/runner-config:/etc/gitlab-runner gitlab/gitlab-runner:v16.11.3 register
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
// 第一步:输入gitlab的访问地址即可
Enter the GitLab instance URL (for example, https://gitlab.com/):
http://192.168.168.168:8888

// 第二步:输入 从gitlab管理页面上的复制token
Enter the registration token:
pAovXYzGHsy5EWF7zKiU

// 第三步:输入runner名称 比如:docker-microservice-runner
Enter a description for the runner:
[94030fed2ba3]: docker-microservice-runner

// 第四步:输入Tag 比如:blog
Enter tags for the runner (comma-separated):
blog

// 第五步:输入说明备注 直接回车跳过
Enter optional maintenance note for the runner:

WARNING: Support for registration tokens and runner parameters in the 'register' command has been deprecated in GitLab Runner 16.11 and will be replaced with support for authentication tokens. For more information, see https://gitlab.com/gitlab-org/gitlab/-/issues/380872
Registering runner... succeeded runner=pAovXYzG

// 第六步:这个时候可以看到上面日志已经注册完成,选择执行器 docker
Enter an executor: docker, parallels, virtualbox, docker+machine, docker-ssh+machine, instance, kubernetes, custom, docker-ssh, shell, ssh:
docker

// 第七步:选择默认基础镜像,这个镜像其实无所谓,因为我们在实际CICD中会指定镜像。这里就直接填 docker:24.0.2
Enter the default Docker image (for example, ruby:2.7):
docker:24.0.2
Runner registered successfully. Feel free to start it, but if it's running already the config should be automatically reloaded!

Configuration (with the authentication token) was saved in "/etc/gitlab-runner/config.toml"

现在我们得到了配置文件,还需要修改一项重要配置(重要)!!!

因为我们使用Docker部署的Runner,也就是使用 Docker-in-Docker (DinD) 模式进行部署。
需要挂载/var/run/docker.sock,以允许容器内的 Docker 引擎访问宿主机的 Docker。

1
2
3
4
5
// 编辑配置文件 
vim runner-config/config.toml

// 修改volumes这一行,改为如下即可
volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"]

保存之后,整个runner的配置文件就准备完成了!

2.启动gitlab-runner
配置文件OK之后,就可以启动Runner了,执行如下命令(注意所在目录需要跟上面保持一致)

1
2
3
4
docker run -d --name gitlab-runner  \
-v $(pwd)/runner-config:/etc/gitlab-runner \
-v /var/run/docker.sock:/var/run/docker.sock \
gitlab/gitlab-runner:v16.11.3

启动之后,就可以在Gitlab上看到 Runner了。

构建JAVA微服务CICD

整个流程:maven打包得到jar包 -> Dockerfile构建镜像并上传仓库 -> Kubectl部署到K8s集群
在这个过程中,牵扯到账号信息,为了安全方便可以配置成CICD变量。

配置CICD变量

  • DOCKER_REGISTRY docker地址 比如:registry.cn-hangzhou.aliyuncs.com/blog
  • DOCKER_REGISTRY_USER docker登录名
  • DOCKER_REGISTRY_PASSWORD docker密码
  • K8S_KUBECONFIG_TEST k8s测试集群的config文件
  • K8S_KUBECONFIG_PROD k8s生产集群的config文件
  • JAVA_MAVEN_SETTINGS maven的setting.xml配置

注意K8sconfig变量类型为 File 文件内容就是集群的KubeConfig

Maven的setting.xml文件如下,需要替换成自己的私服即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
<mirrors>
<mirror>
<id>nexus-aliyun</id>
<mirrorOf>central</mirrorOf>
<name>Nexus aliyun</name>
<url>http://maven.aliyun.com/nexus/content/groups/public</url>
</mirror>
</mirrors>
</settings>

增加部署文件

进入springboot项目仓库,创建 Dockerfile 、k8sdeploy_test.yaml 、k8sdeploy_prod.yaml
项目结构如下:

1
2
3
4
5
6
7
8
项目名称
├── src/main
│ └── java
│ └── resources
├── pom.xml
├── Dockerfile
├── k8sdeploy_test.yaml
├── k8sdeploy_prod.yaml
1
2
3
4
5
6
FROM registry.cn-hangzhou.aliyuncs.com/blog/jre17
MAINTAINER dollcode
ARG JAR_FILE
COPY ${JAR_FILE} app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","/app.jar"]

k8sdeploy_test.yaml 内容如下:

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
37
apiVersion: apps/v1
kind: Deployment
metadata:
name: APPLICATIONNAME
namespace: blog
spec:
replicas: 1
selector:
matchLabels:
app: APPLICATIONNAME
template:
metadata:
labels:
app: APPLICATIONNAME
spec:
containers:
- name: APPLICATIONNAME
image: APPLICATIONIMAGE
imagePullPolicy: Always
env:
- name: SPRING_PROFILES_ACTIVE
value: test
ports:
- containerPort: APPLICATIONPORT
---
apiVersion: v1
kind: Service
metadata:
name: APPLICATIONNAME-svc
spec:
ports:
- port: APPLICATIONPORT
protocol: TCP
targetPort: APPLICATIONPORT
selector:
app: APPLICATIONNAME
type: ClusterIP

k8sdeploy_prod.yaml 内容差不多 只是环境不一样,就不贴了。

编写CICD脚本

依次点击 CICD -> Editor 编辑脚本内容如下

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
stages:
- maven_build
- docker_build
- deploy_k8s

variables:
# 指定项目的端口和代码版本
PROJECT_PORT: "8080"
PROJECT_VERSION: "1.0"
# 指定maven-build配置路径
MAVEN_CLI_OPTS: "-s /root/.m2/settings.xml"
# 定义docker镜像的tag命名
REGISTRY_TAG: "ci-$CI_PIPELINE_ID"
# 定义docker镜像的命名
REGISTRY_APPNAME: "$DOCKER_REGISTRY/blog/$CI_PROJECT_NAME"
# 全局环境
DEPLOY_ENV: "dev"
# K8s命名空间
K8S_NAMESPACE: "default"

# 前置脚本:设置不同分支使用不同的配置和镜像地址,部署到不同的K8s集群
# dev分支为测试环境部署:registry.cn-hangzhou.aliyuncs.com/blog_test/spring-service:ci-101
# dev分支为测试环境部署:registry.cn-hangzhou.aliyuncs.com/blog_prod/spring-service:ci-101
before_script:
- mkdir -p .m2/repository ~/.kube
- if [ "$CI_COMMIT_BRANCH" == "dev" ]; then
export DEPLOY_ENV=test;
mv $K8S_KUBECONFIG_TEST $HOME/.kube/config;
elif [ "$CI_COMMIT_BRANCH" == "main" ]; then
export DEPLOY_ENV=prod;
mv $K8S_KUBECONFIG_PROD $HOME/.kube/config;
else
echo "This branch is not configured for deployment.";
exit 1;
fi
- export REGISTRY_APPNAME=$DOCKER_REGISTRY/blog_$DEPLOY_ENV/$CI_PROJECT_NAME;

# maven打包,注意image要改为自己项目JDK对应版本 如maven:3.8.6-jdk-11、 maven:3.8.6-jdk-8
mavenjar_job:
stage: maven_build
image: maven:3.8.6-jdk-17
# maven依赖做缓存
cache:
key: global-cache
paths:
- .m2/repository
policy: pull-push
script:
- mkdir -p ~/.m2
- echo "$JAVA_MAVEN_SETTINGS" > ~/.m2/settings.xml
- mvn $MAVEN_CLI_OPTS clean package -DskipTests -Dmaven.repo.local=.m2/repository
artifacts:
paths:
- target/$CI_PROJECT_NAME-$PROJECT_VERSION.jar
tags:
- blog

# Docker镜像打包上传
container_job:
stage: docker_build
image: docker:24.0.2
script:
# build镜像 并一个命名当前版本,一个命名latest
- docker build --build-arg JAR_FILE="target/$CI_PROJECT_NAME-$PROJECT_VERSION.jar" --tag $REGISTRY_APPNAME:$REGISTRY_TAG --tag $REGISTRY_APPNAME:latest .
# 登录docker私服仓库
- docker login -u "$DOCKER_REGISTRY_USER" -p "$DOCKER_REGISTRY_PASSWORD" $DOCKER_REGISTRY
# 推送镜像
- docker push $REGISTRY_APPNAME:$REGISTRY_TAG
- docker push $REGISTRY_APPNAME:latest
tags:
- blog

# 部署K8S
deploy_job:
stage: deploy_k8s
image:
# 版本号要和K8s集群版本一致
name: bitnami/kubectl:1.28
entrypoint: [""]
script:
# 替换对应环境yaml中的项目端口、项目名称、项目镜像
- sed -i s/APPLICATIONPORT/$PROJECT_PORT/ k8sdeploy_$DEPLOY_ENV.yaml
- sed -i s/APPLICATIONNAME/$CI_PROJECT_NAME/ k8sdeploy_$DEPLOY_ENV.yaml
# 因为镜像名称中包含特殊字符(/),所以使用%代替分隔符
- sed -i s%APPLICATIONIMAGE%$REGISTRY_APPNAME:$REGISTRY_TAG% k8sdeploy_$DEPLOY_ENV.yaml
# 部署
- kubectl apply -f k8sdeploy_$DEPLOY_ENV.yaml --namespace $K8S_NAMESPACE
tags:
- blog

部署

提交代码 或者 手动触发 部署完成!!!

踩坑汇总

Maven依赖缓存

如果没有设置依赖缓存,每一次都会重新拉取依赖jar。而Springboot的依赖是非常多的,每次拉取超级耗时。
这里有一个坑:gitlab CICD只能缓存项目工作空间下的目录。所以需要创建.m2/repository 并且在maven build命令中指定目录

bitnami/kubectl无法执行命令

在deploy_job执行中,日志报错如下:

1
2
3
4
5
Using docker image sha256:be85f4da59a1c49507de843111579c94b9adf3 for bitnami/kubectl:1.28 with digest bitnami/kubectl@sha256:9657fd84779759711e59a51f4993567562 ...
E0816 09:35:39.752914 1 run.go:120] "command failed" err="unknown command \"sh\" for \"kubectl\"\n\nDid you mean this?\n\tset\n\tcp\n"
Cleaning up project directory and file based variables
00:01
ERROR: Job failed: exit code 1

因为bitnami/kubectl 镜像的默认入口点是 kubectl,因此执行 sh 命令时,它会认为这是一个 kubectl 的子命令,而不是一个 Shell 命令。
必须要显式覆盖镜像的入口点或命令,以便运行 sh。所以需要指定 entrypoint: [""]