GitLab-CI/CD入门实操

以Spring boot项目为例。传统方式是本地生成jar包,FTP上传服务器,重启服务;如果是内网测试服,也可以在服务器上安装git拉取代码,在服务器上编译打包。但这都需要人为干预,于是CI/CD就出现了。

  • CI:Continuous Integration(持续集成)。自动构建和测试每次提交的代码,以确保所引入的更改符合所有测试、准则和代码合规性标准。
  • CD:Continuous Delivery(持续交付)和Continuous Deployment(持续部署)。基于CI,前者侧重于交付给客户或质量团队(比如决定是否对新版本进行压测),而后手动部署/自动部署,如果是自动部署的话就是持续部署了。

CI/CD的工具有很多,最流行的当属jenkins。不过以笔者为数不多的经验来看,作为后起之秀的gitlab更简单一点,也更灵活,不会像jenkins那样笨重。当然,两者的概念都是挺多的,没有师父,光靠自己入门都不容易。

GitLab-CI/CD流程示例

从左往右看,首先研发人员完成需求提交代码到 GitLab。GitLab 触发一次 Build,构建好服务,然后开始跑单元测试、集成测试。等待测试结果通过后,再由负责该项目的同事进行 CodeReview,灰度发布,正式部署到线上。

概念

本文基于GitLab 13.7版本

Pipline

Pipelines comprise:

  • Jobs, which define what to do. For example, jobs that compile or test code.
  • Stages, which define when to run the jobs. For example, stages that run tests after stages that compile the code.

Jobs are executed by runners. Multiple jobs in the same stage are executed in parallel, if there are enough concurrent runners.

Stages

  • Manage:项目周期或团队周期的各项数据统计分析。主要是各环节耗时统计,比如对于典型的Issue(提出问题)->Plan(列入计划)->Code(编码)->Test(测试)->Package(打包)流程,每个环节的耗时决定了整体问题处理的响应速度。
  • Plan:借助诸多工具进行有效的项目管理。
  • Create:代码管理。
  • Verify:代码质量分析、代码合并(持续集成)、单元测试等。
  • Package:将代码打包,并作为依赖库对外提供。
  • Secure(ULTIMATE版提供):检查应用程序是否存在可能导致未经授权访问、数据泄漏或拒绝服务的安全漏洞。GitLab可以对应用程序的代码执行静态和动态测试,查找已知的缺陷并在合并请求中报告它们。然后可以在合并之前修复缺陷。安全团队可以使用仪表板获取项目和组的高级视图,并在需要时启动修正过程。
  • Release:持续交付。
  • Configure:配置[文件]参与DevOps各环节。
  • Monitor:GitLab收集并显示已部署应用程序的性能指标,以便您可以立即知道代码更改如何影响生产环境。
  • Defend:若干用于服务安全防御的中间件。

上述包含了GitLab-DevOps整个流程的所有环节,CI/CD只是其中的一部分。

GitLab Runner

可以安装在任意机子上,通过它可以[在一台机子上]注册多个runner实例到gitlab服务器。每个runner用于执行一个或多个具体任务(如build、test)。

runner有以下三类,可用范围从大到小

  • Shared runners are available to all groups and projects in a GitLab instance.
  • Group runners are available to all projects and subgroups in a group.
  • Specific runners are associated with specific projects. Typically, specific runners are used for one project at a time.

我们可以直接安装GitLab Runner到宿主机,也可以使用docker方式安装。注意这两种方式会影响到后续.gitlab-ci.yml中对pipline的定义。比如要编译maven项目,如果executor设为shell,那么若宿主机中安装有mvn命令,前者可以在scripts中直接使用mvn,而后者并不能,只能通过定义Dockerfile,在其中定义搭建mvn环境到编译代码的整个流程。

采用docker方式安装的话,可以参考Docker搭建自己的Gitlab CI Runner

docker pull gitlab/gitlab-runner:latest
docker run -d --name louwen-runner --restart always \
  -v /srv/gitlab-runner/config:/etc/gitlab-runner \
  -v /var/run/docker.sock:/var/run/docker.sock \
  gitlab/gitlab-runner:latest

