Bootstrap

基于Gitlab +Docker 的CI/CD实践

原作者@小松同学

基于Gitlab +Docker 的CI/CD实践

  1. 背景

目前点米HRO项目已经利用Jenkins实现了持续集成,但是这套方案过度依赖开发人员对于Shell脚本掌握程度,上手难度相对较高。同时现有方案还未实现服务容器化部署,因此本文将介绍一种基于

Gitlab+Docker 实现容器化部署的一套CI/CD工作流解决方案。

 

  1. 环境准备
  1. Gitlab 服务器一台 (源码存放)
  2. 应用构建服务器一台 (应用打包构建)

 

  1. 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 核心指令

 

  1. 项目实战

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 

基本主要分为四大部分:

  1. variables: 用于定义全局变量
  2. cache: 用于定义构建过程中的缓存
  3. stages: 用于定义CI各个阶段
  4. 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

  1. 首先要先获取gitlab-ci的Token:
项目主页 -> Sttings -> CI/CD -> Runners Expand

 

在构建服务器上面使用命令行注册:

gitlab-runner register

需要按照步骤输入:

  1. 输入gitlab的服务URL: 

http://gitlab.2haohro.com/

  1. 输入gitlab-ci的Token,参考上图示例获取
  2. 关于集成服务中对于这个runner的描述

For pes ci

  1. 给这个gitlab-runner输入一个标记,这个tag非常重要,在后续的使用过程中需要使用这个tag来指定gitlab-runner; 

maven,docker

  1. 是否运行在没有tag的build上面。在配置gitlab-ci的时候,会有很多job,每个job可以通过tags属性来选择runner。这里为true表示如果job没有配置tags,也执行
  2. 是否锁定runner到当前项目
  3. 选择执行器,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/

;