极大提高项目部署的生产力!分享一个半自动化的CICD实现方案

前言

完全自动化的 CICD 确实好,代码提交后就自动构建自动发布新版本,实现不停机更新的情况下,还能随时回滚,这搁谁不喜欢啊~

但理想很丰满,现实往往很骨感,不是所有开发/生产环境都具备部署 CICD 的条件

先说结论,这些 CICD 服务都有一些问题,要么就是网络不通,要么就是太重太麻烦不具备部署条件(服务器都在内网,无法直连)

所以我在工作过程中,「创新」了一种 CICD 的平替方案,通过一个脚本,实现一键发布!

PS: 由于篇幅关系,无法在文章里贴出全部代码,有需要的同学可以在公众号后台回复「半自动 CICD 脚本」获取

关于 CICD

现在常见的 CICD 服务都具备一定门槛,咱们讨论一下:

  • Github Actions: 最适合开源项目使用,不用部署配置,完全免费 👍 不过在生产环境往往因为网络问题用不了
  • GitLab CI/CD: 很重,需要部署和配置 GitLab 服务
  • Jenkins: 很重,需要部署和配置 Jenkins 服务
  • Azure DevOps 和 AWS CodePipeline: 这俩依赖它家的云服务,而且都是国外的,基本不用考虑的

在这些常用的之外,还有一些其他不入流的,这里也一并看看:

  • 国内的 Gitee 流水线: 这个类似 Github Actions,不过却是收费的,打个工而已,难道还得自费上班?直接 pass
  • CircleCI: 云原生 CI/CD 服务,提供与 GitHub 和 Bitbucket 的集成,国外使用应该很不错,但国内网络环境肯定是不允许的
  • Bitbucket Pipelines: 与 Bitbucket 仓库紧密集成,适合使用 Bitbucket 进行代码托管的团队,与 Github 类似的情况,不用考虑了

PS: 有时候不得不感叹,国内国外仿佛两个世界…

除了这些之外,我还找到一个轻量级的开源 CICD 项目: https://github.com/flowci/flow-core-x

这个看起来不错,感觉可以用在 HomeLab 或者 NAS 上,到时来尝试一下。

解决方案

我的解决方案是用「脚本 + docker」实现一键发布、不停机更新、随时回滚版本~

基本思路,我画了个简单的图,方便理解

graph LR A([本地])-->B1(打 TAG) A-->B2(docker build) B2-- 推送镜像 -->C[(镜像仓库)] A-- SSH连接 -->D{服务器} C-- 拉取镜像 -->D D-- 启动 -->E([线上服务])

这个方案只需要简单的配置,之后就可以一键发布了,所以我称之为「半自动 CICD」

原理是本地 git 仓库打版本 tag(如: v0.0.1),然后运行脚本会自动识别这个版本 tag,构建镜像之后打上同样的 tag,再推送远程镜像仓库,到了服务器上再拉下来启动,完事~(就是这么简单朴素)

如何使用

使用这个方案的前提是:

  • 使用 git 管理代码
  • 使用 docker 部署项目
  • 需要有一个私有的 docker 镜像仓库,可以自建,也可以使用阿里云这类私有镜像服务(免费)
  • 服务器能访问到 docker 镜像仓库(内网的话可以自建)

修改 compose 配置

在使用脚本之前,需要一点小小的配置,后面就可以解放生产力了~

还是以基于 DjangoStarter 框架 的项目为例

compose.yaml 配置文件

services: 	# 省略无关内容   app:     image: ${APP_IMAGE_NAME}:${APP_IMAGE_TAG}     container_name: $APP_NAME-app 

.env 环境变量

APP_PORT=9876 APP_NAME=meta-hub APP_IMAGE_NAME=meta-hub APP_IMAGE_TAG=v0.0.2 

到时脚本运行时会自动修改 .env 里的版本 APP_IMAGE_TAG

打 tag

在本地开发完成之后

使用 git tag 功能给 commit 打版本 tag

例如:

git tag v0.1.1 

运行脚本

这次的脚本我是用 Python 编写的,不过没有其他外部依赖,完全使用标准库实现,还算比较方便的

python scripts/build_docker.py 

PS: 后续我会考虑使用 C# 或者 Go 重新写这个脚本,支持 AOT,作为一个工具添加到系统 PATH,使用起来更方便

脚本

接下来放一个简化版本的脚本

由于篇幅关系,无法在文章里贴出全部代码,有需要的同学可以在公众号后台回复「半自动 CICD 脚本」获取

这个脚本,总共一百多行,麻雀虽小五脏俱全,实现了完整的功能。

一开始我是用的 paramiko.SSHClient 来建立 SSH 连接的,不过后面觉得还是不要引入额外的复杂度比较好,最终简化成这样,直接使用系统自带的 SSH 命令。

