DockOne微信分享(一八二):基于 GitLab 的 CI 实践

【编者的话】本文讲述 GitLab CI 的架构及其能力特性,分析它在 DevOps 实践中的作用。 通过分析 Docker In Docker 的技术细节,详细讲述 CI 实践以及在生产环境中的所做的优化,包括但不限于镜像仓库等,以达到数倍的性能提升。 本次分享内容以 GitLab Community Edition 11.0.4 edb037c 为例。 # 为何选择 GitLab CI? ## 认识 GitLab CI 什么是 GitLab CI? GitLab CI 是 GitLab 为了提升其在软件开发工程中作用,完善 DevOps 理念所加入的 CI/CD 基础功能。可以便捷的融入软件开发环节中。通过 GitLab CI 可以定义完善的 CI/CD Pipeline。 优势 * GitLab CI 是默认包含在 GitLab 中的,我们的代码使用 GitLab 进行托管,这样可以很容易的进行集成 * GitLab CI 的前端界面比较美观,容易被人接受 * 包含实时构建日志,容易追踪 * 采用 C/S 的架构,可方面的进行横向扩展,性能上不会有影响 * 使用 YAML 进行配置,任何人都可以很方便的使用。 ## 重点概念 Pipeline Pipeline 相当于一个构建任务,里面可以包含多个流程,如依赖安装、编译、测试、部署等。 任何提交或者 Merge Request 的合并都可以触发 Pipeline Stages Stage 表示构建的阶段,即上面提到的流程。
  • 所有 Stages 按顺序执行,即当一个 Stage 完成后,下一个 Stage 才会开始
  • 任一 Stage 失败,后面的 Stages 将永不会执行,Pipeline 失败
  • 只有当所有 Stages 完成后,Pipeline 才会成功
JobsJob 是 Stage 中的任务。
  • 相同 Stage 中的 Jobs 会并行执行
  • 任一 Job 失败,那么 Stage 失败,Pipeline 失败
  • 相同 Stage 中的 Jobs 都执行成功时,该 Stage 成功
好的,基本的概念已经和大家介绍了, 大家可以发现,上面说的概念,没有提到任务的实际执行者,那任务在哪里执行呢?## GitLab RunnerRunner 是任务的实际执行者, 可以在 MacOS/Linux/Windows 等系统上运行。使用 Golang 进行开发。 同时也可部署在 Kubernetes 上。注册
docker run --rm -t -i -v /path/to/config:/etc/gitlab-runner --name gitlab-runner gitlab/gitlab-runner register \  --executor "docker" \  --docker-image alpine:3 \  --url "https://gitlab.com/" \  --registration-token "PROJECT_REGISTRATION_TOKEN" \  --description "docker-runner" \  --tag-list "dev" \  --run-untagged \  --locked="true"
上面的示例为将 Runner 注册为一个容器, 当然大家也可以直接在物理机上执行。 在物理机上的注册方式与注册为容器大致相同。
sudo gitlab-runner register \  --non-interactive \  --url "https://gitlab.com/" \  --registration-token "PROJECT_REGISTRATION_TOKEN" \  --executor "docker" \  --docker-image alpine:3 \  --description "docker-runner" \  --tag-list "docker,aws" \  --run-untagged \  --locked="false" \
(这段代码来自官方文档)接下来,我们来看下 Runner 的类型, 以便在使用时进行区分。 类型
  • Shared:Runner runs jobs from all unassigned projects
  • Group:Runner runs jobs from all unassigned projects in its group
  • Specific:Runner runs jobs from assigned projects
  • Locked:Runner cannot be assigned to other projects
  • Paused:Runner will not receive any new jobs
配置首先最外层的是全局配置, 默认会有
concurrent = 1check_interval = 0
这两个。 比较需要关注的是下面几个:全局配置* concurrent:并发数,0 为无限制。* sentry_dsn:与 Sentry 联动,可以将异常等收集至 Sentry 中。* listen_address:暴露出 metrics 供 Prometheus 监控。Executor
  • Shell
  • Docker(本次的分享内容)
  • Docker Machine and Docker Machine SSH(autoscaling)
  • Parallels
  • VirtualBox
  • SSH
  • Kubernetes(推荐)
