DevOps 入门系列 :搭建一条GitLab CI_CD 端到端流水线
GitLab CI/CD 端到端流水线
注:本节代码需手动删除注释后使用!!!!!
一、端到端 CI/CD 流水线
本章目标:编写一条完整的流水线,实现闭环:代码提交 → 自动测试 → 自动构建镜像 → 推送阿里云ACR → ECS自动部署 + 手动回滚
我们将分阶段逐个编码实现 Job,最后合并为一份完整流水线配置文件。
整条流水线长什么样?
流水线划分为 5 个串行阶段,前序 Stage 失败则后续 Stage 终止执行。
stages:
- test # 单元测试,代码校验
- build_image # Docker构建镜像,导出tar镜像包
- push_image # 加载tar镜像,推送至阿里云ACR私有仓库
- deploy # SSH远程ECS,拉取镜像、启停容器、健康校验
- rollback # 手动触发,回滚至上一个可用版本
流程解释
-
test:执行
pytest单元测试,测试异常直接终止整条流水线,避免无效打包部署。 -
build_image:测试通过后构建 Docker 镜像,使用流水线 ID 作为唯一不可变镜像标签,镜像导出为
image.tar,通过 GitLab 制品向下游传递。 -
push_image:接收上游 tar 镜像文件,本地加载镜像,补全仓库路径标签,推送阿里云容器镜像仓库。
-
deploy:SSH 免密登录 ECS 服务器,拉取新镜像、销毁旧容器、启动新版本容器、应用健康检查;部署成功落地版本号到服务器本地文件。
-
rollback:人工手动触发 Job,读取服务器留存历史版本标签,拉取旧镜像完成容器回滚。
1.1 阶段一:test —— 让流水线先帮你检查代码
目标:代码git push后自动执行单元测试,测试失败流水线直接终止,阻断后续打包部署。
设计原则:测试放在流水线首阶段,提前拦截故障代码,节约构建、服务器资源,是 CI 流水线通用规范。
1.1.1 项目目录结构
my-flask-app/
├── app.py # Flask 应用(带健康检查接口)
├── requirements.txt # Python 依赖
├── tests/
│ └── test_app.py # 单元测试
├── Dockerfile # 镜像构建文件
└── .gitlab-ci.yml # CI/CD 流水线配置
1.1.2 前置文件
为了跑通我们编写的流水线,你先需要编写以下几个文件(放在 Git 仓库根目录):
app.py—— 一个极简的 Flask 应用
from flask import Flask, jsonify
import os
app = Flask(__name__)
@app.route('/')
def hello():
return "Hello from CI/CD!"
@app.route('/health')
def health():
# 健康检查接口,返回 200 表示服务正常
return jsonify({"status": "ok"}), 200
if __name__ == '__main__':
# 监听所有网卡,端口由环境变量 PORT 指定(默认 5000)
port = int(os.environ.get('PORT', 5000))
app.run(host='0.0.0.0', port=port)
requirements.txt—— 依赖清单
Flask==2.3.3
pytest==7.4.0
1.1.3 最小测试用例
def test_hello():
assert "hello" in "hello world"
1.1.4 Test 阶段基础 Job 配置
- 我们需要一个名为
run_tests的 job用来执行测试任务,归属于 test 阶段 - 该测试依赖于 Python镜像(python:3.9-slim)
- 执行
test_app.py脚本进行测试
stages:
- test
run_tests:
stage: test
image: python:3.9-slim
before_script:
- pip install pytest #python:3.9-slim不带pytest,要手动安装
script:
- pytest tests/ #执行tests目录下所有测试用例
配置逐行说明
| 配置项 | 说明 |
|---|---|
stage: test | 绑定至 test 阶段,同阶段 Job 并行执行,阶段串行 |
image: python:3.9-slim | GitLab Runner 基于该 Python 镜像创建运行容器 |
before_script | 脚本前置执行命令,用于安装项目依赖 |
script | 核心执行逻辑,pytest 用例失败会返回非 0 退出码,流水线标记失败 |
1.1.5 集成测试报告(Artifacts 制品留存)
光执行测试还不够,我们需要知道测试结果,因此需要输出可视化报告并生成制品留存:安装pytest-html生成可视化测试报告,通过artifacts保存报告,流水线结束可在 GitLab 页面下载查看:
run_tests:
stage: test
image: python:3.9-slim
before_script:
- pip install pytest pytest-html
script:
- pytest tests/ --html=report.html
artifacts:
paths:
- report.html
when: always # 无论流水线成功/失败都保留报告
expire_in: 3 days # 制品自动过期清理时间
1.1.6 阶段自测方案
-
修改用例:
assert 1 == 2,提交代码,观察流水线失败变红; -
恢复正确用例,重新提交,流水线执行成功变绿。
1.2 阶段二:build_image —— 构建镜像并保存为 tar 文件
目标:测试通过后构建 Docker 镜像,导出 tar 镜像包,通过 GitLab 制品传递给下一阶段 Push Job。
为什么导出 tar?
GitLab 每个 Job 运行在独立临时容器,Job 执行完毕容器销毁,本地构建的 Docker 镜像随之丢失;通过docker save打包成文件,借助artifacts跨 Job 传输镜像。
1.2.1 项目 Dockerfile
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "app.py"]
1.2.2 Build 镜像 Job 配置
stages:
- test
- build_image
docker_build:
stage: build_image
image: docker:24.0.7
services:
- docker:24.0.7-dind
variables:
DOCKER_HOST: tcp://docker:2375 #Docker引擎路径
DOCKER_TLS_CERTDIR: "" #关闭TLS安全验证,防止连接失败
script:
- docker build -t myapp:$CI_PIPELINE_ID .
- docker save myapp:$CI_PIPELINE_ID -o image.tar
artifacts:
paths:
- image.tar
expire_in: 1 day
配置说明
-
services: docker:xxx-dind:Docker-in-Docker,容器内部启动 Docker 守护进程,实现容器内构建镜像; -
DOCKER_HOST:指定 Docker 客户端连接内部 DinD 服务地址; -
$CI_PIPELINE_ID:GitLab 内置环境变量,每条流水线唯一 ID,用作不可变镜像标签; -
docker save:镜像导出为本地image.tar,制品配置使下游 Job 自动拉取该文件。
1.2.3 自测方式
临时注释test阶段配置,仅保留 build_image,提交代码;在流水线 Job 详情页下载image.tar,验证文件正常生成。
1.3 阶段三:push_image —— 把镜像推送到阿里云 ACR
目标:读取上游image.tar,本地加载镜像、重命名仓库标签、推送至阿里云私有镜像仓库 ACR。
1.3.1 前置准备
-
阿里云 ACR 信息示例
-
仓库域名:
registry.cn-hangzhou.aliyuncs.com -
命名空间:
devops-demo -
镜像仓库名:
my-flask-app
-
-
GitLab 项目→设置→CI/CD→变量,添加环境密钥变量(项目密文,不写入配置文件)
变量名 内容 ALIYUN_USER ACR 登录用户名 ALIYUN_PASSWORD ACR 仓库登录密码
1.3.2 Push Job 配置
stages:
- test
- build_image
- push_image
docker_push:
stage: push_image
image: docker:24.0.7
services:
- docker:24.0.7-dind
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
before_script:
# 先登录仓库
- docker login -u "$ALIYUN_USER" -p "$ALIYUN_PASSWORD" registry.cn-hangzhou.aliyuncs.com
script:
# 加载上游制品镜像包
- docker load -i image.tar
# 重打阿里云全路径标签
- docker tag myapp:$CI_PIPELINE_ID registry.cn-hangzhou.aliyuncs.com/devops-demo/my-flask-app:ci-$CI_PIPELINE_ID
# 推送远程ACR仓库
- docker push registry.cn-hangzhou.aliyuncs.com/devops-demo/my-flask-app:ci-$CI_PIPELINE_ID
1.3.3 功能验证
流水线执行成功后,登录阿里云 ACR 控制台,对应仓库下查看新增镜像,标签格式:ci-流水线ID。
1.4 阶段四:deploy —— 自动部署到云服务器
Docker镜像已经推送至我们的仓库中,接下去我们要用流水线将其从仓库中拿出来,部署到服务器上。
目标:SSH 远程连接阿里云 ECS,拉取新镜像、关停旧容器、启动新版本、应用健康检测、落地版本号用于后续回滚。
1.4.1 部署前置配置
-
ECS 安全组放行:22 端口 (SSH) + 应用端口 5000;
-
ECS 服务器预装 Docker 环境;
-
GitLab CI 新增服务器连接变量:
变量名 说明 ECS_HOST ECS 公网 IP 地址 ECS_USER 服务器登录用户名(如 root) ECS_PASSWORD 服务器登录密码
1.4.2 最简部署 Job
stages:
- test
- build_image
- push_image
- deploy
deploy_to_ecs:
stage: deploy
image: alpine:latest
before_script:
# 安装 SSH 远程连接工具,让 CI 能登录你的服务器执行部署命令
- apk add --no-cache openssh-client sshpass
script:
# 自动登录服务器并执行部署
- sshpass -p "$ECS_PASSWORD" ssh -o StrictHostKeyChecking=no $ECS_USER@$ECS_HOST "
docker login -u $ALIYUN_USER -p $ALIYUN_PASSWORD registry.cn-hangzhou.aliyuncs.com &&
docker pull registry.cn-hangzhou.aliyuncs.com/devops-demo/my-flask-app:ci-$CI_PIPELINE_ID &&
docker stop myapp || true &&
docker rm myapp || true &&
docker run -d --name myapp -p 5000:5000 --restart=unless-stopped registry.cn-hangzhou.aliyuncs.com/devops-demo/my-flask-app:ci-$CI_PIPELINE_ID
"
|| true:容器不存在时 stop/rm 命令不抛出异常,避免部署中断;--restart=unless-stopped:服务器意外重启自动拉起容器。
1.4.3 新增应用健康检查(规避部署成功但服务异常)
在docker run后追加健康探测脚本,连续 5 次探测应用/health接口,失败则部署 Job 报错:
sleep 3
for i in 1 2 3 4 5; do
if curl -s http://localhost:5000/health > /dev/null; then
echo "Health check OK"
exit 0
fi
sleep 2
done
echo "Health check FAILED"
exit 1
1.4.4 落地当前版本号(回滚依赖)
部署成功后,在 ECS 服务器/opt/myapp/version.current写入当前镜像标签,作为回滚目标版本:
echo "ci-$CI_PIPELINE_ID" > /opt/myapp/version.current
该脚本放入after_script,部署成功后自动执行。
script:
# 命令同上
after_script:
- sshpass -p "$ECS_PASSWORD" ssh -o StrictHostKeyChecking=no $ECS_USER@$ECS_HOST "
mkdir -p /opt/myapp &&
echo 'ci-$CI_PIPELINE_ID' > /opt/myapp/version.current
"
1.4.5 部署验证
-
首次部署完成:浏览器访问
http://ECS公网IP:5000可正常访问服务; -
修改项目代码重新提交,流水线自动触发全流程,服务自动更新。
1.5 阶段五:rollback —— 手动触发回滚
目标:新版本故障时,人工点击 GitLab 流水线按钮,一键回滚至上一个稳定版本。
when: manual:Job 不会随流水线自动运行,需在 GitLab 页面手动点击启动。
stages:
- test
- build_image
- push_image
- deploy
- rollback
rollback:
stage: rollback
image: alpine:latest
when: manual
before_script:
- apk add --no-cache openssh-client sshpass
script:
- |
OLD_TAG=$(sshpass -p "$ECS_PASSWORD" ssh -o StrictHostKeyChecking=no $ECS_USER@$ECS_HOST "cat /opt/myapp/version.current")
if [ -z "$OLD_TAG" ]; then
echo "No previous version found"
exit 1
fi
sshpass -p "$ECS_PASSWORD" ssh -o StrictHostKeyChecking=no $ECS_USER@$ECS_HOST "
docker pull registry.cn-hangzhou.aliyuncs.com/devops-demo/my-flask-app:$OLD_TAG &&
(docker stop myapp || true) &&
(docker rm myapp || true) &&
docker run -d --name myapp -p 5000:5000 registry.cn-hangzhou.aliyuncs.com/devops-demo/my-flask-app:$OLD_TAG
"
回滚实操验证
-
部署 V1 版本→修改代码部署 V2 版本;
-
进入流水线页面,手动执行 rollback Job;
-
访问服务,确认应用退回 V1 版本。
1.6 组合起来:完整的 .gitlab-ci.yml
使用全局变量统一管理仓库地址、命名空间,修改配置只需变更顶部变量,可直接复制使用,按需替换自定义参数。
stages:
- test
- build_image
- push_image
- deploy
- rollback
# 全局自定义变量,按需修改
variables:
REGISTRY: "registry.cn-hangzhou.aliyuncs.com"
NAMESPACE: "你的命名空间"
IMAGE_NAME: "你的仓库名"
IMAGE_TAG: "ci-$CI_PIPELINE_ID"
IMAGE_FULL: "$REGISTRY/$NAMESPACE/$IMAGE_NAME:$IMAGE_TAG"
# pip依赖缓存,加速测试安装依赖
cache:
key:
files:
- requirements.txt
paths:
- .cache/pip
# ========== 1.自动化测试阶段 ==========
run_tests:
stage: test
image: python:3.9-slim
before_script:
- pip install --cache-dir .cache/pip pytest
script:
- pytest tests/
# ========== 2.镜像构建&导出tar ==========
docker_build:
stage: build_image
image: docker:24.0.7
services:
- docker:24.0.7-dind
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
script:
- docker build -t $IMAGE_NAME:$CI_PIPELINE_ID .
- docker save $IMAGE_NAME:$CI_PIPELINE_ID -o image.tar
artifacts:
paths:
- image.tar
expire_in: 1 day
# ========== 3.推送镜像至阿里云ACR ==========
docker_push:
stage: push_image
image: docker:24.0.7
services:
- docker:24.0.7-dind
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
before_script:
- docker login -u "$ALIYUN_USER" -p "$ALIYUN_PASSWORD" $REGISTRY
script:
- docker load -i image.tar
- docker tag $IMAGE_NAME:$CI_PIPELINE_ID $IMAGE_FULL
- docker push $IMAGE_FULL
# ========== 4.ECS自动化部署+健康检查 ==========
deploy_to_ecs:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache openssh-client sshpass
script:
- sshpass -p "$ECS_PASSWORD" ssh -o StrictHostKeyChecking=no $ECS_USER@$ECS_HOST "
set -e;
docker login -u $ALIYUN_USER -p $ALIYUN_PASSWORD $REGISTRY &&
docker pull $IMAGE_FULL &&
(docker stop myapp || true) &&
(docker rm myapp || true) &&
docker run -d --name myapp --restart=unless-stopped -p 5000:5000 $IMAGE_FULL &&
sleep 3 &&
for i in 1 2 3 4 5; do
if curl -s http://localhost:5000/health > /dev/null; then
echo 'Health check passed';
exit 0;
fi;
sleep 2;
done;
echo 'Health check failed' && exit 1;
"
# 部署成功后写入当前版本号
after_script:
- sshpass -p "$ECS_PASSWORD" ssh -o StrictHostKeyChecking=no $ECS_USER@$ECS_HOST "
mkdir -p /opt/myapp && echo $IMAGE_TAG > /opt/myapp/version.current
"
environment:
name: production
url: http://$ECS_HOST:5000
only:
- main # 仅main分支触发自动部署
# ========== 5.手动回滚Job ==========
rollback:
stage: rollback
image: alpine:latest
when: manual
before_script:
- apk add --no-cache openssh-client sshpass
script:
- |
OLD_TAG=$(sshpass -p "$ECS_PASSWORD" ssh -o StrictHostKeyChecking=no $ECS_USER@$ECS_HOST "cat /opt/myapp/version.current")
if [ -z "$OLD_TAG" ]; then
echo "No previous version found"
exit 1
fi
OLD_IMAGE_FULL="$REGISTRY/$NAMESPACE/$IMAGE_NAME:$OLD_TAG"
sshpass -p "$ECS_PASSWORD" ssh -o StrictHostKeyChecking=no $ECS_USER@$ECS_HOST "
docker login -u $ALIYUN_USER -p $ALIYUN_PASSWORD $REGISTRY &&
docker pull $OLD_IMAGE_FULL &&
(docker stop myapp || true) &&
(docker rm myapp || true) &&
docker run -d --name myapp --restart=unless-stopped -p 5000:5000 $OLD_IMAGE_FULL
"
environment:
name: production
action: rollback
补充拓展优化建议
-
安全优化:摒弃
sshpass明文密码登录,改用 SSH 密钥免密登录 ECS,私钥存入 GitLab CI 变量; -
版本优化:使用 SemVer 语义化标签替代流水线 ID;
-
部署优化:生产环境替换原生 docker run 为 Docker Compose/K8s 部署;
-
通知优化:配置流水线钉钉 / 企业微信告警,部署成功 / 失败推送消息。
更多推荐
所有评论(0)