即便一个小项目也有它的CI/CD流水线

 吴佳兴 译 分布式实验室 

即便一个小项目也有它的CI/CD流水线

现如今,使用市面上的一些工具配置一套简单的CI/CD流水线并不是一件难事。给一个副项目弄一套这样的流水线也是一个学习许多东西的好方法。Docker,Gitlab,Portainer这些优秀的组件可以用来搭建这个流水线。
示例项目

即便一个小项目也有它的CI/CD流水线


作为一名法国索菲亚科技园区(位于法国南部)的技术活动组织者,我经常被问到是否有办法知道所有即将举行的活动(会议,灌水,由当地协会组织的聚会等……)。由于此前并没有一个单独的地方列出所有的这些活动,我便开发了https://sophia.events ,这是一个非常简单的网站页面,它会尝试维护一份最新的活动列表。此项目的代码可以在GitLab[1]上找到。
声明:这个项目超级简单,但是项目本身的复杂度并不是本文的重点。这里我们将详细介绍到的CI/CD流水线的各个组件可以用几乎相同的方式应用到更复杂的项目上。它们也非常适合微服务的场景。
快速过一下代码

即便一个小项目也有它的CI/CD流水线


为了简化起见,这里有一份events.json文件,每个新事件均会被添加到里面。该文件的部分内容见下面的代码段(抱歉里面掺杂了一些法语):即便一个小项目也有它的CI/CD流水线

此文件将会被一个mustache模板[2]渲染并生成最终的网站素材。
Docker多阶段构建
一旦生成了最终的网站素材,它们将会被拷贝到一个Nginx镜像里,该镜像将会被部署到目标机器上。
得益于多阶段构建(multi-stage build),本次构建分为两部分:

  • 网站素材的生成

  • 包含网站素材的最终镜像的创建


用来构建镜像的Dockerfile如下:即便一个小项目也有它的CI/CD流水线