#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Docker镜像构建、推送和远程部署脚本  功能: 1. 获取最新git tag作为版本号 2. 构建Docker镜像并推送到配置的镜像仓库 3. SSH连接到远程服务器进行自动部署  配置项(环境变量或默认值): - REGISTRY_URL: 镜像仓库地址,如: registry.example.com - REGISTRY_NAMESPACE: 镜像仓库命名空间 - IMAGE_NAME: 镜像名称 - REMOTE_HOST: 远程服务器配置,如: user@server-ip -p 2022 - REMOTE_PROJECT_PATH: 远程项目路径 """  import os import sys import subprocess import threading from typing import Optional, Tuple  # 默认配置 DEFAULTS = {     'REGISTRY_URL': 'registry.example.com',     'REGISTRY_NAMESPACE': 'namespace',     'IMAGE_NAME': 'image-name',     'REMOTE_HOST': 'host-name',  # 远程服务器地址或~/.ssh/config中的Host别名     'REMOTE_PROJECT_PATH': '/path/to/project', }   def get_config(key: str) -> str:     """获取配置值,优先使用环境变量,否则使用默认值"""     return os.environ.get(key, DEFAULTS.get(key, ''))   def _reader_thread(pipe, lines_list, stream_to_print_to):     """在独立线程中读取管道输出"""     try:         for line in iter(pipe.readline, ''):             lines_list.append(line)             if stream_to_print_to:                 # 实时打印                 stream_to_print_to.write(line)                 stream_to_print_to.flush()     finally:         pipe.close()  def run_cmd(cmd: str, show_output: bool = True) -> Tuple[int, str, str]:     """     执行命令并实时显示输出,同时捕获输出内容。     返回状态码、stdout和stderr。     """     if show_output:         print(f"执行: {cmd}")      process = subprocess.Popen(         cmd,         shell=True,         stdout=subprocess.PIPE,         stderr=subprocess.PIPE,         text=True,         bufsize=1,         universal_newlines=True     )      stdout_lines = []     stderr_lines = []      stdout_thread = threading.Thread(         target=_reader_thread,         args=(process.stdout, stdout_lines, sys.stdout if show_output else None)     )     stderr_thread = threading.Thread(         target=_reader_thread,         args=(process.stderr, stderr_lines, sys.stderr if show_output else None)     )      stdout_thread.start()     stderr_thread.start()      stdout_thread.join()     stderr_thread.join()      returncode = process.wait()      stdout = ''.join(stdout_lines)     stderr = ''.join(stderr_lines)      if returncode != 0:         print(f"n错误: 命令执行失败 (返回码: {returncode})")         # 错误输出已经被实时打印,这里不再重复打印         sys.exit(1)      return returncode, stdout, stderr   def get_latest_tag() -> str:     """获取最新git tag"""     _, tag, _ = run_cmd("git describe --tags --abbrev=0")     tag = tag.strip()     if not tag:         print("错误: 没有找到git tag")         sys.exit(1)     print(f"最新tag: {tag}")     return tag   def deploy_to_remote(version: str) -> None:     """部署到远程服务器"""     host = get_config('REMOTE_HOST')     remote_path = get_config('REMOTE_PROJECT_PATH')      print(f"n🔗 通过SSH连接到 {host} 进行部署...")      # 1. 更新远程 .env 文件     print(f"n🔄 更新远程.env文件...")     update_cmd = f'ssh {host} "sed -i 's/^TAG=.*/TAG={version}/' {remote_path}/.env"'     run_cmd(update_cmd)      # 2. 重启远程容器     print(f"n🔄 重启远程容器...")     restart_cmd = f'ssh {host} "cd {remote_path} && docker compose up -d"'     run_cmd(restart_cmd)      print("n✅ 远程部署完成!")   def main():     print("🚀 开始Docker镜像构建、推送和部署流程n")      # 1. 获取最新tag     version = get_latest_tag()      # 2. 构建镜像     print("n📦 构建Docker镜像...")     run_cmd("docker compose build app")      # 3. 打tag     registry = get_config('REGISTRY_URL')     namespace = get_config('REGISTRY_NAMESPACE')     image_name = get_config('IMAGE_NAME')     registry_image = f"{registry}/{namespace}/{image_name}:{version}"      print(f"n🏷️ 给镜像打tag...")     run_cmd(f"docker tag {image_name} {registry_image}")      # 4. 推送镜像     print(f"n📤 推送镜像到仓库...")     run_cmd(f"docker push {registry_image}")     print(f"镜像已推送: {registry_image}")      # 5. 远程部署     deploy_to_remote(version)      print("n🎉 所有任务已完成!")   if __name__ == "__main__":     main() 

小结

真的是解放生产力啊,这个方案极大降低了部署的工作量

这个方法值得推广,我决定把这个脚本内置在「DjangoStarter 框架」中~

发表评论

评论已关闭。

相关文章