返回文章列表文章索引阅读全文
STARBLOG / 阅读手记
DevOps

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.

曦远 (DealiAxy)约 27 分钟
目录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:

  1. Retrieve the latest git tag as the version number
  2. Build Docker image and push it to the configured image repository
  3. 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这个日志收集系统:ExceptionLess的安装、配置、使用 由于ExceptionLess官方提供的客户端只有.Net/.NetCore平台和js的,本文继续介绍另一个日志收集系统:Sentry Sentry 是一个实时事件日志记录和聚合平台。(官方说的是错误监控 Error Monitor)它专门用于监视错误和提取执行适当的事后操作所需的所有信

2022-07-23 · DevOps · 约 1 分钟

Gitlab的CI_CD自动部署前端项目

前言 之前陆续写了 Gitlab 的安装使用还有 Gitlab CI/CD 的配置使用,已经把 AspNetCore 的后端项目都做了持续集成了,尝到甜头之后,现在前端的项目也要加入自动化部署,所以经过一番折腾,有了本文~ 前文回顾: GitLab 的安装、配置、使用 持续集成指南:GitLab 的 CI/CD 工具配置与使用 跟之前的 AspNetCore 项目操作差不多,我依然使用 docke

2022-09-30 · DevOps · 约 1 分钟

讨论

共 0 条

良言一句三冬暖,恶语伤人六月寒。

加载评论中…