文章

gitlab-ci

懒才是第一生产力:能把重复事情自动化的勤奋的懒人才是第一生产力!

  1. CI/CD
  2. runner
  3. .gitlab-ci.yml
  4. pipeline
    1. stages
    2. job
      1. 执行条件
    3. 不同类型的pipeline
      1. basic pipeline
      2. parent-child
      3. branch & merge request pipeline
      4. avoid duplicate pipelines
  5. variable
  6. keyword
  7. 一个父子pipeline示例
    1. reference vs. yaml anchor
    2. image
    3. services
    4. artifacts
    5. 莫名的changes触发

CI/CD

什么是CI/CD?持续集成(Continuous Integration),持续交付(Continuous Delivery),持续部署(Continuous Deployment)。自动在代码提交之后跑test、构建发布包或者docker镜像,甚至还能直接deploy。把一系列流程化的东西自动化起来,是程序员的分内之事。况且,自动化之后真的挺爽的!

  • https://docs.gitlab.com/ee/ci/

一个示例:

  • https://docs.gitlab.com/ee/ci/quick_start/

runner

想执行CI/CD,得有runner。runner可以在实体机上部署,也可以部署为docker container。可以共享,也可以专用:

  • https://docs.gitlab.com/runner/

runner是跑CI/CD的基础,但并不属于程序员关心的范围,由op搭好就行了。程序员更需要关心的是CI/CD的执行文件怎么写。

.gitlab-ci.yml

  • https://docs.gitlab.com/ee/ci/quick_start/#create-a-gitlab-ciyml-file

类似于Dockerfile,.gitlab-ci.yml是gitlab执行CI/CD的依据、命令清单。

ci主要由以下部分组成:

  • pipeline
    • stages
    • job
  • variable

pipeline

pipeline代表了整个ci的流程,由stage和job组成。

stages

stages定义了一共有哪些步骤,如果没定义,默认是.pre/build/test/deploy/.post,但是最好显式定义出来。

  • https://docs.gitlab.com/ee/ci/yaml/index.html#stages

定义job的时候,需要使用stage关键字标明它在哪个stage执行:

  • https://docs.gitlab.com/ee/ci/yaml/index.html#stage

stage执行的规则:

  1. 同一stage的job并行执行;
  2. 上一stage的job成功了才会执行下一stage的job(除非下一stage的job标记了when: on_failure);

If a job does not specify a stage, the job is assigned the test stage. 太隐晦了,还是都显式标出来比较好。

job

job代表了要执行的任务。

  • https://docs.gitlab.com/ee/ci/jobs/

job名称随便定义,只要不使用gitlab ci的关键字就行。job至少要包含一个script,声明工作的内容:

1
2
job1:
  script: "execute-script-for-job1"

job使用stage标明自己属于pipeline的哪个阶段。

执行条件

job一般都会设置执行条件:

  • rules:复杂条件
    • https://docs.gitlab.com/ee/ci/yaml/index.html#rules
  • only/except:简单条件
    • https://docs.gitlab.com/ee/ci/yaml/index.html#only–except

rules条件比较复杂,能够涵盖only/except的含义;后者支持的场景有限,但用起来比较简单。

1
2
3
4
5
6
7
job1:
  script: echo "Hello, Rules!"
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      when: manual
      allow_failure: true
    - if: $CI_PIPELINE_SOURCE == "schedule"

上述示例用了两个if他们是或的关系

  • 如果pipeline是merge request的pipeline,符合条件;
  • 如果pipeline是schedule,也符合条件;
  • 其他条件都不会执行该job;

具体可以好好看看:when to run jobs。需要注意的是条件的取值

  • api:仅指pipeline的api,使用trigger的api不算api;
  • trigger:使用trigger的api(需要trigger token)触发,虽然也用了api,但算trigger;

比如使用(pipeline)api触发,需要项目的access token或者个人的acess token:

1
2
3
4
curl --request POST  --header "PRIVATE-TOKEN:mGEsgo2nEEkAs8dxAp98" \
           --header "Content-Type:application/json"  \
           --data '{ "ref":"'universe-tag'","variables":[{"key":"RUN_ONLINE", "value":"'true'"}]}'  \
           "https://gitlab.x.com/api/v4/projects/7290/pipeline"

