最近公司需要用到一个名叫Concourse CI的CI/CD工具,那么我当然就要学习一下啦。顺便还能水一篇,啊不,写一篇博客,当作学习过程中的笔记。
准备数据库 Concourse 使用PostgreSQL数据库来存储数据,所以首先要初始化好一个数据库。
如果要使用自建的数据库,那么可以参考这篇官方文档 。
我这里用的是Railway
1 2 3 4 5 6 7 8 9 10 11 12 CREATE  SCHEMA concourse;CREATE  ROLE concourse WITH  ENCRYPTED PASSWORD 'concourse' ;ALTER  ROLE concourse WITH  LOGIN;GRANT  USAGE,CREATE  ON  SCHEMA concourse TO  concourse;GRANT  ALL  ON  ALL  TABLES IN  SCHEMA concourse TO  concourse;GRANT  ALL  ON  ALL  SEQUENCES IN  SCHEMA concourse TO  concourse;
安装 Concourse CI 这里我将用两台服务器完成 Concourse 的部署,一个用来部署web节点,一个用来部署worker节点。
Web 节点 Concourse 的 web 节点中会运行一个名为TSA的服务用来注册worker节点,所以首先我们要在web节点创建TSA服务所需的 SSH 密钥对。
1 2 3 4 5 6 7 8 cd  ~/docker/concoursessh-keygen -t rsa -b 4096 -m PEM -f ./session_signing_key ssh-keygen -t rsa -b 4096 -m PEM -f ./tsa_host_key touch  authorized_worker_keys
然后编写docker-compose.yml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 version:  '3' services:   concourse:      image:  concourse/concourse:latest      restart:  always      container_name:  concourse           network_mode:  host      privileged:  true           command:  web           volumes:        -  /home/boris1993/docker/concourse:/keys      environment:        TZ:  Asia/Shanghai                      HTTP_PROXY:  http://127.0.0.1:8899        HTTPS_PROXY:  http://127.0.0.1:8899        ALL_PROXY:  socks5://127.0.0.1:8899               CONCOURSE_BIND_PORT:  8085               CONCOURSE_EXTERNAL_URL:  http://192.168.1.123:8085               CONCOURSE_SESSION_SIGNING_KEY:  /keys/session_signing_key        CONCOURSE_TSA_HOST_KEY:  /keys/tsa_host_key        CONCOURSE_TSA_AUTHORIZED_KEYS:  /keys/authorized_worker_keys               CONCOURSE_POSTGRES_HOST:  containers-us-east-123.railway.app        CONCOURSE_POSTGRES_USER:  concourse        CONCOURSE_POSTGRES_PORT:  5511        CONCOURSE_POSTGRES_PASSWORD:  concourse        CONCOURSE_POSTGRES_DATABASE:  railway               CONCOURSE_ADD_LOCAL_USER:  concourse:concourse               CONCOURSE_MAIN_TEAM_LOCAL_USER:  concourse  
接下来执行docker compose up -d启动容器,过几分钟就可以在http://192.168.1.123:8085打开 Concourse 的页面了。首次启动可能耗时比较久,因为要花时间初始化数据库里面的各种表。
Worker 节点 上面启动的web节点只是用来给我们看的,它并不能执行任何的构建任务,所以还需要启动至少一个worker节点来运行构建任务。
首先还是生成密钥:
1 2 3 cd  ~/docker/concourse-workerssh-keygen -t rsa -b 4096 -m PEM -f ./worker_key 
生成了 worker 节点的 SSH 密钥对之后,我们需要把worker_key.pub中的内容添加到 web 节点的authorized_worker_keys文件中,以通知 web 节点可以接受这个 worker 的加入请求。authorized_worker_keys文件改好后需要重启 web 节点的 Docker 容器以使修改生效。
接下来编写docker-compose.yml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 version:  '3' services:   concourse-worker:      image:  concourse/concourse:latest      restart:  always      container_name:  concourse_worker      network_mode:  host      privileged:  true           command:  worker      volumes:               -  /home/ubuntu/docker/concourse:/keys               -  /home/ubuntu/docker/concourse/data:/opt/concourse/      environment:               CONCOURSE_NAME:  'worker-1'               CONCOURSE_RUNTIME:  containerd        CONCOURSE_CONTAINERD_DNS_SERVER:  8.8 .8 .8               CONCOURSE_TSA_HOST:  192.168 .1 .123 :2222               CONCOURSE_TSA_PUBLIC_KEY:  /keys/tsa_host_key.pub        CONCOURSE_TSA_WORKER_PRIVATE_KEY:  /keys/worker_key        CONCOURSE_WORK_DIR:  /opt/concourse/worker  
然后执行docker compose up -d启动即可。
安装 Fly CLI 虽然 Concourse 带有一个 Web 界面,但是我们在 Web 界面里面干不了什么,因为它的所有管理操作都需要通过它的Fly CLI来完成。
要安装Fly CLI,你可以从刚才打开的 Dashboard 里面下载,也可以到 Concourse 的 GitHub Releases 中下载。
macOS 用户可能会想,我能不能用Homebrew来安装这个东西?一开始我也是这么想的,但是后面我发现,fly 的版本是要跟着 web 节点的版本走的,所以死了这条心,老老实实从 Dashboard 里面下载吧。
检查 worker 的状态 为了确保 worker 节点是成功连接到 web 节点,我们需要用fly命令来检查 worker 节点的状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 $ fly login -t default -c http://192.168.1.123:8085 logging in  to team 'main'  navigate to the following URL in  your browser:   http://192.168.1.123:8085/login?fly_port=49290 or enter token manually (input hidden): target saved $ fly -t default workers name          containers  platform  tags  team  state    version  age worker-1      0           linux     none  none  running  2.4      14h14m 
Hello World 世间万物都可以从一个 hello world 学起,Concourse 也不例外。我们可以跟着 Concourse Tutorial [^3] 中Hello World一节的描述,把这个 task 执行起来。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 $ git clone  https://github.com/starkandwayne/concourse-tutorial.git Cloning into 'concourse-tutorial' ... remote: Enumerating objects: 5, done . remote: Counting objects: 100% (5/5), done . remote: Compressing objects: 100% (5/5), done . remote: Total 3794 (delta 0), reused 4 (delta 0), pack-reused 3789 Receiving objects: 100% (3794/3794), 11.18 MiB | 25.00 KiB/s, done . Resolving deltas: 100% (2270/2270), done . $ cd  concourse-tutorial/tutorials/basic/task-hello-world $ fly -t default execute -c task_hello_world.yml uploading task-hello-world done  executing build 1 at http://localhost:8080/builds/1 initializing waiting for  docker to come up... Pulling busybox@sha256:afe605d272837ce1732f390966166c2afff5391208ddd57de10942748694049d... sha256:afe605d272837ce1732f390966166c2afff5391208ddd57de10942748694049d: Pulling from library/busybox 0669b0daf1fb: Pulling fs layer 0669b0daf1fb: Verifying Checksum 0669b0daf1fb: Download complete 0669b0daf1fb: Pull complete Digest: sha256:afe605d272837ce1732f390966166c2afff5391208ddd57de10942748694049d Status: Downloaded newer image for  busybox@sha256:afe605d272837ce1732f390966166c2afff5391208ddd57de10942748694049d Successfully pulled busybox@sha256:afe605d272837ce1732f390966166c2afff5391208ddd57de10942748694049d. running echo  hello world hello world succeeded 
可以看到,Concourse 收到这个 task 之后,下载了一个 Busybox 的 Docker 镜像,然后执行了echo hello world这条命令。那么,Concourse 是怎么知道要如何执行一个 task 呢?这就得从上面运行的task_hello_world.yml说起了。
一个 task 的配置文件 Task 是 Concourse 的流水线 (pipeline) 中最小的配置单元,我们可以把它理解成一个函数,在我们配置好它的行为之后,它将永远按照这个固定的逻辑进行操作。
上面的task_hello_world.yml就是配置了一个 task 所要进行的操作,它的内容不多,我们一块一块拆开来看。
1 2 3 4 5 6 7 8 9 10 --- platform:  linux image_resource:   type:  docker-image    source:  {repository:  busybox } run:   path:  echo    args:  [hello  world ] 
platform属性指定了这个 task 要运行在哪种环境下。需要注意,这里指的是 worker 运行的环境,比如这里指定的linux,就意味着 Concourse 将会挑选一个运行在 Linux 中的 worker。
image_resource属性指定了这个 task 将会运行在一个镜像容器中。其中的type属性说明这个镜像是一个 Docker 镜像,source中{repository: busybox}说明了要使用 Docker 仓库中的busybox作为基础镜像。
run属性就是这个 task 实际要执行的任务,其中的path指定了要运行的命令,这里可以是指向命令的绝对路径、相对路径,如果命令在$PATH中,那么也可以直接写命令的名称;args就是要传递给这个命令的参数。
如果要执行的命令非常复杂,我们也可以把命令写在一个 shell 脚本中,然后在run.path中指向这个脚本,比如这样:
1 2 run:   path:  ./hello-world.sh  
这样一来,就很清楚了。这个 task 会在一台 Linux 宿主机中执行,它将在一个 busybox 镜像中运行echo hello world这条命令。
把多个 task 串起来 虽然我们在上面已经有了一个能用的 task,但是上面说了,task 只是一个 pipeline 的最小组成部分。而且在正式环境中,一个 CI / CD 任务可能会用到多个 task 来完成完整的构建任务。那么,怎么把多个 task 串起来呢?手动去做这件事显然不现实,所以就有了 pipeline。
这里我们还是用 Concourse Tutorial [^3] 中的示例来演示。
首先我们先看一下这个配置文件的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 --- jobs:   -  name:  job-hello-world      public:  true      plan:        -  task:  hello-world          config:            platform:  linux            image_resource:              type:  docker-image              source:  {repository:  busybox }           run:              path:  echo              args:  [hello  world ] 
一个 pipeline 可以有多个 job,这些 job 决定了这个 pipeline 将会以怎样的形式来执行。而一个 job 中最重要的配置,是 plan,即需要执行的步骤。一个 plan 中的作业步,可以用来获取或更新某个资源,也可以用来执行某一个 task。
上面这个 pipeline 只有一个名为job-hello-world的 job,这个 job 里面只有一个作业步,名为hello-world,是一个 task,操作是在一个 busybox 镜像中执行echo hello world命令。
在使用这个 pipeline 之前,我们需要把它注册到 Concourse 中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 $ fly -t default set-pipeline -c pipeline.yml -p hello-world jobs :  job job-hello-world has been added: + name: job-hello-world + plan: + - config: +     container_limits: {} +     image_resource: +       source : +         repository: busybox +       type : docker-image +     platform: linux +     run: +       args: +       - hello world +       path: echo  +   task: hello-world + public: true  apply configuration? [yN]: y pipeline created! you can view your pipeline here: http://localhost:8080/teams/main/pipelines/hello-world the pipeline is currently paused. to unpause, either:   - run the unpause-pipeline command :     fly -t default unpause-pipeline -p hello-world   - click play next to the pipeline in  the web ui 
现在一个新的 pipeline 就被注册到 Concourse 中了。在它的 Web UI 中也能看到这个 pipeline。
但是,这个 pipeline 现在还是暂停状态的,需要把它恢复之后才能使用。那么怎么恢复呢?其实上面set-pipeline操作的输出已经告诉我们了。
the pipeline is currently paused. to unpause, either:fly -t default unpause-pipeline -p hello-world
这个 pipeline 目前是被暂停的,如果要恢复,可以使用下面两种方法之一:fly -t default unpause-pipeline -p hello-world
 
