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

【编者的话】本文作者通过一个简单的小项目详细介绍了如何使用Docker, GitLab, Portainer等组件搭建一套CICD流水线

 

长文预警

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

示例项目

作为一名法国索菲亚科技园区(位于法国南部)的技术活动组织者,我经常被问到是否有办法知道所有即将举行的活动(会议,灌水,由当地协会组织的聚会等…)。由于此前并没有一个单独的地方列出所有的这些活动,我便开发了 https://sophia.events ,这是一个非常简单的网站页面,它会尝试维护一份最新的活动列表。此项目的代码可以在 Gitlab 上找到。

声明:这个项目超级简单,但是项目本身的复杂度并不是本文的重点。这里我们将详细介绍到的CI/CD流水线的各个组件可以用几乎相同的方式应用到更复杂的项目上。它们也非常适合微服务的场景。

快速过一下代码

为了简化起见,这里有一份events.json文件,每个新事件均会被添加到里面。该文件的部分内容见下面的代码段(抱歉里面掺杂了一些法语):

{
  “events”: [
    {
      “title”:All Day DevOps 2018”,
      “desc”:We’re back with 100, 30-minute practitioner-led sessions and live Q&A on Slack. Our 5 tracks include CI/CD, Cloud-Native Infrastructure, DevSecOps, Cultural Transformations, and Site Reliability Engineering. 24 hours. 112 speakers. Free online.”,
      “date”:17 octobre 2018, online event”,
      “ts”:20181017T000000”,
      “link”:https://www.alldaydevops.com/",
      “sponsors”: [{“name”: “all-day-devops”}]
    },
    {
      “title”: “Création d’une Blockchain d’entreprise (lab) & introduction aux smart contracts”,
      “desc”: “Venez avec votre laptop ! Nous vous proposons de nous rejoindre pour réaliser la création d’un premier prototype d’une Blockchain d’entreprise (Lab) et avoir une introduction aux smart contracts.”,
    “ts”: “20181004T181500”,
    “date”: “4 octobre à 18h15 au CEEI”,
    “link”: “https://www.meetup.com/fr-FR/IBM-Cloud-Cote-d-Azur-Meetup/events/254472667/",
    “sponsors”: [{“name”: “ibm”}]
    },
    …
  ]
}

 

此文件将会被一个mustache模板渲染并生成最终的网站素材。

Docker多阶段构建

一旦生成了最终的网站素材,它们将会被拷贝到一个nginx镜像里,该镜像将会被部署到目标机器上。

得益于多阶段构建(multi-stage build),本次构建分为两部分:

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

用来构建镜像的Dockerfile如下:

# 生成素材
FROM node:8.12.0-alpine AS build  
COPY . /build  
WORKDIR /build  
RUN npm i  
RUN node clean.js  
RUN ./node_modules/mustache/bin/mustache events.json index.mustache > index.html