本地测试
为了测试生成站点,只需克隆该仓库然后运行test.sh脚本即可。它将随后创建出一个镜像并运行一个容器:


  1. $ git clone git@gitlab.com:lucj/sophia.events.git


  2. $ cd sophia.events


  3. $ ./test.sh

  4. Sending build context to Docker daemon  2.588MB

  5. Step 1/12 : FROM node:8.12.0-alpine AS build

  6. ---> df48b68da02a

  7. Step 2/12 : COPY . /build

  8. ---> f4005274aadf

  9. Step 3/12 : WORKDIR /build

  10. ---> Running in 5222c3b6cf12

  11. Removing intermediate container 5222c3b6cf12

  12. ---> 81947306e4af

  13. Step 4/12 : RUN npm i

  14. ---> Running in de4e6182036b

  15. npm notice created a lockfile as package-lock.json. You should commit this file.

  16. npm WARN www@1.0.0 No repository field.

  17. added 2 packages from 3 contributors and audited 2 packages in 1.675s

  18. found 0 vulnerabilities

  19. Removing intermediate container de4e6182036b

  20. ---> d0eb4627e01f

  21. Step 5/12 : RUN node clean.js

  22. ---> Running in f4d3c4745901

  23. Removing intermediate container f4d3c4745901

  24. ---> 602987ce7162

  25. Step 6/12 : RUN ./node_modules/mustache/bin/mustache events.json index.mustache > index.html

  26. ---> Running in 05b5ebd73b89

  27. Removing intermediate container 05b5ebd73b89

  28. ---> d982ff9cc61c

  29. Step 7/12 : FROM nginx:1.14.0

  30. ---> 86898218889a

  31. Step 8/12 : COPY --from=build /build/*.html /usr/share/nginx/html/

  32. ---> Using cache

  33. ---> e0c25127223f

  34. Step 9/12 : COPY events.json /usr/share/nginx/html/

  35. ---> Using cache

  36. ---> 64e8a1c5e79d

  37. Step 10/12 : COPY css /usr/share/nginx/html/css

  38. ---> Using cache

  39. ---> e524c31b64c2

  40. Step 11/12 : COPY js /usr/share/nginx/html/js

  41. ---> Using cache

  42. ---> 1ef9dece9bb4

  43. Step 12/12 : COPY img /usr/share/nginx/html/img

  44. ---> e50bf7836d2f

  45. Successfully built e50bf7836d2f

  46. Successfully tagged registry.gitlab.com/lucj/sophia.events:latest

  47. => web site available on http://localhost:32768


我们可以使用上述输出的末尾提供的URL访问网站页面。 
即便一个小项目也有它的CI/CD流水线
目标环境

即便一个小项目也有它的CI/CD流水线


云厂商创建的一台虚拟机
或许你也注意到了,这个网站并不是那么关键(每天只有几十次访问),也因此它只需要跑在一台单个的虚拟机上即可。该虚拟机是由Exoscale[3],一个伟大的欧洲云厂商,它上面的Docker Machine创建出来的。
顺便一提,如果你想试试Exoscale的服务的话,知会我一声,我可以提供20欧元的优惠券。
以Swarm模式启动的Docker守护进程
在上面这台虚拟机上运行的Docker守护进程被配置成以Swarm模式运行,因此它支持使用Docker Swarm原生提供的stack,service,config以及secret等原语和它强大(且易于使用)的编排功能。
以docker stack形式运行的应用
下述文件内容里定义了一个包含网站素材的nginx web服务器作为一个服务(service)运行。

version: "3.7"services:  www:    image: registry.gitlab.com/lucj/sophia.events    networks:      - proxy    deploy:      mode: replicated      replicas: 2      update_config:        parallelism: 1        delay: 10s      restart_policy:        condition: on-failurenetworks:  proxy:    external: true


这里有几处需要解释下:

  • 镜像存储在托管到gitlab.com的私有镜像仓库(这里没涉及到Docker Hub)。

  • 服务是以2个副本的形式运行在副本模式下,这也就意味着同一时间该服务会有两个正在运行中的任务/容器。Swarm的service会关联一个VIP(虚拟IP地址),这样一来目标是该服务的每个请求会在两个副本之间实现负载均衡。

  • 每次完成服务更新时(部署一个新版本的网站),其中一个副本会被更新,然后在10秒后更新第二个副本。这可以确保在更新期间整个网站仍然可用。我们也可以使用回滚策略,但是在这里没有必要。

  • 服务会被绑定到一个外部的代理网络,这样一来TLS termination(在Swarm里部署的,跑在另外一个服务里,但是超出本项目的范畴)可以发送请求给www服务。


要运行这个stack只需要执行如下命令:

$ docker stack deploy -c sophia.yml sophia_events


统御一切的Portainer
Portainer[4]是一套很棒的Wbe UI工具,它可以很方便地管理Docker宿主机和Docker Swarm集群。下面是Portainer操作界面的一张截图,里面列出了Swarm集群里当前可用的stack。 即便一个小项目也有它的CI/CD流水线
当前设定下有3个stack:

  • Portainer自己

  • 包含了跑着我们网站的服务的sophia_events

  • tls,TLS termination服务


如果列出跑在sophia_events stack里的www服务的明细的话,我们将可以看到该服务的webhook已经处于激活状态。Portainer 1.19.2(迄今为止最新的版本)已经加入了这一功能的支持,它允许定义一个HTTP Post端点,可以在被调用后触发一次服务的更新。正如我们稍后将会看到的,GitLab runner会负责调用这个webhook。 
即便一个小项目也有它的CI/CD流水线
备注:从屏幕截图中可以看到,笔者是通过localhost:8888这个地址访问Portainer的用户界面。由于笔者不想将Portainer实例对外暴露,因此是通过SSH隧道访问,该隧道可以通过如下命令开启:

ssh -i ~/.docker/machine/machines/labs/id_rsa -NL 8888:localhost:9000 $USER@$HOST


这样一来,目标是本地机器上的8888端口的所有请求均会通过SSH转发到虚拟机上的9000端口上。9000端口是Portainer在虚拟机上运行时监听的端口,但是并未对外开放,因为它被Exoscale配置的一个安全组禁用了。
备注:在上述命令里,用来连接虚拟机的ssh key是在虚拟机创建时由Docker Machine生成的一个key。
GitLab runner
Gitlab的runner是一个负责执行定义在.gitlab-ci.yml文件里的一组action的进程。就我们这个项目来说,我们定义了一个我们自己的runner,它在虚拟机上以一个容器的形式运行。
第一步就是带上一堆参数来注册该runner。

CONFIG_FOLDER=/tmp/gitlab-runner-configdocker run — rm -t -i \ -v $CONFIG_FOLDER:/etc/gitlab-runner \ gitlab/gitlab-runner register \   --non-interactive \   --executor "docker" \   —-docker-image docker:stable \   --url "https://gitlab.com/" \   —-registration-token "$PROJECT_TOKEN" \   —-description "Exoscale Docker Runner" \   --tag-list "docker" \   --run-untagged \   —-locked="false" \   --docker-privileged


在上述参数中,PROJECT_TOKEN可以在GitLab.com的项目页面上找到,并可以用来注册外部的runner。 
即便一个小项目也有它的CI/CD流水线
用来注册一个新的runner的注册token。
一旦runner注册上了,我们需要启动它:

CONFIG_FOLDER=/tmp/gitlab-runner-configdocker run -d \ --name gitlab-runner \ —-restart always \ -v $CONFIG_FOLDER:/etc/gitlab-runner \ -v /var/run/docker.sock:/var/run/docker.sock \ gitlab/gitlab-runner:latest


等到它注册上了而且启动起来了,该runner便会出现在GitLab.com上的项目页面里。 
即便一个小项目也有它的CI/CD流水线
为此项目创建的runner。
每当有新的commit推送到仓库,此runner随后便会接收到一些要做的任务。它会按顺序执行.gitlab-ci.yml文件里定义好的测试、构建和部署几个阶段。

variables:  CONTAINER_IMAGE: registry.gitlab.com/$CI_PROJECT_PATH  DOCKER_HOST: tcp://docker:2375stages:  - test  - build  - deploytest:  stage: test  image: node:8.12.0-alpine  script:    - npm i    - npm testbuild:  stage: build  image: docker:stable  services:    - docker:dind  script:    - docker image build -t $CONTAINER_IMAGE:$CI_BUILD_REF -t $CONTAINER_IMAGE:latest .    - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.gitlab.com    - docker image push $CONTAINER_IMAGE:latest    - docker image push $CONTAINER_IMAGE:$CI_BUILD_REF  only:    - masterdeploy:  stage: deploy  image: alpine  script:    - apk add --update curl    - curl -XPOST $WWW_WEBHOOK  only:    - master

  • 测试阶段(test stage)将会运行一些预备检查,确保events.json文件格式正确,并且这里没有遗漏镜像

  • 构建阶段(build stage)会做镜像的构建并将它推送到GitLab上的镜像仓库

  • 部署阶段(deploy stage)将会通过发送给Portainer的一个webhook触发一次服务的更新。WWW_WEBHOOK变量的定义可以在Gitlab.com上项目页面的CI/CD设置里找到。


即便一个小项目也有它的CI/CD流水线
备注:
  • runner在Swarm上是以一个容器的形式运行。我们可以使用一个共享的runner,这是一些公用的runner,它们会在托管到GitLab的不同项目所需的任务之间分配时间。但是,由于runner需要访问Portainer的端点(用来发送webhook),也因为笔者不希望Portainer能够从外界访问到,将runner跑在集群里会更安全一些。

  • 再者,由于runner跑在一个容器里,为了能够通过Portainer暴露在宿主机上的9000端口连到Portainer,它会将webhook请求发送到Docker0桥接网络上的IP地址。也因此,webhook将遵循如下格式:http://172.17.0.1:9000/api[…]a7-4af2-a95b-b748d92f1b3b。


部署流程

即便一个小项目也有它的CI/CD流水线


新版本的站点更新遵循如下流程: 
即便一个小项目也有它的CI/CD流水线
  1. 一个开发者推送了一些变更到GitLab。这些变更基本上囊括了events.json文件里一个或多个新的事件加上一些额外赞助商的logo。

  2. Gitlab runner执行在.gitlab-ci.yml里定义好的一组action。

  3. Gitlab runner调用在Portainer中定义的webhook。

  4. 在接收到webhook后,Portainer将会部署新版本的www服务。它通过调用Docker Swarm的API实现这一点。Portainer可以通过在启动时绑定挂载的/var/run/docker.sock套接字来访问该API。

    如果你想知道更多此unix套接字用法的相关信息,也许你会对之前这篇文章《Docker Tips : about /var/run/docker.sock[5]》感兴趣。

  5. 随后,用户便能看到新版本的站点。


示例
让我们一起来修改代码里的一些内容随后提交/推送这些变更。


  1. $ git commit -m 'Fix image'


  2. $ git push origin master


如下截图展示了GitLab.com上的项目页面里的commit触发的流水线作业。 
即便一个小项目也有它的CI/CD流水线
在Portainer一侧,它将会收到一个webhook请求,随后会执行一次服务的更新操作。这里可能看不太清,但是一个副本已经完成了更新,通过第二个副本可以访问站点。随后,几秒钟之后,第二个副本也更新完毕。 即便一个小项目也有它的CI/CD流水线
小结
即便对于这样一个小项目,为它建立一套CI/CD流水线也是一个很好的练习,尤其是可以更加熟悉GitLab(这一直在笔者要学习的列表里面),它是一个非常出色而且专业的产品。这也是一次体验大家期待已久的Portainer的最新版本(1.19.2)推出的webhook功能的机会。此外,对于像这样的副项目,Docker Swarm的使用是无脑上手的,很酷而且易于使用......
相关链接:

  1. https://gitlab.com/lucj/sophia.events

  2. https://gitlab.com/lucj/sophia.events/blob/master/index.mustache

  3. http://exoscale.ch/

  4. https://portainer.io/

  5. https://medium.com/lucjuggery/about-var-run-docker-sock-3bfd276e12fd


原文链接:https://medium.com/lucjuggery/even-the-smallest-side-project-deserves-its-ci-cd-pipeline-281f80f39fdf



上一篇:动手开发第一个 Cypress 测试应用


下一篇:即便一个小项目也有它的CI/CD流水线