原作者@小松同学
基于Gitlab +Docker 的CI/CD实践
- 背景
目前点米HRO项目已经利用Jenkins实现了持续集成,但是这套方案过度依赖开发人员对于Shell脚本掌握程度,上手难度相对较高。同时现有方案还未实现服务容器化部署,因此本文将介绍一种基于
Gitlab+Docker 实现容器化部署的一套CI/CD工作流解决方案。
- 环境准备
- Gitlab 服务器一台 (源码存放)
- 应用构建服务器一台 (应用打包构建)
- Gitlab 持续集成
3.1 Gitlab CI 是什么?
GitLab CI 是GitLab内置的进行持续集成的工具,只需要在仓库根目录下创建.gitlab-ci.yml 文件,并配置GitLab Runner;每次提交的时候,gitlab将自动识别到.gitlab-ci.yml文件,并且使用Gitlab Runner执行该脚本。
3.2 Gitlab Runner 是什么?
GitLab-Runner就是一个用来执行.gitlab-ci.yml 脚本的工具。可以理解成,Runner就像认真工作的工人,GitLab-CI就是管理工人的中心,所有工人都要在GitLab-CI里面注册,并且表明自己是为哪个项目服务。当相应的项目发生变化时,GitLab-CI就会通知相应的工人执行对应的脚本。
GitLab-Runner可以分类两种类型:Shared Runner(共享型)和Specific Runner(指定型)。
- Shared Runner:所有工程都能够用的,且只有系统管理员能够创建。
- Specific Runner:只有特定的项目可以使用。
3.3 Gilab CI 核心概念
管道(pipeline)
每个推送到 Gitlab 的提交都会产生一个与该提交关联的管道(pipeline),若一次推送包含了多个提交,则管道与最后那个提交相关联,管道(pipeline)就是一个分成不同阶段(stage)的作业(job)的集合。
阶段(Stage)
阶段是对批量的作业的一个逻辑上的划分,每个 GitLab CI/CD 都必须包含至少一个 Stage。多个 Stage 是按照顺序执行的,如果其中任何一个 Stage 失败,则后续的 Stage 不会被执行,整个 CI 过程被认为失败。
作业(Job)
作业就是运行器(Runner)要执行的指令集合,Job 可以被关联到一个 Stage。当一个 Stage 执行的时候,与其关联的所有 Job 都会被执行。在有足够运行器的前提下,同一阶段的所有作业会并发执行。作业状态与阶段状态是一样的,实际上,阶段的状态就是继承自作业的。
3.4 Gilab CI 核心指令
- 项目实战
4.1 安装Gitlab/Docker(略)
4.2 工程项目
总体工程结构如下:
4.2.1 创建项目
新建一个 spring boot 2.3 版本的项目(Maven 工程),POM文件如下:
注:2.1 以后提供了分层构建的支持,但是2.3版本更加方便
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.dianmi.pes</groupId>
<artifactId>pes-backend</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>pes-backend</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.RELEASE</version>
</parent>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<profiles>
<profile>
<!-- 开发环境 -->
<id>dev</id>
<properties>
<profiles.active>dev</profiles.active>
</properties>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
<profile>
<!-- 测试环境 -->
<id>test</id>
<properties>
<profiles.active>test</profiles.active>
</properties>
</profile>
<profile>
<!-- 生产环境 -->
<id>prod</id>
<properties>
<profiles.active>prod</profiles.active>
</properties>
</profile>
</profiles>
<build>
<finalName>pes-backend</finalName>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<!-- 开启分层构建-->
<configuration>
<layers>
<enabled>true</enabled>
</layers>
</configuration>
</plugin>
</plugins>
</build>
</project>
4.2.2 maven配置
新建.m2文件夹,同时在该文件夹下创建settings.xml 文件,用于maven构建所使用到的配置。这里只设置了镜像,实际开发中,可以配置进行一些私服配置等。
<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
https://maven.apache.org/xsd/settings-1.0.0.xsd">
<mirrors>
<mirror>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<mirrorOf>central</mirrorOf>
</mirror>
</mirrors>
</settings>
4.2.3 Gitlab
基本主要分为四大部分:
- variables: 用于定义全局变量
- cache: 用于定义构建过程中的缓存
- stages: 用于定义CI各个阶段
- xxx_package、xxx_build、xxx_deploy : 这些是每个环境在每个阶段需要执行的任务
variables:
MAVEN_CLI_OPTS: "-s .m2/settings.xml --batch-mode"
MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository"
DOCKER_IMAGE: registry.2haohro.com/dianmi-pes/pes-svc
# 定义缓存
# 如果gitlab runner是shell或者docker,此缓存功能没有问题
# 如果是k8s环境,要确保已经设置了分布式文件服务作为缓存
cache:
key: pes-ci-cache
paths:
- .m2/repository
# 本次构建的阶段: package jar --> build_image、push_image --> pull image 、run
stages:
- package
- build
- deploy
# --------------------------------- 1.dev job start ------------------------
dev_package:
image: maven:3.6-jdk-8-alpine
stage: package
tags:
- maven
variables:
ENV: dev
script:
- echo "=============== [dev] 开始编译源码,在target目录生成jar文件 ==============="
- mvn $MAVEN_CLI_OPTS clean compile package -Dmaven.test.skip=true -P $ENV
- echo "target文件夹" `ls target/`
only:
- dev
# 生产镜像的job
dev_build:
stage: build
tags:
- docker
variables:
ENV: dev
script:
- echo "从缓存中恢复的target文件夹" `ls target/`
- echo "=============== 登录Harbor ==============="
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- echo "=============== 打包Docker镜像 : ==============="
- docker build -t $DOCKER_IMAGE-$ENV:$CI_COMMIT_SHA .
- echo "=============== 推送到镜像仓库 ==============="
- docker push $DOCKER_IMAGE--$ENV:$CI_COMMIT_SHA
- echo "=============== 登出 ==============="
- docker logout
- echo "清理掉本次构建的jar文件"
- rm -rf target/*.jar
only:
- dev
dev_deploy:
stage: deploy
tags:
- docker
script:
- echo '=============准备开始发布dev环境==============='
- echo "=============== 登录Harbor ==============="
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- echo "=============== 拉取镜像 ==============="
only:
- dev
# --------------------------------- 1.dev job end --------------------------
# --------------------------------- 2.test job start -----------------------
test_package:
image: maven:3.6-jdk-8-alpine
stage: package
tags:
- maven
variables:
ENV: test
script:
- echo "=============== [prod] 开始编译源码,在target目录生成jar文件 ==============="
- mvn $MAVEN_CLI_OPTS clean compile package -Dmaven.test.skip=true -P $ENV
- echo "target文件夹" `ls target/`
artifacts: # 用于在同一流水线传递文件
paths:
- target/*.jar
only:
- test
# 生产镜像的job
test_build:
stage: build
tags:
- docker
variables:
ENV: test
script:
- echo "从缓存中恢复的target文件夹" `ls target/`
- echo "=============== 登录Harbor ==============="
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- echo "=============== 打包Docker镜像 : ==============="
- docker build -t $DOCKER_IMAGE-$ENV:$CI_COMMIT_SHA .
- echo "=============== 推送到镜像仓库 ==============="
- docker push $DOCKER_IMAGE-$ENV:$CI_COMMIT_SHA
- echo "=============== 登出 ==============="
- docker logout
- echo "清理掉本次构建的jar文件"
- rm -rf target/*.jar
after_script:
- echo "清理虚悬镜像"
- docker rmi $(docker images -q -f dangling=true)
only:
- test
test_deploy:
stage: deploy
tags:
- docker
script:
- echo '=============准备开始发布test环境==============='
- echo "=============== 登录Harbor ==============="
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- echo "=============== 拉取镜像 ==============="
only:
- test
# --------------------------------- 2.test job end -------------------------
# --------------------------------- 3.prod job start -----------------------
prod_package:
image: maven:3.6-jdk-8-alpine
stage: package
tags:
- maven
variables:
ENV: prod
script:
- echo "=============== [prod] 开始编译源码,在target目录生成jar文件 ==============="
- mvn $MAVEN_CLI_OPTS clean compile package -Dmaven.test.skip=true -P $ENV
- echo "target文件夹" `ls target/`
artifacts: # 用于在同一流水线传递文件
paths:
- target/*.jar
only:
- master
# 生产镜像的job
prod_build:
stage: build
tags:
- docker
variables:
ENV: prod
script:
- echo "从缓存中恢复的target文件夹" `ls target/`
- echo "=============== 登录Harbor ==============="
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- echo "=============== 打包Docker镜像 : ==============="
- docker build -t $DOCKER_IMAGE-$ENV:$CI_COMMIT_SHA .
- echo "=============== 推送到镜像仓库 ==============="
- docker push $DOCKER_IMAGE-$ENV:$CI_COMMIT_SHA
- echo "=============== 登出 ==============="
- docker logout
- echo "清理掉本次构建的jar文件"
- rm -rf target/*.jar
after_script:
- echo "清理虚悬镜像"
- docker rmi $(docker images -q -f dangling=true)
only:
- master
prod_deploy:
stage: deploy
tags:
- docker
script:
- echo '=============准备开始发布prod环境==============='
- echo "=============== 登录Harbor ==============="
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- echo "=============== 拉取镜像 ==============="
when: manual
only:
- master
# --------------------------------- 3.prod job end -------------------------
补充说明:这里CI脚本针对不同环境写的有点啰嗦,可以后续优化
4.2.4 Dockerfile 编写
# 指定基础镜像,这是分阶段构建的前期阶段
FROM fabletang/jre8-alpine as builder
# 执行工作目录
WORKDIR application
# 配置参数
ARG JAR_FILE=target/*.jar
# 将编译构建得到的jar文件复制到镜像空间中
COPY ${JAR_FILE} application.jar
# 添加java项目启动脚本到镜像中
COPY java-run.sh java-run.sh
# 通过工具spring-boot-jarmode-layertools从application.jar中提取拆分后的构建结果
RUN java -Djarmode=layertools -jar application.jar extract
# 正式构建镜像
FROM fabletang/jre8-alpine
ARG PROJECT_NAME=pes-svc
WORKDIR application
# 添加java项目启动脚本到镜像中
COPY --from=builder application/java-run.sh java-run.sh
# 前一阶段从jar中提取除了多个文件,这里分别执行COPY命令复制到镜像空间中,每次COPY都是一个layer
COPY --from=builder application/dependencies/ ./
COPY --from=builder application/spring-boot-loader/ ./
COPY --from=builder application/snapshot-dependencies/ ./
# 在多结构件镜像的过程中,在某些特殊情况下,会出现该错误, 可通过在错误的COPY之间添加 RUN true 解决 参考:https://github.com/moby/moby/issues/37965
RUN true
COPY --from=builder application/application/ ./
ENTRYPOINT ["sh","java-run.sh"]
补充说明:这里使用docker 分阶段构建
4.2.5 服务启动脚本
#!/bin/bash
java org.springframework.boot.loader.JarLauncher -Dproject.name=$PROJECT_NAME -server -Xms512m -Xmx512m -XX:CompressedClassSpaceSize=128m -XX:MetaspaceSize=200m -XX:MaxMetaspaceSize=200m
注:其实这些Java 启动参数,可以使用Docker 环境变量传入,例如脚本中的$PROJECT_NAME
4.3 Gitlab runner
4.3.1 安装GitLab Runner (略)
4.3.2 注册runner
- 首先要先获取gitlab-ci的Token:
项目主页 -> Sttings -> CI/CD -> Runners Expand
在构建服务器上面使用命令行注册:
gitlab-runner register
需要按照步骤输入:
- 输入gitlab的服务URL:
http://gitlab.2haohro.com/
- 输入gitlab-ci的Token,参考上图示例获取
- 关于集成服务中对于这个runner的描述
For pes ci
- 给这个gitlab-runner输入一个标记,这个tag非常重要,在后续的使用过程中需要使用这个tag来指定gitlab-runner;
maven,docker
- 是否运行在没有tag的build上面。在配置gitlab-ci的时候,会有很多job,每个job可以通过tags属性来选择runner。这里为true表示如果job没有配置tags,也执行
- 是否锁定runner到当前项目
- 选择执行器,gitlab-runner实现了很多执行器,这里选择Shell模式
4.4 测试验证
修改代码提交并推送至远程仓库,触发CI
搬运自@小松同学
参考资料
参考博客: https://choerodon.io/zh/blog/introduction-to-gitlab-ci/
Gitlab官方文档: https://docs.gitlab.com/ee/ci/yaml/README.html#parameter-details
Spring boot 官方文档:https://docs.spring.io/spring-boot/docs/2.3.0.RELEASE/reference/html/