即便一个小项目也有它的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
部署流程
新版本的站点更新遵循如下流程:
- 一个开发者推送了一些变更到Gitlab。这些变更基本上囊括了events.json文件里一个或多个新的事件加上一些额外赞助商的logo。
- Gitlab runner执行在.gitlab-ci.yml里定义好的一组action。
- Gitlab runner调用在Portainer中定义的webhook。
- 在接收到webhook后,Portainer将会部署新版本的www服务。它通过调用Docker Swarm的API实现这一点。Portainer可以通过在启动时绑定挂载的/var/run/docker.sock套接字来访问该API。
如果你想知道更多此unix套接字用法的相关信息,也许你会对之前这篇文章About /var/run/docker.sock感兴趣。
- 随后,用户便能看到新版本的站点。
示例
让我们一起来修改代码里的一些内容随后提交/推送这些变更。
$ 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(译者:吴佳兴)