注册runner实例(可注册多个实例)

docker exec -it louwen-runner gitlab-runner register

会让我们填一系列配置项,如下:

Enter the GitLab instance URL (for example, https://gitlab.com/):
http://192.168.1.26:9980/
Enter the registration token:
cJMXGJWx7qx9AmpSc6ee
Enter a description for the runner:
[a0debaaf80a9]: runner for InkScreen-API project
Enter tags for the runner (comma-separated):
InkScreenAPI
Registering runner... succeeded                     runner=cJMXGJWx
Enter an executor: docker-ssh, shell, docker-ssh+machine, kubernetes, custom, parallels, ssh, virtualbox, docker+machine, docker:
docker
Enter the default Docker image (for example, ruby:2.6):
maven:3-jdk-8

完事后,我们在gitlab->xxxProjct中就能找到该runner:

Gitlab 15.6 版本前,一个项目可以关联多个 runner,然后使用 tags 设定用于执行 job 的 runners,然而一个 runner 只能关联一个项目。它们是多对一的关系。这就造成了,每一个 CI/CD 项目,至少要新注册一个 runner,而实际情况,大部分项目的 runner 设定是一样的,而且博主个人理解,项目关联的只是注册 runner 时添加的配置节,运行时实例都是需要的时候临时启动的,所以也不存在进程安全、资源竞争的问题;相反,博主目前并没有遇到一个项目需要设置多个 runner 的场景。所以,这种反向多对一的设计就很怪。

所幸,从 Gitlab 15.6 开始,原本用于注册 runner 的 registration token 被废弃,推荐使用 Runner authentication token,该 token 不再是具体项目的属性,而是 runner 实例本身的属性,如此,多个项目可以借此使用同一个 runner。

接下来,就可以定义项目构建流程了。项目的构建流程是由项目根目录的.gitlab-ci.yml文件控制的。当然了,一个pipeline可以涉及到多个runner。

.gitlab-ci.yml

定义一个pipline,以下为示例

variables:
  DOCKER_TLS_CERTDIR: "/certs"

# stage也可以自定义
stages:
  - build jar
#  - test
  - build and run image

#job's name 可以随意取
buildJar:
  stage: build jar
  variables:
    # 若要使cache生效,须指定-Dmaven.repo.local
    MAVEN_OPTS: "-Dmaven.repo.local=.m2"
  cache:
    key: ${CI_COMMIT_REF_SLUG}
    paths:
      - .m2
  only:
    - dev
  script:
#    package 已包含 test 步骤,所以流程中不需要另外配置test job
    - mvn clean package
  tags:
    - inkscreen_api
  artifacts:
    paths:
      - target/admin.jar
    expire_in: 3600 seconds

deploy:
  stage: build and run image
  image: docker:stable
  services:
    - docker:dind
  only:
    - dev
  variables:
    IMAGE_NAME: newton/inkscreen-api:$CI_COMMIT_REF_NAME
    PORT: 38082
  script:
    - docker build --build-arg JAR_PATH=target/admin.jar -t $IMAGE_NAME .
    - docker run -p $PORT:$PORT  -d --name inkscreen-$CI_COMMIT_REF_NAME --env spring.redis.host=myredis $IMAGE_NAME
  tags:
    - inkscreen_api

cache

cache常用在dacker-based job之间传递文件。比如项目依赖的公共jar包,jobA辛辛苦苦从网上down了下来,结果运行完了,jobA所在容器也跟着被移除,自然里面的所有文件都不存在了。后续其它job用到相同的jar包还要重新下载。同样的,pipline多次执行,jobA自己每次也要重新下载。

为了解决这个问题,gitlab-ci采用了cache的方式。指定文件/目录,每次job结束前将其打包,放到/etc/gitlab-runner/config.toml中对应的[runners.docker][volumes]指定的卷内,其它job(包括自己)运行前,对应的cache都会被加载并解压到容器内。

artifacts

artifacts是job生成的中间产物,会以压缩包(.zip)的形式生成。它会自动上传到gitlab服务器,the artifacts will be downloaded and extracted in the context of later stages。所以它和cache很像,但是设计它们的初衷是不同的。

Don't use caching for passing artifacts between stages, as it is designed to store runtime dependencies needed to compile the project:

  • cache: For storing project dependencies

    Caches are used to speed up runs of a given job in subsequent pipelines, by storing downloaded dependencies so that they don't have to be fetched from the internet again (like npm packages, Go vendor packages, etc.) While the cache could be configured to pass intermediate build results between stages, this should be done with artifacts instead.

  • artifacts: Use for stage results that will be passed between stages.

    Artifacts are files generated by a job which are stored and uploaded, and can then be fetched and used by jobs in later stages of the same pipeline. In other words, you can't create an artifact in job-A in stage-1, and then use this artifact in job-B in stage-1. This data will not be available in different pipelines, but is available to be downloaded from the UI.

The name artifacts sounds like it's only useful outside of the job, like for downloading a final image, but artifacts are also available in later stages within a pipeline.

另外,同样key的cache会被覆盖,而artifacts一旦生成就固定了,当然我们可以设置expire_in,过期删除之。

可参看各类语言/平台的.gitlab-ci.yml模板

实战

我们定义一个最简单的pipline:第一步编译生成jar包,第二步将jar包导入docker镜像并运行,在某些环节还需加入代码review。因为最后我们会以docker容器运行jar包,所以这里不建议docker-based runner/executor的形式,因为该形式导致Docker-in-Docker的场景,带来可能的一些麻烦且难以解决的问题(比如内嵌容器如何关联外部服务以及对外提供服务)。所以我们直接宿主机安装GitLab Runner。

如果一定要以docker-based形式,那么可参看使用GitLab CI和Docker自动部署SpringBoot应用。在该文中,并没有在runner所在宿主机中运行容器,而是将生成的镜像发布到镜像仓库,再登录目标服务器拉取镜像运行,所以不存在Docker-in-Docker的麻烦事。

安装&注册[GitLab ]Runner

# 1.Add the official GitLab repository
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.rpm.sh" | sudo bash
# 2.Install the latest version of GitLab Runner
export GITLAB_RUNNER_DISABLE_SKEL=true; sudo -E yum install gitlab-runner

注册runner

sudo gitlab-runner register -n \
  --url http://192.168.1.26:9980/ \
  --registration-token cJMXGJWx7qx9AmpSc6ee \
  --executor shell \
  --tag-list "inkscreen_hostrunner" \
  --description "Host Runner for InkScreen"

Add the gitlab-runner user to the docker group:

sudo usermod -aG docker gitlab-runner

.gitlab-ci.yml

stages:
  - build jar
  - build and run image

#job's name 可以随意取
buildJar:
  stage: build jar
  variables:
    # 默认是clone,改为fetch加快拉取速度(若本地无则会自动clone)
    GIT_STRATEGY: fetch
  only:
    - dev
  script:
    - >
      docker run -d --rm --name justforpackage-$CI_COMMIT_REF_NAME
      -v "$(pwd)":/build/inkscreen
      -v /inkscreen/maven/m2:/root/.m2
      -w /build/inkscreen
      maven:3-jdk-8 mvn clean package

    - sleep 60
  tags:
    - inkscreen_hostrunner
  artifacts:
    paths:
      - louwen-admin/target/louwen-admin.jar
    expire_in: 3600 seconds

testDeploy:
  stage: build and run image
  only:
    - dev
  variables:
    # 不拉取代码
    GIT_STRATEGY: none
    IMAGE_NAME: louwen/inkscreen-api:$CI_COMMIT_REF_NAME
    PORT: 38082
  before_script:
    # 移除旧容器和镜像。这里为什么要写成一行,下面有讲
    - if [ docker ps | grep inkscreen-$CI_COMMIT_REF_NAME ]; then docker stop inkscreen-$CI_COMMIT_REF_NAME; docker rm inkscreen-$CI_COMMIT_REF_NAME; docker rmi $IMAGE_NAME; fi
  script:
    - docker build --build-arg JAR_PATH=louwen-admin/target/louwen-admin.jar -t $IMAGE_NAME .
    - >
      docker run -d --name inkscreen-$CI_COMMIT_REF_NAME
      -p $PORT:$PORT
      --network my_bridge --env spring.redis.host=myredis
      -v /inkscreen/inkscreen-api/logs/:/logs/
      -v /inkscreen/inkscreen-api/louwen-admin/src/main/resources/:/configs/
      $IMAGE_NAME
  tags:
    - inkscreen_hostrunner

注意build jar环节我们sleep了60秒,是因为docker run并不会等待内部脚本执行完,而是启动后就直接返回了,此时jar包尚未生成,所以此处阻塞一段时间等待打包结束。正常应该写一段脚本循环判断jar包是否已生成,若生成或超时则跳出循环,此处作为演示简单sleep。

在testDeploy任务中,before_script被我写成了一行,最初版本是:

  before_script:
    # 若未找到记录,则该条命令会返回1,gitlab就直接报错返回了ERROR: Job failed: exit status 1
    - docker ps | grep inkscreen-$CI_COMMIT_REF_NAME
    - >
      if [ $? -eq 0 ]
      then
        docker stop inkscreen-$CI_COMMIT_REF_NAME
        docker rm inkscreen-$CI_COMMIT_REF_NAME
        docker rmi $IMAGE_NAME
      fi

后改为

  before_script:
    # 将检测语句直接作为条件内置,解决了上面的问题
    - >
      if       
      docker ps | grep inkscreen-$CI_COMMIT_REF_NAME
      then
        docker stop inkscreen-$CI_COMMIT_REF_NAME
        docker rm inkscreen-$CI_COMMIT_REF_NAME
        docker rmi $IMAGE_NAME      
      fi

报错syntax error near unexpected token 'fi',估计是换行/回车格式的原因。上述两个问题都可以通过单独建.sh文件的方式解决,我这里简单地将所有语句排成一行。

Dockerfile

生成镜像自然少不了Dockerfile

FROM openjdk:8-jdk-oracle
MAINTAINER louwen

# 外部传入,主程序路径
ARG JAR_PATH
ENV LANG=C.UTF-8 LC_ALL=C.UTF-8

COPY $JAR_PATH /app.jar
EXPOSE 38082
ENTRYPOINT ["java","-jar","/app.jar"]

题外话,其实我们完全可以将build jar环节也放在Dockerfile中,如下

#
# build jar stage
#
FROM maven:3-jdk-8 AS MAVEN_BUILD

COPY pom.xml /build/
COPY src /build/src/
WORKDIR /build/
RUN mvn clean package

#
FROM openjdk:8-jdk-oracle
MAINTAINER louwen
COPY --from=MAVEN_BUILD /build/target/*.jar /app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

代码规范

目前较流行的代码检测工具是SonarQube,不过其社区版本对同一个代码仓库无法区分不同分支,从而实现按代码的不同分支显示对应分支的扫描结果。这里我们使用Gitlab-CI的Code Quality stage,其使用的是Codeclimate,它是为代码质量分析平台提供的一个命令行接口工具,通过它可以在本机 Docker 容器中对要分析的代码执行质量分析,并生成分析报告。我们熟知常用的代码质量检测工具例如 SonarQube、CheckStyle 等等,而 Codeclimate 接入了这些工具,而且支持我们自定义检测工具。

按照官方说法,使用Code Quality需要基于docker-based runner/executor,所以我们另外使用docker方式安装GitLab-Runner并注册一个runner(参考上述概念小节),executor选择docker。

include:
  - template: Code-Quality.gitlab-ci.yml

# 以下配置参考网上一些资料,据说是官方示例,然而我没有在官方文档找到
code_quality:
  image: docker:stable
  variables:
    DOCKER_DRIVER: overlay2
    # gitlab 13.6及之后版本支持
    REPORT_FORMAT: html
  allow_failure: true
  services:
    - docker:dind
  script:
  # 镜像版本号格式参看 https://gitlab.com/gitlab-org/ci-cd/codequality/-/tree/master#versioning-and-release-cycle
#    - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
    - docker run
      --net=host
      --env SOURCE_CODE="$PWD"
      --volume "$PWD":/code
      --volume /var/run/docker.sock:/var/run/docker.sock
      "registry.gitlab.com/gitlab-org/security-products/codequality:${VERSION:-latest}" /code
  artifacts:
    paths: [ gl-code-quality-report.html ]
  tags:
    - InkScreenAPI

执行的时候可能会卡在拉取镜像环节。手动docker pull registry.gitlab.com/gitlab-org/security-products/codequality:latest,发现各种超时。我开个阿里云香港ECS的抢占式实例(便宜)然后docker pull | save | load将镜像文件迁移到公司测试服,还是报Unable to find image 'registry.gitlab.com/gitlab-org/security-products/codequality:latest' locally,不知如何将host中的镜像映射到docker:stable中。看来还是得kexue上网。

理论上,需要专人在合适的时候对提交的代码进行质量把关,一般这工作可以放在Merge Request下进行。Merge Request的工作流程可以参看在团队中使用GitLab中的Merge Request工作模式

FAQ

  1. dial tcp: lookup docker on 192.168.1.1:53: no such host错误。
    This error occurs with docker-based gitlab runners such as the one we’re that are configured using a docker executor. The error message means that the inner docker container doesn’t have a connection to the host docker daemon.
    解决:将/etc/gitlab-runner/config.toml中对应的[runners.docker]节点设置privileged = true,增加卷映射volumes = ["/cache", "/var/run/docker.sock:/var/run/docker.sock"][或|并]在.gitlab-ci.yml的job定义中增加services: - docker:dind。(可能需要重启runner docker restart gitlab-runner

表述不准确,参看gitlab-runner 中的 Docker-in-Docker

  1. Cannot connect to the Docker daemon at tcp://docker:2375. Is the docker daemon running?错误
    解决:参看gitlab-runner 中的 Docker-in-Docker

  2. 拉取代码时提示warning: failed to remove xxxx: Permission denied
    简单粗暴地编辑/etc/passwd,将gitlab-runner账号对应的uid:gid改为0:0(和root一样)。

  3. Code Quality提示docker: Error response from daemon: Head https://registry.gitlab.com/v2/gitlab-org/security-products/codequality/manifests/13-7-stable: Get https://gitlab.com/jwt/auth?scope=repository%3Agitlab-org%2Fsecurity-products%2Fcodequality%3Apull&service=container_registry: dial tcp [2606:4700:90:0:f22e:fbec:5bed:a9b9]:443: connect: cannot assign requested address.
    在scripts->docker run增加参数--net=host

其它

发件邮箱配置

在pipline流程执行过程中,我们希望有任何风吹草动都能及时收到消息,邮件就是一个比较好的提醒方式。

vi /etc/gitlab/gitlab.rb

### GitLab email server settings
###! Docs: https://docs.gitlab.com/omnibus/settings/smtp.html
###! **Use smtp instead of sendmail/postfix.**

gitlab_rails['smtp_enable'] = true
gitlab_rails['smtp_address'] = "smtp.exmail.qq.com"
gitlab_rails['smtp_port'] = 465
gitlab_rails['smtp_user_name'] = "xxxx@yyyy.com"
gitlab_rails['smtp_password'] = "xxxxxxxx"
gitlab_rails['smtp_domain'] = "exmail.qq.com"
gitlab_rails['smtp_authentication'] = "login"
gitlab_rails['smtp_enable_starttls_auto'] = true
gitlab_rails['smtp_tls'] = true

### Email Settings

gitlab_rails['gitlab_email_enabled'] = true

##! If your SMTP server does not like the default 'From: gitlab@gitlab.example.com'
##! can change the 'From' with this setting.
##! 要与上面的 smtp_user_name 保持一致
gitlab_rails['gitlab_email_from'] = 'xxxx@yyyy.com'
# gitlab_rails['gitlab_email_display_name'] = 'Example'
# gitlab_rails['gitlab_email_reply_to'] = 'noreply@example.com'
# gitlab_rails['gitlab_email_subject_suffix'] = ''
# gitlab_rails['gitlab_email_smime_enabled'] = false
# gitlab_rails['gitlab_email_smime_key_file'] = '/etc/gitlab/ssl/gitlab_smime.key'
# gitlab_rails['gitlab_email_smime_cert_file'] = '/etc/gitlab/ssl/gitlab_smime.crt'
# gitlab_rails['gitlab_email_smime_ca_certs_file'] = '/etc/gitlab/ssl/gitlab_smime_cas.crt'

gitlab-ctl reconfigure使配置生效
测试

gitlab-rails console
irb(main):003:0> Notify.test_email('whatever@qq.com', 'Message Subject', 'Message Body').deliver_now

登录whatever@qq.com查看受否收到信件。


jenkins + gitlab

如果使用jenkins作为CI/CD工具,代码由gitlab托管,那么它们之间的交互需要两个token:

  1. api token,用于jenkins调用gitlab api使用
  2. ssh密钥对,jenkins拉取代码使用(当然我们也可以使用用户名/密码方式拉取)

mvn package、install、deploy都干了什么

  • mvn clean package依次执行了clean、resources、compile、testResources、testCompile、test、jar(打包)等7个阶段。
  • mvn clean install依次执行了clean、resources、compile、testResources、testCompile、test、jar(打包)、install等8个阶段。
  • mvn clean deploy依次执行了clean、resources、compile、testResources、testCompile、test、jar(打包)、install、deploy等9个阶段。

由上可知:

  • package命令完成了项目编译、单元测试、打包功能,但没有把打好的可执行jar包(war包或其它形式的包)布署到本地maven仓库和远程maven私服仓库
  • install命令完成了项目编译、单元测试、打包功能,同时把打好的可执行jar包(war包或其它形式的包)布署到本地maven仓库,但没有布署到远程maven私服仓库
  • deploy命令完成了项目编译、单元测试、打包功能,同时把打好的可执行jar包(war包或其它形式的包)布署到本地maven仓库和远程maven私服仓库

alpine

Alpine Linux 是一个社区开发的面向安全应用的轻量级Linux发行版。很多镜像都会专门基于Alpine构建,大小会小很多。比如:

  • gitlab/gitlab-runner:latest based on Ubuntu.
  • gitlab/gitlab-runner:alpine based on Alpine with much a smaller footprint (~160/350 MB Ubuntu vs ~45/130 MB Alpine compressed/decompressed).

修改GitLab-ce域名

刚部署好的GitLab新建的项目ssh地址一般是个短链接如git@AKDJF3ld:xxx,有时候会不太好使,可以通过配置文件的修改,指向域名。参看Docker部署GitLab并实现基本配置。还有一种方式是修改容器内的/var/opt/gitlab/gitlab-rails/etc/gitlab.yml,特别是要修改端口的时候。

vim /opt/gitlab/embedded/service/gitlab-rails/config/gitlab.yml
# host: 192.168.xx.xx
# port: xxxx
# 进入容器内,不要在外部docker restart 或者 容器内 gitlab-ctl reconfigure,否则配置又还原回去了
gitlab-ctl restart

参考资料

当谈到 GitLab CI 的时候,我们该聊些什么(上篇)
什么是devops,基于Gitlab从零开始搭建自己的持续集成流水线(Pipeline)
持续集成之.gitlab-ci.yml篇
Building Docker images with GitLab CI/CD
自动化 DevOps 使用 Codeclimate 执行代码质量分析
GitLab CI/CD
基于 Gitlab 的 Code Review 最佳实践

posted @ 2021-01-21 15:00  莱布尼茨  阅读(10483)  评论(2编辑  收藏  举报