[CI/CD]小型技术团队开发规范与最小化 DevOps CI/CD 研发流程实施指南

[CI/CD]小型技术团队开发规范与最小化 DevOps CI/CD 研发流程实施指南

绝大部分公司开发团队配置一般为小型技术开发团队(5-15人)设计,旨在提供一套轻量、高效、可落地的开发规范与 CI/CD 实施方案。方案以 Gitea 为核心代码托管与 CI/CD 引擎,避免引入重型工具(如 Jenkins/GitLab),实现最小化 DevOps 闭环,供灵活参考。


第一部分:开发与分支管理规范

1. 分支模型设计 (简化版 GitFlow)

针对小型团队,摒弃繁琐的分支模型,采用以交付为导向的简化分支策略:

  • **main (或 prod)**:生产环境分支。始终保持可发布状态,仅接受来自 test 的合并。打 Tag 管理版本(如 v1.2.0)。
  • **test**:测试环境分支。接受来自 dev 的合并,部署至测试环境供 QA 验证。
  • **dev**:开发环境主分支。所有特性分支的汇聚地,部署至开发环境进行联调。
  • **feature/***:特性分支。从 dev 切出,开发完成后合并回 dev。(如 feature/user-login)
  • **hotfix/*:热修复分支。从 main 切出,修复线上紧急 Bug,修复后必须同时合并回 main 和 dev**。

2. 分支合并与 Deadline 流程

为防止分支长期不合并导致“合并地狱”,制定严格的合并 Deadline 规范。

graph TD
    subgraph 需求与开发阶段
        A[需求评审完成] --> B[从 dev 创建 feature 分支]
        B --> C{开发进行中}
        C -->|每日提交| D[保持与 dev 分支同步 rebase]
    end

    subgraph 合并与 Deadline 控制
        D --> E["开发完成, 发起
PR/MR 到 dev"] E --> F{Code Review & 自动化检查} F -->|不通过| C F -->|通过| G[合并入 dev] G --> H{"Dev 环境联调 Deadline:
1天或一个发布周期内,切记不能堆积"} H -->|超时未通过| I[打回重构, 记录延期原因] H -->|按时通过| J[发起 PR/MR 到 test] J --> K{"QA 测试 Deadline:
3天或一个发布周期内,切记不能堆积"} K -->|发现 Bug| L[在 feature 分支修复, 重新走合并流程] K -->|测试通过| M[合并入 test] M --> N{"UAT/预发验证 Deadline:
1天或一个发布周期内,切记不能堆积"} N -->|通过| O[发起 PR/MR 到 main] N -->|不通过| L end subgraph 发布阶段 O --> P[合并入 main, 打 Tag] P --> Q[触发 Prod 环境自动化部署] end style H fill:#ffcccc,stroke:#ff0000 style K fill:#ffcccc,stroke:#ff0000 style N fill:#ffcccc,stroke:#ff0000

3. 配置文件与敏感信息管理

  • 代码与配置分离:严禁将数据库密码、API Key、第三方 Secret 等硬编码在代码中。
  • 环境变量:Go 项目推荐使用 godotenv 或 viper 读取环境变量。
  • 配置模板:仓库中必须提供 .env.example 或 config.yaml.example,包含所有配置项的 Key 和默认值/注释,但不包含真实敏感值。
  • 环境差异化:不同环境(dev/test/prod)的配置通过 CI/CD 注入,或在目标服务器上维护独立的 .env 文件。

4. 核心注意事项

  • Commit 规范:遵循 Conventional Commits(如 feat: 增加用户登录, fix: 修复空指针),便于自动生成 Changelog。
  • 冲突解决:合并前,开发者必须在本地将 dev 分支 rebase 或 merge 到自己的 feature 分支,解决冲突后再推送到远端发起 PR。
  • 禁止强推:dev、test、main 分支在 Gitea 中必须开启分支保护,禁止 git push -f,必须通过 Pull Request 合并。

第二部分:最小化 DevOps CI/CD 实施规范

基于 Gitea + Gitea Actions(兼容 GitHub Actions 语法),我们无需额外搭建 Jenkins,即可实现最小化 CI/CD。以下针对三种不同底层技术栈的场景提供实施规范。

场景一:GIT + Gitea + Bash (极简物理机/虚拟机部署)

  • 适用场景:项目初期,服务器资源有限,无需容器化,直接运行二进制文件。
  • CI 流程:Gitea Actions 负责代码检查、单元测试、交叉编译生成 Linux 二进制包。
  • CD 流程:通过 SSH 将二进制包和配置文件传输到目标服务器,执行 Bash 脚本重启 systemd 服务。

场景二:GIT + Gitea + Docker/Containerd (容器化单机/集群部署)

  • 适用场景:需要环境一致性,便于迁移,使用 Docker 或 Containerd 作为运行时。
  • CI 流程:Gitea Actions 构建 Docker 镜像,推送到 Gitea 内置的 Package Registry(或阿里云/腾讯云镜像仓库)。
  • CD 流程:目标服务器通过 SSH 执行脚本,拉取最新镜像,停止旧容器,启动新容器(推荐使用 docker-compose 管理)。

场景三:GIT + Gitea + Containerd + K8S (云原生 Kubernetes 部署)

  • 适用场景:中后期,需要高可用、弹性伸缩、滚动更新。
  • CI 流程:同场景二,构建镜像并推送到 Registry。
  • CD 流程:CI 更新 K8S 部署清单(Deployment YAML 中的镜像 Tag),或使用 kubectl set image 命令,触发 K8S 的滚动更新机制。

第三部分:Go Web 应用完整发布部署实战

假设我们有一个基于 Gin 框架的 Go Web 应用 go-web-app,监听 8080 端口。

环境 IP 规划 (192.168.100.x 网段)

  • Gitea 服务器:192.168.100.10 (包含 Gitea 及 Gitea Actions Runner)
  • Dev/Test 服务器:192.168.100.20 (场景一、二使用)
  • Prod 物理机:192.168.100.30 (场景一、二使用)
  • K8S 集群:Master 192.168.100.40,Worker 192.168.100.41 (场景三使用)

基础项目准备

1. 极简 Go 代码 (main.go)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"net/http"
"os"
"github.com/gin-gonic/gin"
)

func main() {
port := os.Getenv("APP_PORT")
if port == "" {
port = "8080"
}
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "pong", "env": os.Getenv("APP_ENV")})
})
r.Run(":" + port)
}

2. 配置文件模板 (.env.example)

1
2
APP_PORT=8080
APP_ENV=dev

场景一实施:Bash 极简部署

1. 目标服务器准备 (192.168.100.20 和 192.168.100.30)
配置 systemd 服务 /etc/systemd/system/go-web-app.service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[Unit]
Description=Go Web App
After=network.target

[Service]
Type=simple
User=deploy
WorkingDirectory=/opt/go-web-app
EnvironmentFile=/opt/go-web-app/.env
ExecStart=/opt/go-web-app/go-web-app
Restart=always

[Install]
WantedBy=multi-user.target

2. Gitea CI/CD 配置 (.gitea/workflows/deploy-bash.yml)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
name: Build and Deploy via Bash
on:
push:
branches: [ dev, test, main ]

jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Setup Go
uses: actions/setup-go@v4
with:
go-version: '1.21'

- name: Build Binary
run: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o go-web-app main.go

- name: Determine Target Env
id: env
run: |
if [[ "${{ github.ref }}" == "refs/heads/dev" ]]; then echo "IP=192.168.100.20" >> $GITHUB_OUTPUT; echo "ENV=dev" >> $GITHUB_OUTPUT;
elif [[ "${{ github.ref }}" == "refs/heads/test" ]]; then echo "IP=192.168.100.20" >> $GITHUB_OUTPUT; echo "ENV=test" >> $GITHUB_OUTPUT;
else echo "IP=192.168.100.30" >> $GITHUB_OUTPUT; echo "ENV=prod" >> $GITHUB_OUTPUT; fi

- name: Deploy via SSH
uses: appleboy/scp-action@master
with:
host: ${{ steps.env.outputs.IP }}
username: deploy
key: ${{ secrets.SSH_PRIVATE_KEY }}
source: "go-web-app,.env.example"
target: "/opt/go-web-app/"
overwrite: true

- name: Restart Service
uses: appleboy/ssh-action@master
with:
host: ${{ steps.env.outputs.IP }}
username: deploy
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cp /opt/go-web-app/.env.example /opt/go-web-app/.env # 实际生产中应保留原.env或从配置中心拉取
sed -i "s/APP_ENV=dev/APP_ENV=${{ steps.env.outputs.env }}/g" /opt/go-web-app/.env
sudo systemctl daemon-reload
sudo systemctl restart go-web-app

场景二实施:Docker/Containerd 容器化部署

1. 编写 Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o go-web-app main.go

FROM alpine:latest
RUN apk --no-cache add ca-certificates tzdata
ENV TZ=Asia/Shanghai
WORKDIR /root/
COPY --from=builder /app/go-web-app .
EXPOSE 8080
CMD ["./go-web-app"]

2. 目标服务器准备 (192.168.100.20)
编写 docker-compose.yml:

1
2
3
4
5
6
7
8
9
version: '3'
services:
go-web-app:
image: 192.168.100.10:3000/team/go-web-app:latest # 指向Gitea镜像仓库
ports:
- "8080:8080"
env_file:
- .env
restart: always

3. Gitea CI/CD 配置 (.gitea/workflows/deploy-docker.yml)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
name: Build and Deploy Docker
on:
push:
branches: [ dev, test, main ]

jobs:
docker-build-push:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3

- name: Login to Gitea Registry
uses: docker/login-action@v2
with:
registry: 192.168.100.10:3000
username: ${{ secrets.GITEA_USER }}
password: ${{ secrets.GITEA_TOKEN }}

- name: Build and Push
uses: docker/build-push-action@v4
with:
context: .
push: true
# 使用分支名作为 tag,main 分支使用 latest
tags: |
192.168.100.10:3000/team/go-web-app:${{ github.ref_name }}
192.168.100.10:3000/team/go-web-app:latest

deploy-docker:
needs: docker-build-push
runs-on: ubuntu-latest
steps:
- name: Determine Target
id: target
run: |
if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then echo "IP=192.168.100.30" >> $GITHUB_OUTPUT;
else echo "IP=192.168.100.20" >> $GITHUB_OUTPUT; fi

- name: Deploy via SSH
uses: appleboy/ssh-action@master
with:
host: ${{ steps.target.outputs.IP }}
username: deploy
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /opt/go-web-app-docker
# 登录镜像仓库拉取最新镜像
docker login 192.168.100.10:3000 -u ${{ secrets.GITEA_USER }} -p ${{ secrets.GITEA_TOKEN }}
docker-compose pull
docker-compose up -d --remove-orphans

场景三实施:Containerd + K8S 云原生部署

1. 编写 K8S 部署清单 (k8s/deployment.yaml)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
apiVersion: apps/v1
kind: Deployment
metadata:
name: go-web-app
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: go-web-app
template:
metadata:
labels:
app: go-web-app
spec:
containers:
- name: go-web-app
image: 192.168.100.10:3000/team/go-web-app:latest # 占位符,CI会替换
ports:
- containerPort: 8080
env:
- name: APP_ENV
value: "prod"
---
apiVersion: v1
kind: Service
metadata:
name: go-web-app-svc
spec:
selector:
app: go-web-app
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: NodePort

2. K8S 节点准备 (192.168.100.40 Master)
配置 Containerd 允许拉取 Gitea 的 HTTP insecure 镜像:
修改 /etc/containerd/config.toml,添加:

1
2
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."192.168.100.10:3000"]
endpoint = ["http://192.168.100.10:3000"]

重启 containerd:systemctl restart containerd。

3. Gitea CI/CD 配置 (.gitea/workflows/deploy-k8s.yml)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
name: Build and Deploy to K8S
on:
push:
branches: [ main ] # K8S通常只部署main/prod

jobs:
build-and-deploy-k8s:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3

- name: Login to Gitea Registry
uses: docker/login-action@v2
with:
registry: 192.168.100.10:3000
username: ${{ secrets.GITEA_USER }}
password: ${{ secrets.GITEA_TOKEN }}

- name: Build and Push Image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: 192.168.100.10:3000/team/go-web-app:${{ github.sha }}

- name: Deploy to K8S
env:
KUBECONFIG_DATA: ${{ secrets.KUBECONFIG }} # 在Gitea Secrets中配置Base64后的kubeconfig
run: |
echo "$KUBECONFIG_DATA" | base64 -d > kubeconfig
export KUBECONFIG=./kubeconfig

# 应用基础资源
kubectl apply -f k8s/deployment.yaml

# 更新镜像为本次构建的 commit sha,触发滚动更新
kubectl set image deployment/go-web-app go-web-app=192.168.100.10:3000/team/go-web-app:${{ github.sha }} -n default

# 等待滚动更新完成
kubectl rollout status deployment/go-web-app -n default --timeout=120s

第四部分:分支间配置 Config 文件管理、版本控制与注意事项深度指南

本部分作为整个开发规范与 CI/CD 实施指南的延续,聚焦于多分支、多环境下配置文件的全生命周期管理。为确保文档编号的全局连贯性,本节子序号自第一部分(1-4)之后顺延。

5. 核心原则与配置文件分层模型

在展开具体方案之前,必须确立三条不可逾越的铁律:

编号铁律说明
R1代码中零敏感信息密码、Token、密钥、证书等绝对禁止提交到任何分支
R2配置模板随代码走,真实值随环境走仓库中只保留 .env.example / config.yaml.example,真实配置文件由 CI/CD 注入或从配置中心拉取
R3配置变更必须可追溯配置文件的每一次修改都必须有 commit 记录或审计日志

针对 dev → test → main(prod) 的分支流转,配置文件应严格分为 三层:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌─────────────────────────────────────────────────────────┐
│ Layer 3: 环境级配置 (Environment-Specific) │
│ 存放位置: 目标服务器 / K8S Secret / Vault │
│ 内容: DB密码、Redis地址、第三方API Key、域名 │
│ 管理方式: 运维手动维护 或 CI/CD Secrets 注入 │
│ ⚠️ 绝不进入 Git 仓库 │
├─────────────────────────────────────────────────────────┤
│ Layer 2: 环境差异化配置 (Environment-Aware Defaults) │
│ 存放位置: Git 仓库 config/dev.yaml, test.yaml, prod.yaml│
│ 内容: 日志级别、功能开关、超时时间、连接池大小 │
│ 管理方式: 随代码版本控制,通过 PR 审核变更 │
│ ✅ 进入 Git 仓库,但不含敏感值 │
├─────────────────────────────────────────────────────────┤
│ Layer 1: 应用默认配置 (Application Defaults) │
│ 存放位置: Git 仓库 config/default.yaml 或代码内硬编码 │
│ 内容: 默认端口、默认路由前缀、框架基础参数 │
│ 管理方式: 随代码版本控制 │
│ ✅ 进入 Git 仓库 │
└─────────────────────────────────────────────────────────┘

配置加载优先级(Go 项目推荐 Viper 实现):
环境变量 > Layer 3 (环境级) > Layer 2 (环境差异化) > Layer 1 (默认值)

6. Git 仓库目录结构与版本控制规范

标准目录树:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go-web-app/
├── .gitea/workflows/ # CI/CD 流水线
├── config/
│ ├── default.yaml # Layer 1: 应用默认配置
│ ├── dev.yaml # Layer 2: 开发环境差异化配置
│ ├── test.yaml # Layer 2: 测试环境差异化配置
│ └── prod.yaml # Layer 2: 生产环境差异化配置
├── .env.example # Layer 3 的模板(占位符)
├── .gitignore # ⚠️ 关键文件
├── main.go
└── k8s/
├── deployment.yaml
├── configmap.yaml # Layer 2 的 K8S 映射
└── secret.yaml.tpl # Layer 3 的模板

.gitignore 必须包含的条目:

1
2
3
4
5
6
# 真实配置文件 - 绝对禁止提交
.env
config/local.yaml
config/override.yaml
*.pem
*.key

7. 敏感信息管理方案(Layer 3 落地)

对于小型团队,推荐直接使用 Gitea Secrets 进行统一管理,避免引入重型 Vault 组件。

在 Gitea 仓库的 Settings → Actions → Secrets 中配置:

Secret 名称示例值用途
DB_PASSWORD_DEVdev_pass_123开发环境数据库密码
DB_PASSWORD_PRODprod_pass_789生产环境数据库密码
JWT_SECRETjwt_super_secretJWT 签名密钥
SSH_PRIVATE_KEY-----BEGIN...部署用 SSH 私钥

.env.example 模板规范:

1
2
3
4
5
6
7
8
9
10
11
12
# ============================================
# 应用配置 - 此文件为模板,请勿填写真实值
# ============================================
APP_PORT=8080
APP_ENV=dev

# 数据库 (⚠️ 真实密码从 Gitea Secrets 获取)
DB_HOST=127.0.0.1
DB_PORT=3306
DB_NAME=app_dev
DB_USER=app_user
DB_PASSWORD=<your_db_password_here>

8. 分支合并中配置文件的冲突处理

冲突高发区:多个开发者同时修改 config/dev.yaml 导致合并冲突。

预防措施:

  1. 按模块分区:在 YAML 中按功能模块(如 auth:、payment:)分区,减少同一行冲突概率。
  2. 独立配置文件:大型模块拆分为 config/dev/auth.yaml、config/dev/payment.yaml。
  3. 合并前强制同步:发起 PR 前,必须在本地执行 git rebase origin/dev 解决冲突。

冲突解决 SOP:

  1. 定位 <<<<<<< 标记,判断是“不同模块冲突(两者都保留)”还是“同一配置项冲突(需沟通确认)”。
  2. 解决冲突后,必须在本地启动应用验证,确保配置格式正确且无缺失项。
  3. 提交并推送。

9. 配置文件版本管理与变更原子性

配置版本号机制:
在 config/default.yaml 中维护版本号,应用启动时校验,防止代码与配置不匹配:

1
2
# config/default.yaml
_config_version: "2.3.0"
1
2
3
4
// Go 代码启动时检查
if v.GetString("_config_version") != "2.3.0" {
log.Fatalf("配置版本不匹配! 请更新配置文件。")
}

变更原子性原则:

新增配置项的代码和配置模板必须在同一个 PR 中提交。 严禁“先合代码,三天后再补配置”的做法,这会导致中间状态的应用启动直接 panic。

配置回滚与备份:
每次 Bash/Docker 部署前,CI 脚本应自动备份当前配置文件:

1
2
3
cp /opt/go-web-app/.env /opt/go-web-app/.env.bak.$(date +%Y%m%d%H%M%S)
# 保留最近 5 个备份
ls -t /opt/go-web-app/.env.bak.* | tail -n +6 | xargs rm -f

10. 三种部署场景下的配置注入流程

场景Layer 1/2 注入方式Layer 3 (敏感值) 注入方式
BashSCP 传输 config/*.yaml 到服务器CI 读取 Gitea Secrets,通过 sed 替换 .env 占位符后 SCP 传输
Docker打包进镜像(或挂载宿主机 config 目录)docker-compose.yml 通过 environment 或 env_file 从宿主机 .env 注入
K8S通过 ConfigMap 挂载到 Pod 内部通过 Secret 注入为环境变量,或使用 External Secrets Operator

11. 配置漂移检测与治理机制

配置漂移:代码仓库中的配置与生产服务器实际运行的配置不一致(如运维手动改了服务器配置但未提交 Git)。

检测手段:
在目标服务器(如 192.168.100.30)配置 Cron 定时巡检脚本:

1
2
3
4
5
6
#!/bin/bash
# 对比服务器本地配置与 Git 仓库 main 分支的最新配置
git clone --depth 1 --branch main http://192.168.100.10:3000/team/go-web-app.git /tmp/repo
if ! diff -q /opt/go-web-app/config/prod.yaml /tmp/repo/config/prod.yaml > /dev/null; then
echo "⚠️ [配置漂移告警]" | mail -s "Config Drift" team@example.com
fi

12. 完整注意事项清单 (Checklist)

开发阶段

  • 新增功能需要新配置项时,同步更新 default.yaml、环境文件、.env.example
  • 敏感配置只写占位符 <your_xxx_here>
  • .gitignore 已包含所有真实配置文件模式

合并阶段

  • 发起 PR 前,已在本地 rebase 目标分支并解决配置冲突
  • Code Review 时重点检查配置文件中是否误提交了敏感信息

部署阶段

  • CI 从 Gitea Secrets 注入敏感值,而非硬编码在 workflow 文件中
  • 部署前已备份目标服务器上的当前配置文件
  • 部署后验证应用启动日志中的配置加载信息是否正确

运维阶段

  • 手动修改服务器配置后,必须同步提交 PR 到 Git 仓库
  • 配置漂移巡检脚本正常运行
  • 定期轮换 Secrets,轮换后更新 Gitea Secrets 并重新部署

结语

本规范通过 Gitea + Gitea Actions 构建了最小化 DevOps 闭环,避免了小型团队陷入工具链维护的泥潭。

  1. 初期:使用 Bash 场景,快速验证业务,成本最低。
  2. 中期:平滑过渡到 Docker 场景,解决环境一致性问题,利用 Gitea Package Registry 管理镜像。
  3. 后期:演进至 K8S 场景,实现高可用与弹性伸缩。

团队应严格遵守分支 Deadline 与合并规范,确保代码流转的高效与安全。配置与代码分离是贯穿始终的红线,务必通过 Secrets 和环境变量进行安全管理。

[CI/CD]小型技术团队开发规范与最小化 DevOps CI/CD 研发流程实施指南

https://www.wdft.com/7c1ee937.html

Author

Jaco Liu

Posted on

2025-12-27

Updated on

2026-10-07

Licensed under