Greatly improve project deployment productivity! Share a semi-automated CICD implementation solution!
Semi-automatic CI/CD? Sounds like a compromise compared to a fully automated solution. Using scripts for one-click deployment can indeed save time, but you'll have to tolerate issues like accessing the server's intranet. In the domestic environment, CI/CD always requires finding your own solutions.
目录10 个章节
Preface
A fully automated CI/CD is truly great. After code submission, it automatically builds and releases new versions, enabling rolling updates without downtime and the ability to roll back at any time—who wouldn't like that?
However, ideals are often much more optimistic than reality. Not all development or production environments are equipped to deploy CI/CD.
To get straight to the point, these CI/CD services have some issues: either the network is inaccessible, or they are too heavy and cumbersome to deploy under the given conditions (servers are all on the internal network and cannot connect directly).
That's why during my work, I "innovated" a practical alternative to CI/CD—a semi-automated approach that achieves one-click deployment with a single script!
PS: Due to space constraints, the full code cannot be included in this article. If needed, you can reply "半自动 CICD 脚本" (Semi-Automated CI/CD Script) in the backend of the official account to get it.
About CI/CD
Nowadays, common CI/CD services all have certain barriers. Let's discuss:
- Github Actions: Best suited for open-source projects, no deployment configuration required, completely free 👍 However, it often cannot be used in production environments due to network issues.
- GitLab CI/CD: Quite heavy, requires deploying and configuring GitLab services.
- Jenkins: Quite heavy, requires deploying and configuring Jenkins services.
- Azure DevOps and AWS CodePipeline: These two depend on their respective cloud services, and both are foreign, so basically, they are not worth considering.
Beyond these common ones, there are also some other less mainstream options, which we will also take a look at:
- Domestic Gitee Pipeline: This is similar to Github Actions but is paid. We're just working a job—should we have to pay out of our own pockets to go to work? Pass on this directly.
- CircleCI: A cloud-native CI/CD service offering integration with GitHub and Bitbucket. It should work great abroad, but the domestic network environment certainly wouldn't allow it.
- Bitbucket Pipelines: Closely integrated with Bitbucket repositories, suitable for teams using Bitbucket for code hosting. Similar to the Github situation, no need to consider it.
PS:
Sometimes one can't help but sigh, as if the domestic and foreign environments are two different worlds...
Besides these, I also found a lightweight open-source CI/CD project: https://github.com/flowci/flow-core-x
This one looks promising. I feel it could be used on a HomeLab or NAS. I'll give it a try sometime.
Solution
My solution uses "scripts + Docker" to achieve one-click deployment, zero-downtime updates, and version rollback at any time~
The basic idea is illustrated in the simple diagram below for easier understanding:
graph LR
A([Local])-->B1(Create TAG)
A-->B2(docker build)
B2-- Push image -->C[(Image Registry)]
A-- SSH connection -->D{Server}
C-- Pull image -->D
D-- Start -->E([Online Service])
This solution only requires simple configuration, after which you can deploy with one click. That's why I call it "semi-automatic CICD."
The principle is to tag the local git repository with a version (e.g., v0.0.1). Then, the script automatically identifies this version tag, builds the image with the same tag, and pushes it to a remote image registry. On the server, the image is pulled and started—that's it~ (Simple and straightforward!)
How to Use
The prerequisites for using this approach are:
- Code management using git
- Project deployment using docker
- A private docker image registry is required. This can be self-hosted or use private image services like Alibaba Cloud (阿里云) which are free
- The server must have access to the docker image registry (can be self-hosted in an internal network environment)
Modifying the compose configuration
Before using the script, a small configuration is needed, which will then boost your productivity~
Using a project based on the DjangoStarter framework as an example.
The compose.yaml configuration file
services:
# Omit unrelated content
app:
image: ${APP_IMAGE_NAME}:${APP_IMAGE_TAG}
container_name: $APP_NAME-app
.env environment variables
APP_PORT=9876
APP_NAME=meta-hub
APP_IMAGE_NAME=meta-hub
APP_IMAGE_TAG=v0.0.2
The script will automatically modify the version APP_IMAGE_TAG in the .env file when it runs.
Creating tags
After completing local development
Use the git tag feature to add a version tag to a commit.
For example:
git tag v0.1.1
Running the Script
I wrote this script in Python, but it has no external dependencies—it's implemented entirely using the standard library, which is quite convenient.
python scripts/build_docker.py
PS: In the future, I plan to rewrite this script in C# or Go to support AOT (Ahead-of-Time Compilation) and add it as a tool to the system PATH for easier use.
Script
Next, I'll provide a simplified version of the script.
Due to space limitations, I can't include the complete code in this article. If needed, you can reply with "半自动 CICD 脚本" (Semi-automatic CICD Script) on our WeChat official account to get it.
This script, totaling just over a hundred lines, is small but fully functional, implementing complete features.
Initially, I used paramiko.SSHClient to establish SSH connections. However, I later decided it was better not to introduce additional complexity and ultimately simplified it like this, directly using the system's built-in SSH command.
#!/usr/bin/env python3
-- coding: utf-8 --
""" Docker Image Build, Push, and Remote Deployment Script
Features:
- Retrieve the latest git tag as the version number
- Build Docker image and push it to the configured image repository
- SSH connect to the remote server for automatic deployment
Configuration (environment variables or default values):
- REGISTRY_URL: Image repository (镜像仓库) address, e.g.: registry.example.com
- REGISTRY_NAMESPACE: Image repository namespace
- IMAGE_NAME: Image name
- REMOTE_HOST: Remote server configuration, e.g.: user@server-ip -p 2022
- REMOTE_PROJECT_PATH: Remote project path """
import os import sys import subprocess import threading from typing import Optional, Tuple
Default Configuration
DEFAULTS = { 'REGISTRY_URL': 'registry.example.com', 'REGISTRY_NAMESPACE': 'namespace', 'IMAGE_NAME': 'image-name', 'REMOTE_HOST': 'host-name', # Remote server address or Host alias in ~/.ssh/config 'REMOTE_PROJECT_PATH': '/path/to/project', }
def get_config(key: str) -> str: """Get configuration value, prioritizing environment variables, otherwise using default values.""" return os.environ.get(key, DEFAULTS.get(key, ''))
def _reader_thread(pipe, lines_list, stream_to_print_to): """Read pipe output in a separate thread.""" try: for line in iter(pipe.readline, ''): lines_list.append(line) if stream_to_print_to: # Real-time printing 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]: """ Execute command and display output in real-time, while capturing the output. Returns exit code, stdout, and stderr. """ if show_output: print(f"Executing: {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"\nError: Command execution failed (Return code: {returncode})")
# Error output has already been printed in real-time, no need to repeat here
sys.exit(1)
return returncode, stdout, stderr
def get_latest_tag() -> str: """Get the latest git tag""" _, tag, _ = run_cmd("git describe --tags --abbrev=0") tag = tag.strip() if not tag: print("Error: No git tags found") sys.exit(1) print(f"Latest tag: {tag}") return tag
def deploy_to_remote(version: str) -> None: """Deploy to a remote server.""" host = get_config('REMOTE_HOST') remote_path = get_config('REMOTE_PROJECT_PATH')
print(f"\n🔗 Connecting to {host} via SSH for deployment...")
# 1. Update remote .env file
print(f"\n🔄 Updating remote .env file...")
update_cmd = f'ssh {host} "sed -i \'s/^TAG=.*/TAG={version}/\' {remote_path}/.env"'
run_cmd(update_cmd)
# 2. Restart remote containers
print(f"\n🔄 Restarting remote containers...")
restart_cmd = f'ssh {host} "cd {remote_path} && docker compose up -d"'
run_cmd(restart_cmd)
print("\n✅ Remote deployment complete!")
def main(): print("🚀 Starting Docker image build, push, and deployment process\n")
# 1. Get the latest tag
version = get_latest_tag()
# 2. Build the image
print("\n📦 Building Docker image...")
run_cmd("docker compose build app")
# 3. Tag the image
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🏷️ Tagging the image...")
run_cmd(f"docker tag {image_name} {registry_image}")
# 4. Push the image
print(f"\n📤 Pushing image to registry...")
run_cmd(f"docker push {registry_image}")
print(f"Image pushed: {registry_image}")
# 5. Remote deployment
deploy_to_remote(version)
print("\n🎉 All tasks completed!")
if name == "main": main()
## Summary
This really frees up productivity. This solution greatly reduces the workload of deployment.
This method is worth promoting. I've decided to build this script into the [DjangoStarter framework](https://github.com/Deali-Axy/DjangoStarter)~
_EOF_
Greatly improve project deployment productivity! Share a semi-automated CICD implementation solution!
- 本文作者:
- 程序设计实验室
- 版权声明:
- 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
相关文章
重启sentry+升级ExceptionLess版本,docker排坑记录
折腾的过程真的是究极折磨,写这篇文章仿佛又被折磨了一遍……
2024-07-19 · DevOps · 约 1 分钟
开启GitLab的邮件通知功能以及一些外观配置
这次把邮件功能、域名、外观啥的配置好了,写篇文章记录一下
2024-07-17 · DevOps · 约 1 分钟
ExceptionLess的安装、配置、使用
前言 Exceptionless 是一个开源的实时的日志收集框架,它可以应用在基于 ASP.NET,ASP.NET Core,Web API,Web Forms,WPF,Console,ASP.NET MVC 等技术开发的应用程序中,并且提供了REST接口可以应用在 Javascript,Node.js 中。(基本就是.Net技术栈的一套东西) 项目地址:https://github.com/ex
2022-07-21 · DevOps · 约 1 分钟
Traefik,想说爱你不容易:一场动态反向代理的心累之旅
如果说 Nginx 是稳重的老好人,那 Traefik 就像一个特立独行的极客。它不按常理出牌,拒绝繁琐配置文件,宣称“自动发现,一切皆自动”,用 Docker label 就能配好反向代理,听起来是不是很优雅?
2025-04-16 · DevOps · 约 1 分钟
讨论
共 0 条加载评论中…