使用trigger(api),需要用到项目的trigger token(仅能用于trigger,和access token不同,后者使用范围更广):

1
2
3
4
5
curl --request POST \
  --form token=5d4044bdf40263b6ebf2bced17fbfc \
  --form ref=universe-tag \
  --form "variables[RUN_ONLINE]=true" \
  "https://gitlab.x.com/api/v4/projects/7290/trigger/pipeline"

rules还支持rules:changes,检测到文件发生变化时就执行job。

only/except

  • refs:(refs关键字看起来可以省略)直接指定分支、正则、或者一些关键字(api/branches/merge_requests等等)。支持数组格式。一个使用场景是master:当是master分支的时候才执行该job;
  • variables:当变量等于某值时,执行该任务;
  • changes:当文件发生变化时,执行该任务;

条件执行job对于test/dev/online任务是非常有用的。

不同类型的pipeline

pipeline有不同的类型,适用于不同的场景:

  • https://docs.gitlab.com/ee/ci/pipelines/pipeline_architectures.html

basic pipeline

  • https://docs.gitlab.com/ee/ci/pipelines/pipeline_architectures.html#basic-pipelines

最简单直白的pipeline,一条线走下去。大多数pipeline都是这样的。

parent-child

  • https://docs.gitlab.com/ee/ci/pipelines/pipeline_architectures.html#child–parent-pipelines
  • https://docs.gitlab.com/ee/ci/pipelines/downstream_pipelines.html#parent-child-pipelines

父子pipeline,父pipeline在某些情况下触发子pipeline。非常适合maven多module的情况,比如:使用rules:changes在子module文件发生变动的情况下触发子pipeline

branch & merge request pipeline

  • https://docs.gitlab.com/ee/ci/pipelines/merge_request_pipelines.html

每次commit就执行的pipeline是branch pipeline,只有在创建了merge request且commit的情况下才执行的pipeline,是merge request pipeline

avoid duplicate pipelines

  • https://docs.gitlab.com/ee/ci/jobs/job_control.html#avoid-duplicate-pipelines

every push to an open merge request’s source branch causes duplicated pipelines,一个commit pipeline,一个merge request pipeline。

可以使用workflow避免这个问题:workflow相当于给job增加全局rules,如果workflow不通过,job一定不会执行。

workflow一般是通过事件决定job是否执行:

1
2
3
4
5
6
# 屏蔽 Merge Request 流水线的执行
workflow:
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
      when: never
    - when: always

variable

  • predefined variable: https://docs.gitlab.com/ee/ci/variables/predefined_variables.html

gitlab预定义了很多变量,可以在ci里直接使用,比如CI_COMMIT_BRANCH代表branch名称。

还能使用variables自定义变量:

1
2
variables:
  DEPLOY_SITE: "https://example.com/"

keyword

gitlab ci的关键字,其实就是各个配置项。需要的时候来看一看即可:

  • https://docs.gitlab.com/ee/ci/yaml/

一个父子pipeline示例

一个样例:maven多模块项目,当子模块的文件内容发生变动时,父pipeline触发子pipeline,给子模块打包docker镜像、上次镜像到hub、部署镜像到rancher。

父pipeline,在root下的.gitlab-ci.yml

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
# 父流水线
# 子项目目录有内容更新时,自动触发子项目的ci执行

stages:
  - trigger-sub-task

variables:
  RANCHER_URL: https://rancher.corp.youdao.com

# 屏蔽 Merge Request 流水线的执行
workflow:
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
      when: never
    - when: always

es-reindex:
  stage: trigger-sub-task
  variables:
    PROJECT: es-reindex
  trigger:
    # 这里不要替换为${PROJECT}
    include: es-reindex/.gitlab-ci.yml
    strategy: depend
  rules:
    - changes:
        - ${PROJECT}/**/*
  1. 所有的job只在非merge request时执行,以防止创建merge request的时候同时执行branch pipeline和merge request pipeline;
  2. 父pipeline创建任务es-reindex,触发子pipeline执行;
  3. 触发条件是子module的文件changes;

