现如今,使用市面上的一些工具配置一套简单的CI/CD流水线并不是一件难事。给一个副项目弄一套这样的流水线也是一个学习许多东西的好方法。Docker,Gitlab,Portainer这些优秀的组件可以用来搭建这个流水线。
示例项目
此文件将会被一个mustache模板[2]渲染并生成最终的网站素材。
Docker多阶段构建
一旦生成了最终的网站素材,它们将会被拷贝到一个Nginx镜像里,该镜像将会被部署到目标机器上。
得益于多阶段构建(multi-stage build),本次构建分为两部分:
网站素材的生成
包含网站素材的最终镜像的创建
本地测试
为了测试生成站点,只需克隆该仓库然后运行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访问网站页面。
目标环境
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服务。
$ docker stack deploy -c sophia.yml sophia_events
统御一切的Portainer
Portainer[4]是一套很棒的Wbe UI工具,它可以很方便地管理Docker宿主机和Docker Swarm集群。下面是Portainer操作界面的一张截图,里面列出了Swarm集群里当前可用的stack。
当前设定下有3个stack:
Portainer自己
包含了跑着我们网站的服务的sophia_events
tls,TLS termination服务
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。
用来注册一个新的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上的项目页面里。
为此项目创建的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设置里找到。
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套接字用法的相关信息,也许你会对之前这篇文章《Docker Tips : about /var/run/docker.sock[5]》感兴趣。
随后,用户便能看到新版本的站点。
$ git commit -m 'Fix image'
$ git push origin master
如下截图展示了GitLab.com上的项目页面里的commit触发的流水线作业。
在Portainer一侧,它将会收到一个webhook请求,随后会执行一次服务的更新操作。这里可能看不太清,但是一个副本已经完成了更新,通过第二个副本可以访问站点。随后,几秒钟之后,第二个副本也更新完毕。
小结
即便对于这样一个小项目,为它建立一套CI/CD流水线也是一个很好的练习,尤其是可以更加熟悉GitLab(这一直在笔者要学习的列表里面),它是一个非常出色而且专业的产品。这也是一次体验大家期待已久的Portainer的最新版本(1.19.2)推出的webhook功能的机会。此外,对于像这样的副项目,Docker Swarm的使用是无脑上手的,很酷而且易于使用......
相关链接:
https://gitlab.com/lucj/sophia.events
https://gitlab.com/lucj/sophia.events/blob/master/index.mustache
http://exoscale.ch/
https://portainer.io/
https://medium.com/lucjuggery/about-var-run-docker-sock-3bfd276e12fd