# 详解 Docker In Docker ## 概述 Docker In Docker 简称 dind,在 GitLab CI 的使用中,可能会常被用于 Service 的部分。 dind 表示在 Docker 中实际运行了一个 Docker 容器,或 Docker daemon。 其实如果只是在 Docker 中执行 Docker 命令, 那装个二进制文件即可。但是如果想要运行 Docker daemon(比如需要执行 docker info)或者访问任意的设备都是不允许的。 Docker 在 run 命令中提供了两个很重要的选项 --privileged 和 --device , 另外的选项比如 --cap-add 和 --cap-drop 跟权限也很相关,不过不是今天的重点,按下不表。 --device 选项可以供我们在不使用 --privileged 选项时,访问到指定设备,比如 docker run --device=/dev/sda:/dev/xvdc --rm -it ubuntu fdisk /dev/xvdc 但是这也只是有限的权限, 我们知道 Docker 的技术实现其实是基于 CGroup 的资源隔离,而 --device 却不足于让我们在容器内有足够的权限来完成 Docker daemon 的启动。 在 2013年 左右, --privileged 选项被加入 Docker, 这让我们在容器内启动容器变成了可能。 虽然 --privileged 的初始想法是为了能让容器开发更加便利,不过有些人在使用的时候,其实可能有些误解。 有时候,我们可能只是想要能够在容器内正常的build 镜像,或者是与 Docker daemon 进行交互,例如 Docker images 等命令。 那么,我们其实不需要 dind, 我们需要的是 Docker Out Of Docker,即 dood,在使用的时候,其实是将 docker.sock 挂载入容器内 例如, 使用如下命令:
sudo docker run --rm -ti -v /var/run/docker.sock:/var/run/docker.sock taobeier/docker /bin/sh
在容器内可进行正常的 Docker images 等操作, 同时需要注意,在容器内的动作,将影响到 宿主机上的 Docker daemon。 ## 如何实现 * 创建组和用户,并将用户加入该组。 使用 groupadd 和 useradd 命令。 * 更新 subuid 和 subgid 文件, 将新用户和组配置到 /etc/subgid 和 /etc/subuid 文件中。 subuid 和 subgid 规定了允许用户使用的从属 ID。 * 接下来需要挂载 /sys/kernel/security 为 securityfs 类型可以使用 mountpoint 命令进行测试 mountpoint /sys/kernel/security 如果不是一个挂载点, 那么使用 mount -t securityfs none /sys/kernel/security 进行挂载。如果没有挂载成功的话, 可以检查是否是 SELinux 或者 AppArmor 阻止了这个行为。这里详细的安全问题,可以参考 Linux Security Modules (LSM)。 * 接下来允许 dockerd 命令启动 daemon 即可, dockerd --host=unix:///var/run/docker.sock --host=tcp://0.0.0.0:2375 即可将docker daemon 监听至 2375 端口。 ## 简单做法 可以直接使用 Docker 官方镜像仓库中的 docker:dind 镜像, 但是在运行时, 需要指定 --privileged 选项。 # CI 实践 ## Runner 实践 看 Runner 部分的配置:
[[runners]]
  name = "docker"
  url = "https://gitlab.example.com/"
  token = "TOKEN"
  limit = 0
  executor = "docker"
  builds_dir = ""
  shell = ""
  environment = ["ENV=value", "LC_ALL=en_US.UTF-8"]
  clone_url = "http://172.17.0.4"
由于网络原因, clone_url 可以配置为可访问的地址,这样代码 clone 的时候,将会使用配置的这个地址。实际请求为 `http://gitlab-ci-token:TOKEN@172.17.0.4/namespace/project.git`。 再看一下 runners.docker 的配置,这部分将影响 Docker 的实际运行:
[runners.docker]
  host = ""
  hostname = ""
  tls_cert_path = "/home/tao/certs"
  image = "docker"
  dns = ["8.8.8.8"]
  privileged = false
  userns_mode = "host"
  devices = ["/dev/net/tun"]
  disable_cache = false
  wait_for_services_timeout = 30
  cache_dir = ""
  volumes = ["/data", "/home/project/cache"]
  extra_hosts = ["other-host:127.0.0.1"]
  services = ["mongo", "redis:3"]
  allowed_images = ["go:[i]", "python:[/i]", "java:*"] 
DNS,Privileged,extra_hosts,Services 比较关键, 尤其是在生产中网络情况多种多样, 需要格外关注。 至于 Devices 配置 ,在今儿分享的一开始已经讲过了, allowed_images 的话, 是做了个限制。 上面几个配置项, 用过 Docker 的同学,应该很容易理解。 我们来看下 Services 这个配置项:
image: registry.docker-cn.com/taobeier/docker

variables:
  DOCKER_DRIVER: overlay2    # overlay2 is best bug need kernel >= 4.2

services:
    - name: registry.docker-cn.com/taobeier/docker:stable-dind
      alias: docker

stages:
  - build
  - deploy


build_and_test:
  stage: build
  tags:
    - build
  script:
    # change repo
    #- sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
    # 使用默认官方源 apk 耗时 7min 30s.  修改后 耗时 18s
    - ping -c 1 docker
    - ping -c 1 registry.docker-cn.com__taobeier__docker
    - ipaddr
    - apk add --no-cache py-pip 
    # 使用默认耗时 1 min 15s.  修改后耗时 43s
    - pip install -i https://mirrors.ustc.edu.cn/pypi/web/simple docker-compose
    - docker-compose up -d
    - docker-compose run --rm web pytest -s -v tests/test_session.py

