认识 GitLab CI 什么是 GitLab CI? GitLab CI 是 GitLab 为了提升其在软件开发工程中作用,完善 DevOps 理念所加入的 CI/CD 基础功能。可以便捷的融入软件开发环节中。通过 GitLab CI 可以定义完善的 CI/CD Pipeline。 优势
-
GitLab CI 是默认包含在 GitLab 中的,我们的代码使用 GitLab 进行托管,这样可以很容易的进行集成
-
GitLab CI 的前端界面比较美观,容易被人接受
-
包含实时构建日志,容易追踪
-
采用 C/S 的架构,可方面的进行横向扩展,性能上不会有影响
-
使用 YAML 进行配置,任何人都可以很方便的使用
-
所有 Stages 按顺序执行,即当一个 Stage 完成后,下一个 Stage 才会开始
-
任一 Stage 失败,后面的 Stages 将永不会执行,Pipeline 失败
-
只有当所有 Stages 完成后,Pipeline 才会成功
-
相同 Stage 中的 Jobs 会并行执行
-
任一 Job 失败,那么 Stage 失败,Pipeline 失败
-
相同 Stage 中的 Jobs 都执行成功时,该 Stage 成功
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 监控。
-
Shell
-
Docker(本次的分享内容)
-
Docker Machine and Docker Machine SSH(autoscaling)
-
Parallels
-
VirtualBox
-
SSH
-
Kubernetes(推荐)
概述 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 端口。
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:*", "python:*", "java:*"]
DNS,Privileged,extra_hosts,Services 比较关键, 尤其是在生产中网络情况多种多样, 需要格外关注。 至于 Devices 配置 ,在今儿分享的一开始已经讲过了, allowed_images 的话, 是做了个限制。
上面几个配置项, 用过 Docker 的同学,应该很容易理解。 我们来看下 Services 这个配置项:
image: registry.docker-cn.com/taobeier/dockervariables: DOCKER_DRIVER: overlay2 # overlay2 is best bug need kernel >= 4.2services: - name: registry.docker-cn.com/taobeier/docker:stable-dind alias: dockerstages: - build - deploybuild_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.pydeploy: 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 容器
{ "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 的项目,它都适合。
Kubernetes项目实战训练营
Kubernetes项目实战训练将于2018年8月17日在深圳开课,3天时间带你系统掌握Kubernetes。本次培训包括:Docker介绍、Docker镜像、网络、存储、容器安全;Kubernetes架构、设计理念、常用对象、网络、存储、网络隔离、服务发现与负载均衡;Kubernetes核心组件、Pod、插件、微服务、云原生、Kubernetes Operator、集群灾备、Helm等,点击下方图片查看详情。