子pipeline,在子模块下创建.gitlab-ci.yml

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
image: harbor-registry.inner.youdao.com/devops/docker:19.03-ydci

services:
  - name: harbor-registry.inner.youdao.com/devops/docker:19.03-dind
    alias: docker

stages:
  - test
  - pom-version
  - package
  - build-push-image
  - deploy
  - notify

# 引用公共ci脚本
include:
  - local: ci-common-script.yml

# run before each job’s script commands
before_script:
  - !reference [.install_reptile_common_and_reptile_parent, script]

unit_test:
  image: harbor-registry.inner.youdao.com/ead/overseas-base:latest
  stage: test
  tags:
    - k8s
  script:
    - mvn clean test
    - if [ -e target/site/jacoco/index.html ]; then cat target/site/jacoco/index.html; fi
  coverage: '/Total.*?([0-9]{1,3})%/'

fetch-version:
  image: harbor-registry.inner.youdao.com/ead/overseas-base:latest
  stage: pom-version
  script:
    - !reference [.set_image_name, script]
  artifacts:
    reports:
      dotenv: ${PROJECT}/build.env

package:
  image: harbor-registry.inner.youdao.com/ead/overseas-base:latest
  stage: package
  tags:
    - k8s
  script:
    - mvn clean package
  artifacts:
    paths:
      - ${PROJECT}/target/

build-image-dev:
  stage: build-push-image
  tags:
    - k8s
  variables:
    IMAGE_NAME: $IMAGE_NAME_DEV
  script:
    - !reference [.build_push_image, script]
  except:
    - master
  when: manual
  dependencies:
    - fetch-version
    - package

build-image-prod:
  stage: build-push-image
  tags:
    - k8s
  variables:
    IMAGE_NAME: $IMAGE_NAME_PROD
  script:
    - !reference [.build_push_image, script]
  only:
    - master
  when: manual
  dependencies:
    - fetch-version
    - package

build-image-prod-latest:
  stage: build-push-image
  tags:
    - k8s
  variables:
    IMAGE_NAME: $IMAGE_NAME_PROD_LATEST
  script:
    - !reference [.build_push_image, script]
  only:
    - master
  when: manual
  dependencies:
    - fetch-version
    - package

deploy-dev:
  stage: deploy
  tags:
    - k8s
  variables:
    # 这些变量来自rancher创建实例后的url
    RANCHER_CLUSTER: 'k8s-dev2'
    RANCHER_PROJECT: 'c-tsqtc:p-b57q6'
    RANCHER_PROJECT_NAME: 'ad'
    RANCHER_NAMESPACE: 'ad'
    RANCHER_WORKLOAD: 'kol-metric'
    IMAGE_NAME: $IMAGE_NAME_DEV
  script:
    - !reference [.deploy_rancher, script]
  except:
    - master
  when: manual
  dependencies:
    - fetch-version
    - build-image-dev

deploy-prod:
  stage: deploy
  tags:
    - k8s
  variables:
    RANCHER_CLUSTER: 'k8s-prod'
    RANCHER_PROJECT: 'c-pfl4t:p-5v9n4'
    RANCHER_PROJECT_NAME: 'ad'
    RANCHER_NAMESPACE: 'ad'
    RANCHER_WORKLOAD: 'kol-metric'
    IMAGE_NAME: $IMAGE_NAME_PROD
  script:
    - !reference [.deploy_rancher, script]
  only:
    - master
  when: manual
  dependencies:
    - fetch-version
    - build-image-prod

notify_success:
  stage: notify
  tags:
    - k8s
  variables:
    MSG: ${PROJECT} CI 成功
  script:
    - !reference [.notify_popo, script]

notify_failure:
  stage: notify
  tags:
    - k8s
  when: on_failure
  variables:
    MSG: ${PROJECT} CI 失败
  script:
    - !reference [.notify_popo, script]
  1. 子pipeline有test/pom-version/package/build-push-image/deploy/nitify几个阶段;
  2. 获取pom version后,通过artifacts传递结果;
  3. 构建镜像的时候,线上(master分支)和开发分支的镜像名称不同,使用rules/except/only启用不同的job;

