極大提高項目部署的生產(chǎn)力!分享一個半自動化的CICD實現(xiàn)方案
前言
完全自動化的 CICD 確實好,代碼提交后就自動構(gòu)建自動發(fā)布新版本,實現(xiàn)不停機更新的情況下,還能隨時回滾,這擱誰不喜歡啊~
但理想很豐滿,現(xiàn)實往往很骨感,不是所有開發(fā)/生產(chǎn)環(huán)境都具備部署 CICD 的條件
先說結(jié)論,這些 CICD 服務(wù)都有一些問題,要么就是網(wǎng)絡(luò)不通,要么就是太重太麻煩不具備部署條件(服務(wù)器都在內(nèi)網(wǎng),無法直連)
所以我在工作過程中,「創(chuàng)新」了一種 CICD 的平替方案,通過一個腳本,實現(xiàn)一鍵發(fā)布!
PS: 由于篇幅關(guān)系,無法在文章里貼出全部代碼,有需要的同學(xué)可以在公眾號后臺回復(fù)「半自動 CICD 腳本」獲取
關(guān)于 CICD
現(xiàn)在常見的 CICD 服務(wù)都具備一定門檻,咱們討論一下:
- Github Actions: 最適合開源項目使用,不用部署配置,完全免費 ?? 不過在生產(chǎn)環(huán)境往往因為網(wǎng)絡(luò)問題用不了
- GitLab CI/CD: 很重,需要部署和配置 GitLab 服務(wù)
- Jenkins: 很重,需要部署和配置 Jenkins 服務(wù)
- Azure DevOps 和 AWS CodePipeline: 這倆依賴它家的云服務(wù),而且都是國外的,基本不用考慮的
在這些常用的之外,還有一些其他不入流的,這里也一并看看:
- 國內(nèi)的 Gitee 流水線: 這個類似 Github Actions,不過卻是收費的,打個工而已,難道還得自費上班?直接 pass
- CircleCI: 云原生 CI/CD 服務(wù),提供與 GitHub 和 Bitbucket 的集成,國外使用應(yīng)該很不錯,但國內(nèi)網(wǎng)絡(luò)環(huán)境肯定是不允許的
- Bitbucket Pipelines: 與 Bitbucket 倉庫緊密集成,適合使用 Bitbucket 進行代碼托管的團隊,與 Github 類似的情況,不用考慮了
PS:
有時候不得不感嘆,國內(nèi)國外仿佛兩個世界…
除了這些之外,我還找到一個輕量級的開源 CICD 項目: https://github.com/flowci/flow-core-x
這個看起來不錯,感覺可以用在 HomeLab 或者 NAS 上,到時來嘗試一下。
解決方案
我的解決方案是用「腳本 + docker」實現(xiàn)一鍵發(fā)布、不停機更新、隨時回滾版本~
基本思路,我畫了個簡單的圖,方便理解
這個方案只需要簡單的配置,之后就可以一鍵發(fā)布了,所以我稱之為「半自動 CICD」
原理是本地 git 倉庫打版本 tag(如: v0.0.1),然后運行腳本會自動識別這個版本 tag,構(gòu)建鏡像之后打上同樣的 tag,再推送遠程鏡像倉庫,到了服務(wù)器上再拉下來啟動,完事~(就是這么簡單樸素)
如何使用
使用這個方案的前提是:
- 使用 git 管理代碼
- 使用 docker 部署項目
- 需要有一個私有的 docker 鏡像倉庫,可以自建,也可以使用阿里云這類私有鏡像服務(wù)(免費)
- 服務(wù)器能訪問到 docker 鏡像倉庫(內(nèi)網(wǎng)的話可以自建)
修改 compose 配置
在使用腳本之前,需要一點小小的配置,后面就可以解放生產(chǎn)力了~
還是以基于 DjangoStarter 框架 的項目為例
compose.yaml 配置文件
services:
# 省略無關(guān)內(nèi)容
app:
image: ${APP_IMAGE_NAME}:${APP_IMAGE_TAG}
container_name: $APP_NAME-app
.env 環(huán)境變量
APP_PORT=9876
APP_NAME=meta-hub
APP_IMAGE_NAME=meta-hub
APP_IMAGE_TAG=v0.0.2
到時腳本運行時會自動修改 .env 里的版本 APP_IMAGE_TAG
打 tag
在本地開發(fā)完成之后
使用 git tag 功能給 commit 打版本 tag
例如:
git tag v0.1.1
運行腳本
這次的腳本我是用 Python 編寫的,不過沒有其他外部依賴,完全使用標準庫實現(xiàn),還算比較方便的
python scripts/build_docker.py
PS: 后續(xù)我會考慮使用 C# 或者 Go 重新寫這個腳本,支持 AOT,作為一個工具添加到系統(tǒng) PATH,使用起來更方便
腳本
接下來放一個簡化版本的腳本
由于篇幅關(guān)系,無法在文章里貼出全部代碼,有需要的同學(xué)可以在公眾號后臺回復(fù)「半自動 CICD 腳本」獲取
這個腳本,總共一百多行,麻雀雖小五臟俱全,實現(xiàn)了完整的功能。
一開始我是用的 paramiko.SSHClient 來建立 SSH 連接的,不過后面覺得還是不要引入額外的復(fù)雜度比較好,最終簡化成這樣,直接使用系統(tǒng)自帶的 SSH 命令。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Docker鏡像構(gòu)建、推送和遠程部署腳本
功能:
1. 獲取最新git tag作為版本號
2. 構(gòu)建Docker鏡像并推送到配置的鏡像倉庫
3. SSH連接到遠程服務(wù)器進行自動部署
配置項(環(huán)境變量或默認值):
- REGISTRY_URL: 鏡像倉庫地址,如: registry.example.com
- REGISTRY_NAMESPACE: 鏡像倉庫命名空間
- IMAGE_NAME: 鏡像名稱
- REMOTE_HOST: 遠程服務(wù)器配置,如: 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', # 遠程服務(wù)器地址或~/.ssh/config中的Host別名
'REMOTE_PROJECT_PATH': '/path/to/project',
}
def get_config(key: str) -> str:
"""獲取配置值,優(yōu)先使用環(huán)境變量,否則使用默認值"""
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]:
"""
執(zhí)行命令并實時顯示輸出,同時捕獲輸出內(nèi)容。
返回狀態(tài)碼、stdout和stderr。
"""
if show_output:
print(f"執(zhí)行: {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錯誤: 命令執(zhí)行失敗 (返回碼: {returncode})")
# 錯誤輸出已經(jīng)被實時打印,這里不再重復(fù)打印
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:
"""部署到遠程服務(wù)器"""
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鏡像構(gòu)建、推送和部署流程\n")
# 1. 獲取最新tag
version = get_latest_tag()
# 2. 構(gòu)建鏡像
print("\n?? 構(gòu)建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?? 所有任務(wù)已完成!")
if __name__ == "__main__":
main()
小結(jié)
真的是解放生產(chǎn)力啊,這個方案極大降低了部署的工作量
這個方法值得推廣,我決定把這個腳本內(nèi)置在「DjangoStarter 框架」中~

浙公網(wǎng)安備 33010602011771號