deploy:
  image: "registry.docker-cn.com/library/centos"
  stage: deploy
  tags:
    - deploy
  script:
    # install ssh client
    - 'ssh-agent || (yum install -y openssh-clients)'
    # run ssh-agent
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null
    # create ssh dir
    - mkdir -p ~/.ssh
    - chmod 700 ~/.ssh
    # use ssh-keyscan to get key
    - ssh-keyscan -p $SSH_PORT $DEPLOY_HOST >> ~/.ssh/known_hosts
    - chmod 644 ~/.ssh/known_hosts

    # - ssh -p $SSH_PORT $DEPLOY_USER@$DEPLOY_HOST ls
    - rm -rf .git
    - scp -r -P $SSH_PORT . $DEPLOY_USER@$DEPLOY_HOST:~/we/
Services 的本质其实是使用了 Docker 的 --link ,我们来看下它如何工作: ## Docker Executor 如何工作 * 创建 service 容器(已经配置在 service 中的镜像) * 创建 cache 容器(存储已经配置在 config.toml 的卷和构建镜像的 Dockerfile) * 创建 build 容器 并且 link 所有的 service 容器 * 启动 build 容器 并且发送 job 脚本到该容器中 * 执行 job 的脚本 * 检出代码:/builds/group-name/project-name/ * 执行 .gitlab-ci.yml 中定义的步骤 * 检查脚本执行后的状态码,如果非 0 则构建失败 * 移除 build 和 service 容器. ## 私有镜像源 用户认证需要 GitLab Runner 1.8 或更高版本,在 0.6 ~ 1.8 版本之间的 Runner 需要自行去 Runner 的机器上手动执行。 默认情况下,如果访问的镜像仓库需要认证的话, GitLab Runner 会使用 DOCKER_AUTH_CONFIG 变量的作为认证的凭证。 注意:DOCKER_AUTH_CONFIG 是完整的 docker auth 凭证,也就是说,它应该和我们 ~/.docker/config.json 中的内容一致,例如:
 {
     "auths": {
         "registry.example.com": {
             "auth": "5oiR5piv5byg5pmL5rab"
         }
     }
 } 
简单的做法就是,我们在本地/服务器上执行 docker login 私有镜像源 登录成功后,将 ~/.docker/config.json 的文件内容直接复制,作为我们的变量的值 或者是 echo -n '用户名:密码' |base64 以这样的方式来获得 auth 的内容,组装成对应的格式,写入 GitLab 的 value 配置中。 # 生产环境中的 CI 性能优化 1、使用国内源对容器镜像进行加速 例如:使用 Docker 中国官方镜像加速服务 https://registry.docker-cn.com 当然各家公司其实也有提供镜像加速的服务。 2、使用私有镜像仓库。例如 Docker Registry,或者 Harbor,我们是在使用 Harbor 作为私有镜像仓库的。 因为网络的原因, 如果默认使用官方镜像, 1. 官方镜像拉不下来;2. 在官方镜像中安装包耗时长;3. 如果换源,需要每个 Dockerfile 都要做相同的事情。 这我们当然是不能同意的。 所以,我们构建了自己的私有镜像。 从 BusyBox 开始 构建 alpine linux 使用私有源, 以此为基础 构建我们所需要的其他镜像。 用户不再需要自行换源。 这个操作完成后, 原先我们需要在 CI 执行的过程中安装 py-pip(为了安装 docker-compose 和我们的服务依赖)耗时从 3min30s 减少到了 18s。 这里,需要说下为何我们是从头开始构建镜像,而不是基于官方镜像。 主要是为了减少镜像体积 以及为了更快的适用于我们的需求。 同样的,我们构建了基础的 Docker 镜像,Python/Maven 等镜像,都是默认使用了我们的私有源,并且,用户在使用时, 并不需要关注换源的事情, 减少用户的心智负担。 3、规范 Dockerfile, 减少不必要的依赖安装,减少镜像体积。其实结合上面的部分,我们做的事情是直接构建了我们的基础镜像 Docker/Alpine/Maven之类的基础镜像,默认直接都换了源。这样既方便使用,还可以减少镜像层数。 4、拆分 job, 通过 tag 的方式可指定 Runner, 由不同的 Runner 来并行执行无强依赖的一些动作。 便于分摊压力。 5、使用 Cache,CI 的构建中,大多数的镜像,其实变化不大,所以使用 Cache 可以成倍的提升 CI 的速度。 6、可能遇到的坑,前面提到了 service 中可以使用各种各样的服务, 无论是 dind 还是 MySQL Redis 等。 但是如果我们全部做到了优化,都使用我们的私有源, 那便会发现问题。 因为 GitLab CI 默认对于 docker:dind 的 service 其实会选择连名为Docker 的 host ,以及 2375 端口。 当使用私有镜像源的时候, 比如:
services:
            - name: registry.docker-cn.com/taobeier/docker:stable-dind