还有一些其他需要介绍的地方——

reference vs. yaml anchor

子pipeline引用了ci-common-script.yml,相当于函数调用,多个子pipeline可以共用这些函数:

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
# ci的公共可用脚本

# 发送popo消息
.notify_popo:
  script:
    - FULL_MSG="$MSG Branch = $CI_COMMIT_REF_NAME SHA = $CI_COMMIT_SHA $CI_PROJECT_URL/pipelines/$CI_PIPELINE_ID"
    - ydci notify popo -t $GITLAB_USER_EMAIL -s "$CI_PROJECT_NAME" -m "${FULL_MSG}"

# 构建并上传镜像
.build_push_image:
  script:
    - echo $CI_HARBOR_TOKEN $CI_HARBOR_USER $CI_HARBOR_REGISTRY
    - echo $IMAGE_NAME
    - docker logout harbor-registry.inner.youdao.com
    - echo $CI_HARBOR_TOKEN | docker login -u $CI_HARBOR_USER --password-stdin $CI_HARBOR_REGISTRY
    - eval docker build --no-cache -t $IMAGE_NAME .
    - eval docker push $IMAGE_NAME

# 更新rancher上的服务
.deploy_rancher:
  script:
    - eval ydci deploy set-image $IMAGE_NAME -c ${RANCHER_CLUSTER} -p ${RANCHER_PROJECT_NAME} -n ${RANCHER_NAMESPACE} -w ${RANCHER_WORKLOAD}
    - WEB_URL="${RANCHER_URL}/p/${RANCHER_PROJECT}/workload/deployment:${RANCHER_NAMESPACE}:${RANCHER_WORKLOAD}"
    - MSG="开始部署 ${RANCHER_NAMESPACE}/${RANCHER_WORKLOAD} ${WEB_URL}"
    - FULL_MSG="$MSG Branch = $CI_COMMIT_REF_NAME SHA = $CI_COMMIT_SHA By = $GITLAB_USER_EMAIL $CI_PROJECT_URL/pipelines/$CI_PIPELINE_ID"
    - echo $FULL_MSG
    - ydci notify popo -t $GITLAB_USER_EMAIL -s "$CI_PROJECT_NAME" -m "${FULL_MSG}"

# 设置镜像名称,自动获取project.version作为tag
.set_image_name:
  script:
    - VERSION=$(mvn -Dexec.executable='echo' -Dexec.args='${project.version}' --non-recursive exec:exec -q)
    - echo "VERSION=$VERSION" >> build.env
    - echo "IMAGE_NAME_DEV=harbor-registry.inner.youdao.com/ead-test/${PROJECT}:${CI_COMMIT_REF_NAME}_${VERSION}" >> build.env
    - echo "IMAGE_NAME_PROD=harbor-registry.inner.youdao.com/ead/${PROJECT}:${VERSION}" >> build.env
    - echo "IMAGE_NAME_PROD_LATEST=harbor-registry.inner.youdao.com/ead/${PROJECT}:latest" >> build.env
    - cat build.env

# 当父pom和reptile-common版本更新时(尚未发布到公共仓库),此时依赖他们的工程必须手动将这些依赖安装到本地
.install_reptile_common_and_reptile_parent:
  script:
    # mvn存在时才执行这些install命令。如果镜像里没有mvn,说明执行的不是mvn相关的任务,也就不需要先install这些依赖
    - if type mvn; then mvn -N -DskipTests=true clean install; fi
    - if type mvn; then mvn -DskipTests=true clean install --projects reptile-common; fi
    - cd ${PROJECT}

引用方式一般为reference:一般用它引用included的配置文件里的内容。比如上述!reference [.build_push_image, script]指的是引用公共文件里build_push_imagescript部分。

yaml anchor也可以做引用,且是yaml本来就有的语法,不属于gitlab-ci范畴。它一般用于“宏替换”,相当于把配置内容整个粘过来。

image

  • https://docs.gitlab.com/ee/ci/yaml/#image