# 构建托管它们的最终镜像
FROM nginx:1.14.0  
COPY --from=build /build/*.html /usr/share/nginx/html/  
COPY events.json /usr/share/nginx/html/  
COPY css /usr/share/nginx/html/css  
COPY js /usr/share/nginx/html/js  
COPY img /usr/share/nginx/html/img

 

本地测试

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

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

$ cd sophia.events

$ ./test.sh
Sending build context to Docker daemon  2.588MB  
Step 1/12 : FROM node:8.12.0-alpine AS build  
 ---> df48b68da02a
Step 2/12 : COPY . /build  
 ---> f4005274aadf
Step 3/12 : WORKDIR /build  
 ---> Running in 5222c3b6cf12
Removing intermediate container 5222c3b6cf12  
 ---> 81947306e4af
Step 4/12 : RUN npm i  
 ---> Running in de4e6182036b
npm notice created a lockfile as package-lock.json. You should commit this file.  
npm WARN www@1.0.0 No repository field.  
added 2 packages from 3 contributors and audited 2 packages in 1.675s  
found 0 vulnerabilities  
Removing intermediate container de4e6182036b  
 ---> d0eb4627e01f
Step 5/12 : RUN node clean.js  
 ---> Running in f4d3c4745901
Removing intermediate container f4d3c4745901  
 ---> 602987ce7162
Step 6/12 : RUN ./node_modules/mustache/bin/mustache events.json index.mustache > index.html  
 ---> Running in 05b5ebd73b89
Removing intermediate container 05b5ebd73b89  
 ---> d982ff9cc61c
Step 7/12 : FROM nginx:1.14.0  
 ---> 86898218889a
Step 8/12 : COPY --from=build /build/*.html /usr/share/nginx/html/  
 ---> Using cache
 ---> e0c25127223f
Step 9/12 : COPY events.json /usr/share/nginx/html/  
 ---> Using cache
 ---> 64e8a1c5e79d
Step 10/12 : COPY css /usr/share/nginx/html/css  
 ---> Using cache
 ---> e524c31b64c2
Step 11/12 : COPY js /usr/share/nginx/html/js  
 ---> Using cache
 ---> 1ef9dece9bb4
Step 12/12 : COPY img /usr/share/nginx/html/img  
 ---> e50bf7836d2f
Successfully built e50bf7836d2f  
Successfully tagged registry.gitlab.com/lucj/sophia.events:latest  
=> web site available on http://localhost:32768

我们可以使用上述输出的末尾提供的URL访问网站页面。

目标环境

云厂商创建的一台虚拟机

或许你也注意到了,这个网站并不是那么关键(每天只有几十次访问),也因此它只需要跑在一台单个的虚拟机上即可。该虚拟机是由Exoscale,一个伟大的欧洲云厂商,它上面的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-failure
networks:  
  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是一套很棒的wbe UI工具,它可以很方便地管理Docker宿主机和Docker Swarm集群。下面是Portainer操作界面的一张截图,里面列出了swarm集群里当前可用的stack。

当前设定下有3个stack:

  • Portainer自己
  • 包含了跑着我们网站的服务的sophia_events
  • tls,TLS termination服务

如果列出跑在sophia_events stack里的www服务的明细的话,我们将可以看到该服务的webhook已经处于激活状态。Portainer 1.19.2(迄今为止最新的版本)已经加入了这一功能的支持,它允许定义一个HTTP Post端点,可以在被调用后触发一次服务的更新。正如我们稍后将会看到的,Gitlab runner会负责调用这个webhook。

备注:从屏幕截图中可以看到,笔者是通过 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-config  
docker 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。

用来注册一个新的runner的注册token。

一旦runner注册上了,我们需要启动它:

CONFIG_FOLDER=/tmp/gitlab-runner-config  
docker 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上的项目页面里。

为此项目创建的runner。

每当有新的commit推送到仓库,此runner随后便会接收到一些要做的任务。它会按顺序执行.gitlab-ci.yml文件里定义好的测试、构建和部署几个阶段。

variables:  
  CONTAINER_IMAGE: registry.gitlab.com/$CI_PROJECT_PATH
  DOCKER_HOST: tcp://docker:2375
stages:  
  - test
  - build
  - deploy
test:  
  stage: test
  image: node:8.12.0-alpine
  script:
    - npm i
    - npm test
build:  
  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:
    - master
deploy:  
  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设置里找到。

 

备注:

  • 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

部署流程

新版本的站点更新遵循如下流程:

  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套接字用法的相关信息,也许你会对之前这篇文章About /var/run/docker.sock感兴趣。

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

示例

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

$ git commit -m 'Fix image'

$ git push origin master

 

如下截图展示了Gitlab.com上的项目页面里的commit触发的流水线作业。

在Portainer一侧,它将会收到一个webhook请求,随后会执行一次服务的更新操作。这里可能看不太清,但是一个副本已经完成了更新,通过第二个副本可以访问站点。随后,几秒钟之后,第二个副本也更新完毕。

小结

即便对于这样一个小项目,为它建立一套CI/CD流水线也是一个很好的练习,尤其是可以更加熟悉GitLab(这一直在笔者要学习的列表里面),它是一个非常出色而且专业的产品。这也是一次体验大家期待已久的Portainer的最新版本(1.19.2)推出的webhook功能的机会。此外,对于像这样的副项目,Docker Swarm的使用是无脑上手的,很酷而且易于使用……

原文链接:even the smallest side project deserves its ci cd pipeline(译者:吴佳兴)

发表评论