本篇是系列中的第五篇内容,我们继续聊聊如何把一个简化过的私有云环境部署在笔记本里,以满足低成本、低功耗、低延时的实验环境。如果你有闲置的轻量云服务器,也可以动手试试。

写在前面

作为“持续集成”章节的第一篇内容,我们先来聊聊在单机服务器上的 CI 的使用。

关于基础的搭建,之前的文章中已经多次提到,所以我就不再赘述,本文将着重介绍过程中的一些细节,如果你对 Gitea 和 Drone 或者 GitLab 感兴趣,可以阅读之前的内容:

为了更低的维护成本,以及后续多机扩展使用,本文所有程序的使用均在容器环境下。

单机 CI 设计

在展开实践细节之前,我们得先来聊聊“设计”。

架构设计

CI 过程中的参与者主要有下面这几类(本篇暂不聊软件仓库部分):用户、Git服务、CI 服务、CI 执行器。

简单针对上面的参与者进行定义:“用户”可以是有血有肉的人,也可以是自动化的脚本或者 BOT,各种数据的创造者;“Git 服务”,用于存储代码数据,提供基础的权限功能和界面管理的程序;“CI 服务”,提供持续集成的任务的调度和管理的程序;“CI 执行器”,用于执行具体的 CI 任务的程序。

考虑到单机服务器上除了 Git 服务和 CI 服务之外,还会运行我们需要更新和部署的程序,为了让资源使用效率更好、维护成本更低、避免我们为每一个 Web 程序配置 HTTPS 证书,我们可以添加一个支持服务发现的应用网关。

即使是单机服务器,我们依旧需要注意 SSH 的使用安全,在多机环境下,我们会使用跳板机和云服务器安全策略来进行集中的安全管理,在单机场景下,我使用 SSH 服务开关来完成简单的安全防护(不用的时候,直接关闭,也为互联网上的嗅探机器人省点电)。

如果将上面的“参与者”用图例来表示,一个最基础的单机 CI 使用模式会类似下面这样:

CI 的基础使用模式

我将图中不同角色的数据交互进行的数字序号标注,简单解释一下这些序号代表的具体内容:

  • “1” 表示了用户使用具体的域名来访问我们的 Git 服务和 CI 服务,来进行仓库管理或者配置 CI 任务。这类交互使用的是 HTTP 的方式,比如在浏览器中访问 https://gitea.lab.comhttps://gitlab.lab.comhttps://drone.lab.com
  • “2” 表示了用户或者客户端使用 SSH 的方式访问 Git 仓库,需要搭配 RSA Key 使用。
  • “3” 和 “4” 表示了 Traefik 使用服务发现的方式,聚合 Git 服务和 CI 服务,为用户提供域名形式的访问方式,这里使用的代理模式同样也是 HTTP。
  • “5” 表示了 SSH 开关和 Git SSH 服务之间的数据交互,交互形式为 TCP。
  • “6” 和 “7” 表示了 CI 服务 分别和Git 服务、CI 执行器之间的数据交互,从 Git 获取仓库变动,然后创建 CI 任务,接着将 CI 任务执行状态不断推送至 Git 服务中,交互形式不限,可以使用 HTTP API,也可以使用各种基于 TCP 的 RPC 的方式。
  • “8” 则表示了 CI 执行器如何从 Git 服务器的代码仓库中获取代码,或者将一些数据更新回 Git 服务器中,一般情况下是使用 HTTP 的方式,我更推荐使用 Git Over SSH 进行交互。

部署模式

在单机全容器模式下,我们一般会用两种方式可以完成部署。

一类是基于文件挂载的方式,比如在 CI 过程中将 CI JOB 容器中的文件系统和宿主机打通,然后将构建产物同步到宿主机中、类似的变体还有使用各种网络文件协议进行文件系统挂载;另外一类,则是使用 SSH 或者 SCP 、Rsync 等方式,在容器中访问宿主机完成数据交换或者服务初始化或启停操作。

单机模式下基于Docker 的 CI 部署模式

除此之外,如果我们借助软件仓库、容器仓库,还能够完成纯容器交付,让交互更纯粹和“干净”。这个话题,我们会在后续文章中展开。

单机 CI 配置实践

接下来,我们以上文中的 “SSH 开关”这个应用,在 Gitea 和 Drone 环境中进行持续集成和部署实践为例,来聊聊如何在单机模式下使用 CI。

配置好 CI 的示例仓库

因为这个项目类型是一个不支持热加载的、需要持续运行的网络程序,程序的更新需要重启服务。所以我们恰好可以使用“部署模式”中的挂载文件的方式更新文件,以及使用 SSH 的方式来进行服务的停止和重新启动。(如果是静态资源类的项目部署,则只需要完成资源替换更新即可)

定义 CI 配置文件

首先将需要集成 CI 的项目放置上传到 Gitea 中的某个仓库中,这里以上文中提到的 Git SSH 开关为例。在项目中创建一个名为 .drone.yml 的 CI 配置文件。

一个相对通用的 CI 配置可以用下面的形式来表达:

---
kind: pipeline
name: default

steps:

- name: clone

- name: stop-previous-services
  depends_on: [ clone ]