那这个 service 的host 是什么呢? 这个 service 的 host 其实是会变成 registry.docker-cn.com__taobeier__docker,然后 GitLab Runner 便会找不到, job 就会执行失败。 有两种解决办法, 一种是加一个变量。
variables:
            DOCKER_HOST: "tcp://registry.docker-cn.com__taobeier__docker:2375"
但是这种方式很麻烦,没有人能完全记住遇到 / 会转换为 _ 难免会有问题。 那么就有了第二种办法:
services:
            - name: registry.docker-cn.com/taobeier/docker:stable-dind
             alias: docker
加一个 alias 。 这个方法目前很少人在用, 毕竟网络上查到的都是第一种 ,但是这个方式却是最简单的。 #Q&A Q:您提到把各种依赖都以 Service 的提供,请问是以哪种方式呢? 比如Python的依赖,怎么做成Service呢?

A:Service 化的依赖,主要是指类似 DB / MySQL/ Reids 之类的。 或者是 dind 其实它提供的是 2375 端口的TCP服务。 Python 的依赖,我推荐的做法是, 构建一个换了源的 Python 镜像。 安装依赖的时候,耗时会少很多。 或者说, 可以在定义 Pipeline 的时候, 将虚拟环境的 venv 文件夹作为 cache ,之后的安装也会检查这个,避免不必要的安装。

Q:请问,你们为什么不用Jenkins Pipeline,而使用GitLab CI?

A:主要原因是我提到的那几个方面。 集成较好, 界面美观优雅, 使用简单(所有有仓库写权限的人 都可以使用, 只要创建 .gitlab-ci.yml 并且配置了 Runner 即可使用) 。换个角度,我们来看下使用Jenkins 的问题, Jenkins 对于项目的配置其实和 GitLab 的代码是分离的, 两部分的, 用户(或者说我们的开发者)在使用的时候, 需要有两个平台, 并且,大多数时候, Jenkins 的权限是不放开的。 对用户来讲, 那相当于是个黑盒。 那可能的问题是什么呢? 遇到构建失败了, 但是只有运维知道发生了什么,但是研发无能为力,因为没有权限。 使用GItLab的好处,这个时候就更加突出了, 配置就在代码仓库里面,并且使用 YAML 的配置,很简单。 有啥问题,直接查,直接改。

Q:关于 Runner 的清理的问题,在长时间使用后,Runner 机器上回产生很多的 Cache 容器,如何清理呢。能够在任务中自动清除吗?

A:这个就相对简单了,首先, 如果你的 Cache 容器确认没用了, 每个 Cache 容器其实都有名字的, 直接按 Cache 的名字过略, 批量删掉。 如果你不确定它是否有用,那你直接删掉也是不影响的, 因为 Docker Excutor 的执行机制是创建完 Service 容器后, 创建 Cache 容器。 要是删掉了,它只是会再创建一次。 如果你想在任务中清除, 目前还没做相关的实践,待我实践后,看看有没有很优雅的方式。

Q:请问下Maven的settings.xml怎么处理?本地Maven仓库呢?

A:我们构建了私有的 Maven 镜像, 私有镜像中是默认使用了我们的私有源。 对于项目中用户无需关注 settings.xml 中是否配置repo。

Q:在GitLab的CD方案中,在部署的时候,需要在变量中配置跳板机的私钥,如果这个项目是对公司整部门开发,那么如何保护这个私钥呢?

A:可以使用 secret variable 将私钥写入其中, (但是项目的管理员,具备查看该 variable 的权限)开发一个 web server (其实只要暴露 IP 端口之类的就可以) 在 CI 执行的过程中去请求, server 对来源做判断 (比如 执行CI 的时候,会有一些特定的变量,以此来判断,是否真的是 CI 在请求)然后返回私钥。

Q:GitLab CI适合什么类型的项目呢?国内目前还比较小众吧?

A:国内目前还较为小众(相比 Jenkins 来说)其实只要需要 CI 的项目,它都适合。

以上内容根据2018年7月24日晚微信群分享内容整理。分享人张晋涛,网易有道资深运维开发工程师,负责推进 DevOps 的实践落地,以及容器化及自动化平台的规划建设等。DockOne每周都会组织定向的技术分享,欢迎感兴趣的同学加微信:liyingjiesd,进群参与,您有想听的话题或者想分享的话题都可以给我们留言。

0 个评论

要回复文章请先登录注册