背景
最近负责公司的 Devops 改造,使用 Gitlab CI/CD 替代之前的 Jenkins。
为什么选择 Gitlab CI/CD 而不是 Jenkins?
- 不引入其他服务,降低复杂度。
- 公司已经采用 Gitlab 来做源码管理了。
- 触发更简单,更敏捷,更灵活。
- 当在开发环境调试时,有的问题需要在环境里才能复现,就需要经常更新代码到环境,如果是 jenkins,需要到页面上点一下触发。而使用 Gitlab CI/CD,只需提交代码就能触发。一次两次倒没啥区别,但像刚才的情况,需要频繁改动代码,更新到环境,两者既有一点差别了。
- 灵活配置,在合并请求下的提交,触发流水线更新到测试环境。打 tag 触发更新到发布环境。
- 方便合并代码前的 CI
- CI 可以检测代码,单元测试,在 pr 时可以明显的看到当前 pr 的流水线通过情况。
- jenkins 需要通过 gitlab 的 webhook 才能实现,得去配置。流水线越复杂,需要配置的就越多。
- Jenkins 支持插件,功能更强大,但是中小项目 Gitlab CI/CD 够用了。
所以,就开始了 3 个月的 Devops 改造,不仅完美替换了以前的 Jenkins,还实现了很多高效、便捷、以前没有的流水线。例如:
- Gitlab CI/CD 实践五:基础镜像 Dcokerfile 仓库 CI 流水线配置
- Gitlab CI/CD 实践六:统一管理 protocol buffer,API 大仓设计与实现
- Gitlab CI/CD 实践八:同步文件到其他 Git 仓库。
Gitlab Runner
Gitlab Runner 可以直接使用二进制、Docker 或者 k8s 来部署,而使用 k8s 部署带来的的好处是:合理利用资源,工作容器会被调度到资源相对空闲的节点(构建是一个比较耗费资源的过程)。
创建单独的 namespace
gitlab-namespace.yaml
apiVersion: v1 kind: Namespace metadata: name: gitlab
注册 Runner
Gitlab Runner 有 3 种级别
- 全局共享
- 因为 executor 使用的是容器,不是 shell,所以非特殊要求,使用这个就行。如果是 shell,可能每个项目用到的环境不同,需要单独使用 runner
- 群组共享
- 项目独占
Runner 的并发性
- 每个 job 会单独起一个容器
- 不同流水线的 job 是并行处理
- 同一流水线同一阶段的 job 也是并行处理
获取 Gitlab CI Register Token
访问全局 Runner 配置地址:https://{gitlab 地址}/admin/runners
gitlab-ci-token-secret.yaml
apiVersion: v1 kind: Secret metadata: name: gitlab-ci-token namespace: gitlab labels: app: gitlab-ci-runner data: GITLAB_CI_TOKEN: XXXGYTdVdE1zc2lXeXpXcXRVTXI=
GITLAB_CI_TOKEN
:Gitlab CI Register Token 的 base64 编码
配置存储
创建对象存储 bucket
因构建过程需要缓存一些文件,例如依赖,所以要给 Runner 配置存储。这里用到的分布式存储是 ceph,首先创建 Runner 专用的账户和对象存储 bucket,参照这篇教程:Go 项目基于 Gitlab CI/CD 实践二:Rook Ceph 创建 S3 bucket 用于 Gitlab Runner 缓存。
gitlab-runner-cache-s3-secret.yaml
apiVersion: v1 kind: Secret metadata: name: gitlab-runner-cache-s3 namespace: gitlab labels: app: gitlab-ci-runner data: CACHE_S3_ACCESS_KEY: "MDZTSEs5T05OUkxYQjdLU1E0UDM=" CACHE_S3_SECRET_KEY: "QXVkc3JhQlN3alZtNWhqTFo3WG9CbUE2UGU1Q2o3SkJZblQ0R3lUQw=="
CACHE_S3_ACCESS_KEY
:对象存储 bucket 的 AKbase64 编码CACHE_S3_SECRET_KEY
:对象存储 bucket 的 SKbase64 编码
配置 SSL 证书
由于 gitlab 服务是 https,runner 访问 gitlab 的注册接口时,需要证书。
获取 SSL 证书
如果 gitlab 是 docker 部署的,ssl 证书所在路径为:/etc/gitlab/ssl/xxx.com.crt
gitlab-certs-configmap.yaml
apiVersion: v1 data: xxx.com.crt: | -----BEGIN CERTIFICATE----- MIIFSjCCBDKgAwIBAgIUSmsUy8IIhVT1Vl+RbRuO2DY5F3QwDQYJKoZIhvcNAQEN... +xxxnphZ/4JE3n3OKiw= -----END CERTIFICATE----- kind: ConfigMap metadata: labels: app: gitlab-ci-runner name: gitlab-ci-runner-certs namespace: gitlab
配置注册 & 注销脚本
默认只有当 Pod 正常通过 Kubernetes(TERM 信号)终止时,才会触发 Runner 取消注册。 如果强制终止 Pod(SIGKILL 信号),Runner 将不会注销自身。必须手动清理这种被杀死的 Runner 。
gitlab-runner-scripts-configmap.yaml
apiVersion: v1 data: run.sh: | #!/bin/bash unregister() { kill %1 echo "Unregistering runner ${RUNNER_NAME} ..." /usr/bin/gitlab-ci-multi-runner unregister -t "$(/usr/bin/gitlab-ci-multi-runner list 2>&1 | tail -n1 | awk '{print $4}' | cut -d'=' -f2)" -n ${RUNNER_NAME} exit $? } trap 'unregister' EXIT HUP INT QUIT PIPE TERM mkdir -p /home/gitlab-runner/.gitlab-runner/certs cp /certs/xxx.com.crt /home/gitlab-runner/.gitlab-runner/certs/ echo "Registering runner ${RUNNER_NAME} ..." /usr/bin/gitlab-ci-multi-runner register -r ${GITLAB_CI_TOKEN} sed -i 's/^concurrent.*/concurrent = '"${RUNNER_REQUEST_CONCURRENCY}"'/' /home/gitlab-runner/.gitlab-runner/config.toml cat >>/home/gitlab-runner/.gitlab-runner/config.toml <<EOF [[runners.kubernetes.volumes.host_path]] name = "docker" mount_path = "/var/run/docker.sock" read_only = true host_path = "/var/run/docker.sock" EOF echo "Starting runner ${RUNNER_NAME} ..." /usr/bin/gitlab-ci-multi-runner run -n ${RUNNER_NAME} & wait kind: ConfigMap metadata: labels: app: gitlab-ci-runner name: gitlab-ci-runner-scripts namespace: gitlab
遇到的坑
我把证书的 configmap 挂载到 /certs/
(这个步骤后面会提到),然后在上面的启动脚本里,将证书文件从 /certs/
拷贝到 /home/gitlab-runner/.gitlab-runner/certs/
,那为什么不直接把证书挂载到 /home/gitlab-runner/.gitlab-runner/certs/
?
如果这么干,/home/gitlab-runner/.gitlab-runner/certs
目录的所有者就是 root,而 runner 容器没有用 root 用户运行,用的是 gitlab-runner
用户。
在执行注册脚本 /usr/bin/gitlab-ci-multi-runner register -r ${GITLAB_CI_TOKEN}
成功时,会创建 runner 的配置文件 /home/gitlab-runner/.gitlab-runner/config.toml
。这时就会报错:PANIC: open /home/gitlab-runner/.gitlab-runner/config.toml: permission denied
,原因就是 gitlab-runner 用户操作 root 创建的目录造成的权限问题。
配置 RBAC
gitlab-runner-rbac.yaml
apiVersion: v1 kind: ServiceAccount metadata: name: gitlab-ci namespace: gitlab --- kind: Role apiVersion: rbac.authorization.k8s.io/v1 metadata: name: gitlab-ci namespace: gitlab rules: - apiGroups: [""] resources: ["*"] verbs: ["*"] --- kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: gitlab-ci namespace: gitlab subjects: - kind: ServiceAccount name: gitlab-ci namespace: gitlab roleRef: kind: Role name: gitlab-ci apiGroup: rbac.authorization.k8s.io
配置 Runner
gitlab-runner-configmap.yaml
apiVersion: v1 data: CACHE_TYPE: "s3" CACHE_SHARED: "true" CACHE_S3_SERVER_ADDRESS: "rook-ceph-rgw-my-store.rook-ceph.svc" CACHE_S3_BUCKET_NAME: "gitlab-runner-cache-bucket" CACHE_S3_INSECURE: "true" REGISTER_NON_INTERACTIVE: "true" REGISTER_LOCKED: "false" METRICS_SERVER: "0.0.0.0:9100" CI_SERVER_URL: "https://xxx.com/ci" RUNNER_REQUEST_CONCURRENCY: "4" RUNNER_EXECUTOR: "kubernetes" KUBERNETES_NAMESPACE: "gitlab" KUBERNETES_PRIVILEGED: "true" KUBERNETES_CPU_LIMIT: "3" KUBERNETES_MEMORY_LIMIT: "4Gi" KUBERNETES_SERVICE_CPU_LIMIT: "3" KUBERNETES_SERVICE_MEMORY_LIMIT: "4Gi" KUBERNETES_HELPER_CPU_LIMIT: "500m" KUBERNETES_HELPER_MEMORY_LIMIT: "500Mi" KUBERNETES_PULL_POLICY: "if-not-present" KUBERNETES_TERMINATIONGRACEPERIODSECONDS: "10" KUBERNETES_POLL_INTERVAL: "5" KUBERNETES_POLL_TIMEOUT: "360" RUNNER_TAG_LIST: "k8s,100.30.30.192,share" RUNNER_NAME: "192-k8s-runner" kind: ConfigMap metadata: labels: app: gitlab-ci-runner name: gitlab-ci-runner namespace: gitlab
CACHE_S3_SERVER_ADDRESS
:ceph 对象存储 serviceCI_SERVER_URL
:gitlab 地址,如果是在 k8s 里,可以配置 service。记得末尾加上/ci
Runner 资源
gitlab-runner-statefulset.yaml
apiVersion: apps/v1 kind: StatefulSet metadata: name: gitlab-ci-runner namespace: gitlab labels: app: gitlab-ci-runner spec: selector: matchLabels: app: gitlab-ci-runner updateStrategy: type: RollingUpdate replicas: 2 serviceName: gitlab-ci-runner template: metadata: labels: app: gitlab-ci-runner spec: volumes: - name: gitlab-ci-runner-scripts projected: sources: - configMap: name: gitlab-ci-runner-scripts items: - key: run.sh path: run.sh mode: 0755 - name: gitlab-ci-runner-certs projected: sources: - configMap: name: gitlab-ci-runner-certs items: - key: xxx.com.crt path: xxx.com.crt mode: 0777 serviceAccountName: gitlab-ci securityContext: runAsNonRoot: true runAsUser: 999 supplementalGroups: [999] containers: - image: gitlab/gitlab-runner:latest name: gitlab-ci-runner command: - /scripts/run.sh envFrom: - configMapRef: name: gitlab-ci-runner - secretRef: name: gitlab-ci-token - secretRef: name: gitlab-runner-cache-s3 ports: - containerPort: 9100 name: http-metrics protocol: TCP volumeMounts: - name: gitlab-ci-runner-scripts mountPath: "/scripts" readOnly: true - name: gitlab-ci-runner-certs mountPath: "/certs" restartPolicy: Always
解决资源依赖关系
kubectl apply 是按照资源定义文件的文件名来创建资源的,并不会处理依赖关系。也就是可能出现 namespace 还没创建,就开始创建其他资源的情况。这里使用 Kustomize
来保证依赖关系(从 1.14 版本开始,kubectl
也开始支持使用 kustomization 文件来管理 Kubernetes 对象)。
kustomization.yaml
resources: - gitlab-namespace.yaml - gitlab-certs-configmap.yaml - gitlab-ci-token-secret.yaml - gitlab-runner-cache-s3-secret.yaml - gitlab-runner-configmap.yaml - gitlab-runner-rbac.yaml - gitlab-runner-scripts-configmap.yaml - gitlab-runner-statefulset.yaml
- 这里的先后顺序不重要,Kustomize 会自动处理
部署
执行命令
进入资源文件目录
. ├── gitlab-certs-configmap.yaml ├── gitlab-ci-token-secret.yaml ├── gitlab-namespace.yaml ├── gitlab-runner-cache-s3-secret.yaml ├── gitlab-runner-configmap.yaml ├── gitlab-runner-rbac.yaml ├── gitlab-runner-scripts-configmap.yaml ├── gitlab-runner-statefulset.yaml └── kustomization.yaml
执行命令即可:kubectl apply -k .
查看资源状态
kubectl get -k .
查看 Runner 是否注册上
https://{gitlab 地址}/admin/runners
参考资料
20220924 更新
Gitlab CI/CD 已在我司使用了 4 个月,期间不断迭代优化,现在基本稳定了。Gitlab CI/CD 系列 的文章是早期的版本,这里更新下最新的情况。
不建议将 Gitlab Runner 安装到 K8S 集群,因为流水线会使用缓存来提高效率,流水线每次使用缓存时会下载缓存并解压,修改缓存内容后再压缩、上传。而常见的流水线缓存是数量很大的小文件集合,例如 maven 依赖,go mod 依赖,nodejs 依赖等,解压缩、上传下载都很耗时。
经实测发现,直接挂载目录来实现缓存功能是最高效的,而不是使用 gitlab 的缓存关键字。所以建议使用 Docker 安装 Gitlab Runner,并通过挂载目录实现缓存功能。可参考 Gitlab CI/CD 实践三:Docker 安装 Gitlab Runner