在成功恢复 pipeline 之后,我们可以看到原来蓝色的 paused 字样变成了灰色的 pending 字样,说明现在这个 pipeline 正在等待任务。
接下来我们就可以手动执行一下这个 pipeline,来检查它是否正常。具体操作说起来太啰嗦,我直接借用 Concourse Tutorial 里面的一个动图来替我说明。
自动触发 job 虽然我们在 Web UI 上点一下加号就能触发 job 开始执行,但是 CI / CD 讲究的就是一个自动化,每次更新都手动去点一下,显然谁都受不了这么折腾。所以,Concourse 也提供了几种自动触发 job 执行的方法。
一种方法是向 Concourse API 发送一个POST请求。这种就是 webhook,没什么特殊的,在版本控制系统里面配置好 webhook 的参数就好了。
另一种方法是让 Concourse 监视某一个资源,在资源发生改变之后自动触发 job 执行。下面我详细说说这个功能。
这里我们假设一个场景:我们有一个 Git 仓库,里面有一个名为test.txt的文件。我们想在每次这个仓库收到新 commit 之后,打印出test.txt的内容。
按照这个思路,我在 Concourse 中注册了如下的 pipeline:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 --- resources:   -  name:  resource-git-test      type:  git      source:               uri:  https://gitee.com/boris1993/git-test.git        branch:  master       -  name:  timer      type:  time      source:               interval:  2m  jobs:      -  name:  job-show-file-content      public:  true      plan:               -  get:  resource-git-test                   trigger:  true                      -  get:  timer          trigger:  true               -  task:  show-file-content          config:            platform:  linux            inputs:                                        -  name:  resource-git-test            image_resource:              type:  docker-image                                                     source:  {repository:  busybox , registry_mirror:  https://dockerhub.azk8s.cn }           run:              path:  cat                           args:  ["./resource-git-test/test.txt" ] 
创建git-test仓库、编辑test.txt等等操作不是重点,也没啥难度,这里不啰嗦了。在完成编辑文件,和 push 到远程仓库后,我们等待 Concourse 检查远程仓库更新,并执行构建步骤。
在 pipeline 视图中点击resource-git-test这个资源,就可以看到这个资源的检查历史,展开某条记录后,还可以看到这条历史相关的构建。
在 Concourse 检查到 git 仓库的更新后,就会执行下面指定的构建步骤。结果大概会是这个样子的:
结束语 至此,我们完整的配置了一个简单的 pipeline。后面我会根据文档,或者根据工作中遇到的情况,继续补充权限管理、复杂的 case 等相关的博文。
[^1]: Concourse CI Concourse - GitHub Concourse Tutorial