本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或重新修改使用,但需要注明来源。 [署名 4.0 国际 (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/deed.zh) 本文作者: 苏洋 创建时间: 2022年01月02日 统计字数: 5441字 阅读时间: 11分钟阅读 本文链接: https://soulteary.com/2022/01/02/private-cloud-environment-installed-in-a-notebook-cicd-part-1.html ----- # 装在笔记本里的私有云环境:持续集成(上) 本篇是系列中的第五篇内容,我们继续聊聊如何把一个简化过的私有云环境部署在笔记本里,以满足低成本、低功耗、低延时的实验环境。如果你有闲置的轻量云服务器,也可以动手试试。 ## 写在前面 作为“持续集成”章节的第一篇内容,我们先来聊聊在单机服务器上的 CI 的使用。 关于基础的搭建,之前的文章中已经多次提到,所以我就不再赘述,本文将着重介绍过程中的一些细节,如果你对 Gitea 和 Drone 或者 GitLab 感兴趣,可以阅读之前的内容: - [《容器方式下的轻量仓库与CI 使用方案:Gitea + Drone 基础篇》](https://soulteary.com/2021/02/25/lightweight-code-warehouse-and-ci-usage-plan-in-docker-with-gitea-and-drone-part-1.html) - [《使用容器方式编译无功能限制的 Drone CI》](https://soulteary.com/2021/04/17/use-docker-to-build-drone-ci-without-functional-limitations.html) - [《轻量安全的部署方案》](https://soulteary.com/2020/07/31/lightweight-and-safe-deployment-solution.html) - [《使用 Docker 和 Traefik v2 搭建轻量代码仓库(Gitea)》](https://soulteary.com/2020/02/04/gitea-git-server-with-docker-and-traefik-v2.html) - 一些 [GitLab 相关的内容](https://soulteary.com/tags/gitlab.html) 为了更低的维护成本,以及后续多机扩展使用,本文所有程序的使用均在容器环境下。 ## 单机 CI 设计 在展开实践细节之前,我们得先来聊聊“设计”。 ### 架构设计 CI 过程中的参与者主要有下面这几类(本篇暂不聊软件仓库部分):用户、Git服务、CI 服务、CI 执行器。 简单针对上面的参与者进行定义:“用户”可以是有血有肉的人,也可以是自动化的脚本或者 BOT,各种数据的创造者;“Git 服务”,用于存储代码数据,提供基础的权限功能和界面管理的程序;“CI 服务”,提供持续集成的任务的调度和管理的程序;“CI 执行器”,用于执行具体的 CI 任务的程序。 考虑到单机服务器上除了 Git 服务和 CI 服务之外,还会运行我们需要更新和部署的程序,为了让资源使用效率更好、维护成本更低、避免我们为每一个 Web 程序配置 HTTPS 证书,我们可以添加一个支持服务发现的应用网关。 即使是单机服务器,我们依旧需要注意 SSH 的使用安全,在多机环境下,我们会使用跳板机和云服务器安全策略来进行集中的安全管理,在单机场景下,我使用 SSH 服务开关来完成简单的安全防护(不用的时候,直接关闭,也为互联网上的嗅探机器人省点电)。 如果将上面的“参与者”用图例来表示,一个最基础的单机 CI 使用模式会类似下面这样: ![CI 的基础使用模式](https://attachment.soulteary.com/2022/01/02/basic-usage-ci.jpg) 我将图中不同角色的数据交互进行的数字序号标注,简单解释一下这些序号代表的具体内容: - “1” 表示了用户使用具体的域名来访问我们的 Git 服务和 CI 服务,来进行仓库管理或者配置 CI 任务。这类交互使用的是 HTTP 的方式,比如在浏览器中访问 `https://gitea.lab.com`、`https://gitlab.lab.com`、`https://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 部署模式](https://attachment.soulteary.com/2022/01/02/basic-usage-cd.jpg) 除此之外,如果我们借助软件仓库、容器仓库,还能够完成纯容器交付,让交互更纯粹和“干净”。这个话题,我们会在后续文章中展开。 ## 单机 CI 配置实践 接下来,我们以上文中的 “SSH 开关”这个应用,在 Gitea 和 Drone 环境中进行持续集成和部署实践为例,来聊聊如何在单机模式下使用 CI。 ![配置好 CI 的示例仓库](https://attachment.soulteary.com/2022/01/02/gitea-with-ci.jpg) 因为这个项目类型是一个不支持热加载的、需要持续运行的网络程序,程序的更新需要重启服务。所以我们恰好可以使用“部署模式”中的挂载文件的方式更新文件,以及使用 `SSH` 的方式来进行服务的停止和重新启动。(如果是静态资源类的项目部署,则只需要完成资源替换更新即可) ### 定义 CI 配置文件 首先将需要集成 CI 的项目放置上传到 Gitea 中的某个仓库中,这里以上文中提到的 Git SSH 开关为例。在项目中创建一个名为 `.drone.yml` 的 CI 配置文件。 一个相对通用的 CI 配置可以用下面的形式来表达: ```yaml --- 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 之后](https://attachment.soulteary.com/2022/01/02/drone-ci-status.jpg) 按照上面的配置将 CI 配置好之后,当我们推送代码到代码仓库触发 CI 任务后。在图形界面中,我们将看到类似上图的结果。 ### 使用 SSH 协议下载代码 不论是使用哪一种 CI 工具,我都推荐你使用 Git Over SSH 的方式来获取代码,而非使用 Git Token 或者账号密码的方式来进行交互。这样可以让你的程序对于某一种 CI 或者 Git 仓库的依赖更低,更容易在合适的时间点、以低成本切换到更合适的工具。 在 Drone CI 中,如果想使用 SSH 方式来下载代码,可以使用下面的配置:(在 GitLab Runner 中同理) ```yaml --- 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 -d` 和 `docker-compose down` 来完成。 因为 CI 在容器中执行,我们不能直接操作宿主机,所以需要借助 SSH 或者 dind 模式的 `docker.sock` 来完成服务状态的改变。 本文先聊聊如何使用 SSH 来解决基础的部署操作: ```yaml - 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 的方式,远程执行 `scp`、`rsync` 等命令同步数据,另外一种则是使用文件挂载的方式。因为我们的部署在同一台机器上,所以文件挂载不失为一个高效的方式。 以 Drone CI 配置为例,演示如何挂载宿主机目录到容器内: ```yaml - 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