- name: update-services
  depends_on: [ stop-previous-services ]

- name: start-new-services
  depends_on: [ update-services ]

上面的配置包含了:下载仓库代码、停止原先的服务、更新服务程序代码、重新启动服务四个过程。在实际生产中,根据业务类型,我们的执行顺序可能会有变化,甚至不再是上面的“串行”方式执行。

Gitea 仓库配置好 CI 之后

按照上面的配置将 CI 配置好之后,当我们推送代码到代码仓库触发 CI 任务后。在图形界面中,我们将看到类似上图的结果。

使用 SSH 协议下载代码

不论是使用哪一种 CI 工具,我都推荐你使用 Git Over SSH 的方式来获取代码,而非使用 Git Token 或者账号密码的方式来进行交互。这样可以让你的程序对于某一种 CI 或者 Git 仓库的依赖更低,更容易在合适的时间点、以低成本切换到更合适的工具。

在 Drone CI 中,如果想使用 SSH 方式来下载代码,可以使用下面的配置:(在 GitLab Runner 中同理)

---
kind: pipeline
name: default

clone:
  disable: true

steps:

- name: clone
  image: alpine/git
  pull: if-not-exists
  environment:
    KEY:
      from_secret: ssh_key
  commands:
    - GIT_HOST=$(echo $DRONE_GIT_SSH_URL | sed 's/git@/\1/' | sed 's/:.*/\1/') && mkdir "$HOME/.ssh" && echo "$KEY" > "$HOME/.ssh/id_rsa" && chmod 600 $HOME/.ssh/id_rsa && eval `ssh-agent -s` && ssh-add $HOME/.ssh/id_rsa && ssh-keyscan $GIT_HOST > ~/.ssh/known_hosts && chmod 400 "$HOME/.ssh/known_hosts";
    - git clone $DRONE_GIT_SSH_URL .
    - git -c advice.detachedHead=false checkout $DRONE_COMMIT

上面的代码中,为了使用 SSH 方式下载程序代码,CI 程序会做两件事:

  1. 从 CI 软件中读取我们预先配置好的 ssh_key 环境变量,然后将变量输出成程序可以直接使用的 rsa_key ,并设置好权限,使用 ssh-agent 加载程序。
  2. 将仓库使用默认的 HTTP 协议替换为 Git 协议,以备程序使用。

当然,想要使用 SSH 方式下载代码,我们需要在 Git 软件的账号或者仓库中配置 SSH Key

使用 SSH 方式操作服务启停

这个应用中,我们在 docker-compose.yml 定义了容器的启动方式,所以服务的启动和关闭可以使用我们熟悉的命令 docker-compose up -ddocker-compose down 来完成。

因为 CI 在容器中执行,我们不能直接操作宿主机,所以需要借助 SSH 或者 dind 模式的 docker.sock 来完成服务状态的改变。

本文先聊聊如何使用 SSH 来解决基础的部署操作:

- name: stop-or-start-services
  image: deploy-tool
  depends_on: [ clone ]
  pull: if-not-exists
  environment:
    KEY:
      from_secret: ssh_key
    # 环境变量,除了私密的定义在 CI 软件的环境变量中,也可显式声明在 CI 配置中
    TARGET_HOST: user@host
    TARGET_PORT: 22
  commands:
    - mkdir "$HOME/.ssh" && echo "$KEY" > "$HOME/.ssh/id_rsa" && chmod 600 $HOME/.ssh/id_rsa && eval `ssh-agent -s` && ssh-add $HOME/.ssh/id_rsa";
    # 关闭服务
    - ssh -i "$HOME/.ssh/id_rsa" -p $TARGET_PORT -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $TARGET_HOST "bash -c \"cd /app-path/ && docker-compose down\""
    # 启动服务
    - ssh -i "$HOME/.ssh/id_rsa" -p $TARGET_PORT -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $TARGET_HOST "bash -c \"cd /app-path/ && docker-compose up -d\""

和下载代码类似,我们从环境变量中初始化 rsa key,然后在 ssh-agent 中加载私钥。然后使用 ssh 客户端连接宿主机,切换工作目录,执行命令操作服务的启动和关闭即可。

同样的,想要使用 SSH 操作服务器,我们需要在服务器对应用户的 ~/.ssh/authorized_keys 中配置对应的公钥。

使用文件挂载的方式更新代码

更新代码有两种方式,一种是使用上文中提到的 SSH 的方式,远程执行 scprsync 等命令同步数据,另外一种则是使用文件挂载的方式。因为我们的部署在同一台机器上,所以文件挂载不失为一个高效的方式。

以 Drone CI 配置为例,演示如何挂载宿主机目录到容器内:

- name: update-services
  image: deploy-tool
  depends_on: [ stop-previous-services ]
  pull: if-not-exists
  commands:
    - rm -rf /deploy/*
    - cp -r /drone/src/* /deploy/
    - cp -r /drone/src/.env /deploy/
  volumes:
    - name: host-dir
      path: /deploy

volumes:
  - name: host-dir
    host:
      path: /app-path

最后

在接下来的“持续集成”相关文章中,我将展开聊聊 CI 在多机和相对复杂场景下的使用,以及其他场景类型的部署实战细节。

–EOF