ci可以在docker里执行,image声明了执行用的docker镜像。job里也可以指定image覆盖全局image声明。

1
2
3
4
5
6
image: harbor-registry.inner.youdao.com/devops/docker:19.03-ydci

unit_test:
  image: harbor-registry.inner.youdao.com/ead/overseas-base:latest
  stage: test
  ...

services

使用services关键字声明容器里额外运行的service,比如运行一个MySQL,然后给一个alias,之后就可以用这个alias访问这个service了。

但是,service不能给原有容器添加一个software,比如:

  • 不能声明一个php service,然后就认为你自己的镜像可以使用php指令了;
  • 不能声明一个MySQL service,然后就认为你自己的镜像可以使用mysql指令了;

只是启动了一个额外的service,可以 访问 这个service,但是不能从job里 执行 这些service的指令。想使用这些指令,需要base image里本身含有这些指令。

常用场景:

  • 起一个后端service,测试后端api;
  • 集成测试,起一个MySQL数据库;

比如,如果使用dind,需要在容器里启动一个docker daemon:

1
2
3
services:
  - name: harbor-registry.inner.youdao.com/devops/docker:19.03-dind
    alias: docker

gitlab ci的dind可参考:Docker - dind

artifacts

job artifacts是job产生的文件、文件夹。可以通过artifacts传递给下一个任务。默认情况下,一个任务会自动下载之前的jobs创建的所有artifact。除非使用dependencies显式声明只需要哪几个job的artifact。

任务可以创建一个文件,里面放几个变量:

1
2
3
4
5
6
  script:
    - VERSION=$(mvn -Dexec.executable='echo' -Dexec.args='${project.version}' --non-recursive exec:exec -q)
    - echo "VERSION=$VERSION" >> build.env
    - echo "IMAGE_NAME_DEV=harbor-registry.inner.youdao.com/ead-test/${PROJECT}:${CI_COMMIT_REF_NAME}_${VERSION}" >> build.env
    - echo "IMAGE_NAME_PROD=harbor-registry.inner.youdao.com/ead/${PROJECT}:${VERSION}" >> build.env
    - echo "IMAGE_NAME_PROD_LATEST=harbor-registry.inner.youdao.com/ead/${PROJECT}:latest" >> build.env

然后使用artifacts:reports“导出”该文件:

1
2
3
  artifacts:
    reports:
      dotenv: ${PROJECT}/build.env

也可以把整个编译后的内容作为artifacts,通过artifacts:paths

1
2
3
4
5
6
7
8
9
10
package:
  image: harbor-registry.inner.youdao.com/ead/overseas-base:latest
  stage: package
  tags:
    - k8s
  script:
    - mvn clean package
  artifacts:
    paths:
      - ${PROJECT}/target/

注意,path是${CI_PROJECT_DIR}的相对路径,所以子pipeline要指定相对父工程目录的路径,而不是子工程目录

dependencies可以显式声明该任务需要哪几个job的artifacts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
build-image-dev:
  stage: build-push-image
  tags:
    - k8s
  variables:
    IMAGE_NAME: $IMAGE_NAME_DEV
  script:
  <<: *docker_image_def
  except:
    - master
  when: manual
  dependencies:
    - fetch-version
    - package

莫名的changes触发

虽然rules:changes触发pipeline似乎很有用,但是实际上发现只要新建一个分支,所有的changes都会生效,子pipeline都会被触发。gitlab的逻辑似乎是:新建的分支“没有”可比较的东西,所以changes一定为true……如果后续在分支上有新的改动,会真正和分支的上一次commit作比较,决定文件是否符合changes。

  • https://stackoverflow.com/a/73569727/7676237
  • https://gitlab.com/gitlab-org/gitlab/-/issues/293645

When pushing a new branch to a project in branch pipeline, CI rules: always evaluated to true, this means that every time a new branch is created all pipeline will run automatically

但是新版本(15.5)的changes已经支持compare_to了,可以指定“相对于谁”的change:

  • https://docs.gitlab.com/ee/ci/yaml/#ruleschangescompare_to
本文由作者按照 CC BY 4.0 进行授权