从零搭建 CI/CD 全流程实战:4 台虚拟机 + Jenkins + Gitea + Registry + Trivy + Ansible

本文记录使用 4 台 Ubuntu 24.04 虚拟机搭建一套完整 CI/CD 流水线的全过程。涵盖架构设计、私有镜像仓库、代码仓库、Jenkins 主从节点、Trivy 镜像扫描、Ansible 自动化部署,以及 Webhook 自动触发。部署过程中所有能自动化的步骤均通过 Ansible Playbook 完成,减少手工操作。

一、背景与目标

1.1 硬件配置

节点配置磁盘角色
Server14C / 4G + 2G Swap60GJenkins 主 + Gitea + Registry + Ansible 控制节点
Server22C / 4G + 2G Swap40GJenkins Agent + 部署目标
Server32C / 4G + 2G Swap40GJenkins Agent + 部署目标
Server42C / 4G + 2G Swap40GJenkins Agent + 部署目标

1.2 网络环境

  • 虚拟机网络:NAT 模式
  • 四台机器均可访问外网
  • 内网 IP 段:192.168.136.0/24
节点IP
Server1192.168.136.155
Server2192.168.136.156
Server3192.168.136.157
Server4192.168.136.158

1.3 项目目标

搭建一套完整的 CI/CD 流水线:

代码提交 → 自动拉取 → 构建镜像 → 安全扫描 → 推送私有仓库 → Ansible 自动部署

设计原则:除 Server1 的基础 Docker 环境外,Server2-4 的所有依赖(Java、Docker 配置、DNS、时钟同步、Trivy、Python Docker SDK)均由 Ansible Playbook 统一推送,不手工登录每台机器操作。


二、最终架构

┌──────────────────────────────────────────────────────────────┐
│                      Server1 (4C/4G)                         │
│  ┌────────┐ ┌─────────┐ ┌───────────────┐ ┌───────────────┐  │
│  │ Gitea  │ │ Jenkins │ │Docker Registry│ │ Ansible       │  │
│  │ :3000  │ │ :8080   │ │    :5000      │ │ (控制节点)     |  │
│  └────────┘ └─────────┘ └───────────────┘ └───────────────┘  │
└──────────────────────────────────────────────────────────────┘
                            │
            ┌───────────────┼───────────────┐
            ▼               ▼               ▼
      ┌──────────┐    ┌──────────┐    ┌──────────┐
      │ Server2  │    │ Server3  │    │ Server4  │
      │ Jenkins  │    │ Jenkins  │    │ Jenkins  │
      │ Agent    │    │ Agent    │    │ Agent    │
      │ + 部署   │    │ + 部署    │    │ + 部署   │
      └──────────┘    └──────────┘    └──────────┘

流水线流程:

  1. 开发者推送代码到 Server1 的 Gitea
  2. Gitea 通过 Webhook 通知 Jenkins
  3. Jenkins 将任务分发到 Server2-4 中的某一台
  4. 构建节点拉取代码 → 构建 Docker 镜像 → Trivy 安全扫描 → 推送到私有 Registry
  5. Jenkins 调用 Ansible Playbook,在目标节点上拉取镜像并启动容器,完成部署

三、部署过程

3.1 基础环境准备(Server1)

1. 安装 Docker(Server1 手工执行一次)

Server1 作为控制节点,需要先有 Docker 环境才能跑 Registry、Gitea、Jenkins:

# 安装 Docker(使用国内加速脚本)
bash <(wget -qO- https://get.xuanyuan.cloud/docker.sh)

2. 安装 Ansible(Server1)

sudo apt update
sudo apt install -y ansible
ansible --version

3. 配置 SSH 免密

Ansible 通过 SSH 管理 Server2-4,需要先打通免密:

# 在 Server1 上生成密钥(如果已有可跳过)
ssh-keygen -t rsa -b 4096 -N "" -f ~/.ssh/id_rsa

# 分发公钥到 Server2-4
for ip in 192.168.136.156 192.168.136.157 192.168.136.158; do
  ssh-copy-id admin_123@$ip
done

4. 创建 Ansible 工作目录与 inventory

sudo mkdir -p /opt/ansible
cd /opt/ansible

创建 inventory.ini:

[servers]
server2 ansible_host=192.168.136.156 ansible_user=admin_123
server3 ansible_host=192.168.136.157 ansible_user=admin_123
server4 ansible_host=192.168.136.158 ansible_user=admin_123

[servers:vars]
ansible_python_interpreter=/usr/bin/python3
ansible_ssh_common_args='-o StrictHostKeyChecking=no'

验证连通性:

ansible -i inventory.ini servers -m ping

3.2 用 Ansible 初始化 Server2-4 基础环境

编写 /opt/ansible/init-nodes.yml,一次性完成以下所有基础配置:

  • 配置国内 DNS
  • 安装并启用时钟同步
  • 配置 Docker 信任私有仓库
  • 安装 Java(Jenkins Agent 依赖)
  • 安装 Python Docker SDK(Ansible docker 模块依赖)
  • 创建 Jenkins Agent 工作目录
---
- name: 初始化 Jenkins 构建节点
  hosts: servers
  become: yes

  vars:
    registry_ip: "192.168.136.155:5000"
    agent_dir: "/opt/jenkins-agent"
    agent_user: "admin_123"

  tasks:
    # ========== 1. 配置 DNS ==========
    - name: 创建 systemd-resolved 配置目录
      file:
        path: /etc/systemd/resolved.conf.d
        state: directory
        mode: '0755'

    - name: 配置国内 DNS
      copy:
        dest: /etc/systemd/resolved.conf.d/dns_servers.conf
        content: |
          [Resolve]
          DNS=223.5.5.5 223.6.6.6
          FallbackDNS=8.8.8.8
        mode: '0644'
      notify: restart systemd-resolved

    # ========== 2. 安装并启用时钟同步 ==========
    - name: 安装 systemd-timesyncd
      apt:
        name: systemd-timesyncd
        state: present
        update_cache: yes

    - name: 启用并启动 systemd-timesyncd
      systemd:
        name: systemd-timesyncd
        enabled: yes
        state: started

    # ========== 3. 安装 Java(Jenkins Agent 依赖)==========
    - name: 安装 OpenJDK 21 JRE
      apt:
        name: openjdk-21-jre-headless
        state: present

    # ========== 4. 安装 Python Docker SDK ==========
    - name: 安装 python3-pip
      apt:
        name: python3-pip
        state: present

    - name: 安装 Docker Python SDK
      pip:
        name: docker
        state: present
        executable: pip3

    # ========== 5. 配置 Docker 信任私有仓库 ==========
    - name: 配置 daemon.json(合并已有配置)
      copy:
        dest: /etc/docker/daemon.json
        content: |
          {
            "registry-mirrors": ["https://docker.xuanyuan.me"],
            "insecure-registries": ["{{ registry_ip }}"]
          }
        mode: '0644'
      notify: restart docker

    # ========== 6. 创建 Jenkins Agent 工作目录 ==========
    - name: 创建 Agent 工作目录
      file:
        path: "{{ agent_dir }}"
        state: directory
        owner: "{{ agent_user }}"
        group: "{{ agent_user }}"
        mode: '0755'

    # ========== 7. 将用户加入 docker 组 ==========
    - name: 将 admin_123 加入 docker 组
      user:
        name: "{{ agent_user }}"
        groups: docker
        append: yes

  handlers:
    - name: restart systemd-resolved
      systemd:
        name: systemd-resolved
        state: restarted

    - name: restart docker
      systemd:
        name: docker
        state: restarted

执行:

cd /opt/ansible
ansible-playbook -i inventory.ini init-nodes.yml
注意:daemon.json 的合并逻辑在 Playbook 里是直接覆盖。如果目标节点已有其他 Docker 配置(如 data-root),需要改成用 blockinfile 或 template 做 JSON 合并,避免丢失原有配置。

3.3 部署 Docker Registry(Server1)

1. 创建数据目录

sudo mkdir -p /opt/docker-data/registry

2. 启动 Registry 容器

docker run -d \
  --name registry \
  --restart=always \
  -p 5000:5000 \
  -v /opt/docker-data/registry:/var/lib/registry \
  registry:2

3. 验证

curl http://localhost:5000/v2/                    # 期望返回 {}
docker info | grep -A 2 "Insecure Registries"     # 确认配置生效

3.4 部署 Gitea(Server1)

1. 创建数据目录

sudo mkdir -p /opt/docker-data/gitea

2. 启动 Gitea 容器

docker run -d \
  --name gitea \
  --restart=always \
  -p 3000:3000 \
  -p 222:22 \
  -v /opt/docker-data/gitea:/data \
  -v /etc/timezone:/etc/timezone:ro \
  -v /etc/localtime:/etc/localtime:ro \
  gitea/gitea:latest

3. 初始化

浏览器打开 http://192.168.136.155:3000,按向导完成:

  • 数据库:SQLite3
  • SSH 服务端口:222(注意不是 22)
  • 创建管理员账号

3.5 部署 Jenkins 主节点(Server1)

1. 创建数据目录

sudo mkdir -p /opt/docker-data/jenkins
sudo chown -R 1000:1000 /opt/docker-data/jenkins

2. 启动 Jenkins 容器

docker run -d \
  --name jenkins \
  --restart=always \
  -p 8080:8080 \
  -p 50000:50000 \
  -v /opt/docker-data/jenkins:/var/jenkins_home \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v /usr/bin/docker:/usr/bin/docker \
  -e JAVA_OPTS="-Xms1024m -Xmx2048m -XX:+UseG1GC" \
  jenkins/jenkins:lts-jdk17

3. 获取初始密码

docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword

浏览器访问 http://192.168.136.155:8080,输入密码,安装推荐插件,创建管理员账号。

注意:安装插件时如遇 java.net.SocketTimeoutException,通常是国内访问 Jenkins 官方源不稳定。可换清华镜像源,或手动下载 .hpi 文件通过 插件管理 → Advanced settings → Deploy Plugin 上传。

3.6 配置 Jenkins 构建节点(Server2-4)

1. 添加 SSH 凭据

  • 获取 Server1 的私钥:cat ~/.ssh/id_rsa
  • Jenkins → 系统管理 → 凭据 → 全局凭据 → Add Credentials
  • 类型:SSH Username with private key
  • ID:server1-ssh-key
  • Username:admin_123
  • Private Key:粘贴 Server1 的私钥

2. 添加节点

配置项Server2Server3Server4
节点名称server2server3server4
Remote root directory/opt/jenkins-agent同左同左
Labelsbuild-nodebuild-nodebuild-node
Launch methodLaunch agents via SSH同左同左
Host192.168.136.156192.168.136.157192.168.136.158
Credentialsserver1-ssh-key同左同左
Host Key VerificationNon verifying同左同左

3. 验证

节点状态应为绿色 ✅。如果显示 ❌ 且日志提示 java: command not found,说明 3.2 节 Playbook 没跑完,重新执行 ansible-playbook -i inventory.ini init-nodes.yml 即可。

3.7 用 Ansible 部署 Trivy

1. 在 Server1 上准备 Trivy 二进制

# 下载(选择适合架构的版本)
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | \
  sudo sh -s -- -b /tmp/trivy-bin

ls /tmp/trivy-bin/trivy

2. 编写 Playbook /opt/ansible/install-trivy.yml

---
- name: 在构建节点上安装 Trivy
  hosts: servers
  become: yes

  tasks:
    - name: 复制 Trivy 二进制
      copy:
        src: /tmp/trivy-bin/trivy
        dest: /usr/local/bin/trivy
        mode: '0755'
        owner: root
        group: root

    - name: 验证 Trivy 版本
      command: trivy --version
      register: trivy_version
      changed_when: false

    - name: 输出版本
      debug:
        msg: "{{ trivy_version.stdout }}"

执行:

cd /opt/ansible
ansible-playbook -i inventory.ini install-trivy.yml

3. 推送 Trivy 镜像与漏洞库到私有仓库

由于国内访问 Google 源受限,Trivy 运行时从私有仓库拉取漏洞库更稳定。在 Server1 上执行:

# 推送 Trivy 镜像
docker pull aquasec/trivy:latest
docker tag aquasec/trivy:latest 192.168.136.155:5000/aquasec/trivy:latest
docker push 192.168.136.155:5000/aquasec/trivy:latest

# 推送漏洞库
docker pull aquasec/trivy-db:2
docker tag aquasec/trivy-db:2 192.168.136.155:5000/aquasec/trivy-db:2
docker push 192.168.136.155:5000/aquasec/trivy-db:2
注意:Trivy 第一次运行时需要下载漏洞库。如果直接从公网拉,国内大概率失败;改为从私有仓库拉取即可绕开。Jenkinsfile 中的 TRIVY_DB_REPOSITORY 参数就是做这件事。

3.8 创建示例项目

1. 在 Gitea 上创建仓库 taobao-page

2. 添加三个文件

index.html(简化版淘宝页面,略,可参照前文)。

Dockerfile:

FROM nginx:alpine
RUN apk upgrade --no-cache && rm -rf /usr/share/nginx/html/*
COPY . /usr/share/nginx/html/
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
说明:apk upgrade --no-cache 会在构建时升级系统包,减少基础镜像中已知漏洞的数量。

Jenkinsfile:

pipeline {
    agent { label 'build-node' }

    environment {
        REGISTRY = "192.168.136.155:5000"
        IMAGE_NAME = "taobao-page"
        IMAGE_TAG = "v${BUILD_NUMBER}"
    }

    stages {
        stage('拉取代码') {
            steps { sh 'ls -la' }
        }

        stage('构建 Docker 镜像') {
            steps {
                sh '''
                    docker build -t ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} .
                    docker tag ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} ${REGISTRY}/${IMAGE_NAME}:latest
                '''
            }
        }

        stage('镜像安全扫描') {
            steps {
                sh '''
                    docker run --rm \
                      -v /var/run/docker.sock:/var/run/docker.sock \
                      -e TRIVY_DB_REPOSITORY="192.168.136.155:5000/aquasec/trivy-db:2" \
                      -e TRIVY_INSECURE="true" \
                      192.168.136.155:5000/aquasec/trivy:latest image \
                      --severity HIGH,CRITICAL \
                      --exit-code 0 \
                      --no-progress \
                      ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}
                '''
            }
        }

        stage('推送镜像到私有仓库') {
            steps {
                sh '''
                    docker push ${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}
                    docker push ${REGISTRY}/${IMAGE_NAME}:latest
                '''
            }
        }

        stage('通过 Ansible 部署') {
            steps {
                sh '''
                    cd /opt/ansible
                    ansible-playbook -i inventory.ini deploy-taobao.yml \
                      -e "image_tag=${IMAGE_TAG}" \
                      -e "registry=${REGISTRY}" \
                      -e "image_name=${IMAGE_NAME}"
                '''
            }
        }
    }
}

3. 推送代码到 Gitea

git init
git config --global user.email "admin@example.com"
git config --global user.name "admin"
git add .
git commit -m "初始化淘宝页面项目"
git remote add origin http://192.168.136.155:3000/admin/taobao-page.git
git push -u origin main

3.9 创建 Jenkins Pipeline 任务

  1. Jenkins → 新建 Item → 名称 taobao-page-build → 选择 Pipeline
  2. 配置:

    • Definition:Pipeline script from SCM
    • SCM:Git
    • Repository URL:http://192.168.136.155:3000/admin/taobao-page.git
    • Credentials:添加 Gitea 用户名密码凭据(ID:gitea-credential)
    • Branch Specifier:*/main
    • Script Path:Jenkinsfile
  3. 保存并测试构建

3.10 配置 Webhook 自动触发

1. 安装 Generic Webhook Trigger 插件

如插件源下载超时,手动下载 .hpi 上传:

  • 下载:https://mirrors.jenkins.io/plugins/generic-webhook-trigger/latest/
  • Jenkins → 系统管理 → 插件管理 → Advanced settings → Deploy Plugin
  • 重启 Jenkins

2. 在 Jenkins 任务中启用

  • 任务配置 → 构建触发器 → 勾选 Generic Webhook Trigger
  • Token 填:taobao-trigger-2026
  • 保存

3. 在 Gitea 中添加 Webhook

  • Gitea → 仓库 → 设置 → Web 钩子 → 添加 Web 钩子
  • 目标 URL:http://172.17.0.1:8080/generic-webhook-trigger/invoke?token=taobao-trigger-2026
  • HTTP 方法:POST
  • POST Content Type:application/json
  • 触发条件:勾选"推送事件"

注意:Gitea 运行在 Docker 容器中,无法通过宿主机内网 IP 192.168.136.155:8080 访问 Jenkins。Webhook URL 需使用 Docker 网关地址 172.17.0.1。

验证:进入 Gitea 容器测试:

docker exec -it gitea sh
wget -q -O - http://172.17.0.1:8080/generic-webhook-trigger/invoke?token=taobao-trigger-2026
# 返回 {"triggered":true} 即成功
exit

3.11 用 Ansible 完成部署

1. 编写 Playbook /opt/ansible/deploy-taobao.yml

---
- name: 部署淘宝页面到目标节点
  hosts: servers
  become: yes
  vars:
    registry: "192.168.136.155:5000"
    image_name: "taobao-page"
    image_tag: "latest"
    container_name: "taobao-page"
    host_port: 8081
    container_port: 80

  tasks:
    - name: 拉取最新镜像
      docker_image:
        name: "{{ registry }}/{{ image_name }}:{{ image_tag }}"
        source: pull
        force_source: yes

    - name: 停止旧容器(如果存在)
      docker_container:
        name: "{{ container_name }}"
        state: stopped
      ignore_errors: yes

    - name: 删除旧容器(如果存在)
      docker_container:
        name: "{{ container_name }}"
        state: absent
      ignore_errors: yes

    - name: 启动新容器
      docker_container:
        name: "{{ container_name }}"
        image: "{{ registry }}/{{ image_name }}:{{ image_tag }}"
        state: started
        restart_policy: always
        ports:
          - "{{ host_port }}:{{ container_port }}"

    - name: 验证容器运行状态
      command: docker ps --filter "name={{ container_name }}" --format "{{ '{{' }}.Status{{ '}}' }}"
      register: container_status
      changed_when: false

    - name: 输出容器状态
      debug:
        msg: "容器 {{ container_name }} 状态:{{ container_status.stdout }}"

2. Jenkins 调用 Ansible

Jenkinsfile 的"通过 Ansible 部署"阶段已定义,构建时自动执行。

3. 验证

# 手动执行一次验证
cd /opt/ansible
ansible-playbook -i inventory.ini deploy-taobao.yml

输出应类似:

TASK [启动新容器] ***
changed: [server2]
changed: [server3]
changed: [server4]

TASK [输出容器状态] ***
ok: [server2] => { "msg": "容器 taobao-page 状态:Up 3 seconds" }

浏览器访问 http://192.168.136.157:8081 即可看到部署的页面。


四、成果验证

4.1 构建记录

构建号触发方式结果
#1手动✅
#2手动 Webhook 测试✅
#3 / #4Gitea 自动触发✅
#5 ~ #11Trivy 调试❌ / ✅
#12Gitea 自动触发 + Trivy 扫描✅

4.2 Ansible 部署日志片段

PLAY [部署淘宝页面到目标节点] ****************************************

TASK [Gathering Facts] *********************************************
ok: [server2]
ok: [server3]
ok: [server4]

TASK [拉取最新镜像] *************************************************
changed: [server2]
changed: [server3]
changed: [server4]

TASK [启动新容器] ***************************************************
changed: [server2]
changed: [server3]
changed: [server4]

TASK [输出容器状态] *************************************************
ok: [server2] => { "msg": "容器 taobao-page 状态:Up 3 seconds" }

4.3 Trivy 扫描结果

192.168.136.155:5000/taobao-page:v11 (alpine 3.24.2)
Total: 1 (HIGH: 1, CRITICAL: 0)
├─ libexpat │ CVE-2026-93990 │ HIGH │ 2.8.4-r0 → 2.8.5-r0
说明:扫描发现有 1 个 HIGH 漏洞,--exit-code 0 让流水线只报告不阻断。生产环境可以改为 --exit-code 1 强制阻断,或在 Dockerfile 里升级相关包。

五、Ansible 在架构中的价值

环节手工方式Ansible 方式
部署执行者登录每台机器Server1 统一调度
幂等性手动 stop/rmdocker_container 模块自带幂等
多目标支持一台一台操作hosts: servers 一次部署全部
配置集中管理分散在 Jenkinsfile 的 shell 里集中在 Playbook
可审计脚本难追踪Playbook 可纳入 Git 版本控制
参数传递硬编码-e 动态传入
扩展性加机器要改脚本只需改 inventory.ini

基础环境初始化也走 Ansible 的意义:Java、DNS、时钟同步、Docker 配置这些"每台都要做一遍"的事,一次 Playbook 全部搞定,避免手工操作遗漏。


六、配置要点速查

#事项说明处理
1Docker 信任私有仓库已有 registry-mirrors 时在原有 JSON 上追加,勿覆盖
2Jenkins 插件源国内访问官方源不稳定换清华源或手动上传 .hpi
3Jenkins Agent 依赖需要 Java由 init-nodes.yml 自动安装
4Server 节点 DNS127.0.0.53 可能无法解析由 init-nodes.yml 统一切换
5时钟同步Jenkins 要求节点时间一致由 init-nodes.yml 启用 systemd-timesyncd
6Gitea Webhook 地址容器无法通过宿主机 IP 访问改用 Docker 网关 172.17.0.1
7Trivy 漏洞库国内无法直连 Google 源推送到私有仓库,用 TRIVY_DB_REPOSITORY 指向
8Ansible docker 模块依赖 Python Docker SDK由 init-nodes.yml 自动安装
9Ansible 权限用户需在 docker 组由 init-nodes.yml 自动加入

七、后续优化方向

  1. Harbor 替代基础 Registry:带 UI、用户权限、镜像扫描、垃圾回收,生产级。
  2. SonarQube 代码质量扫描:在"构建"之前加一道质量门禁,不达标阻断流水线。
  3. 通知集成:在 Jenkinsfile 的 post 块里加钉钉 / 飞书 / 邮件通知。
  4. Prometheus + Grafana 监控:采集 Jenkins 指标,看构建成功率、时长、队列。
  5. 清理策略:

    • Registry:定期 registry garbage-collect
    • 构建节点:定期 docker system prune -f
    • Jenkins:配置"丢弃旧的构建"保留最近 10 次
  6. Kubernetes 化:把 Ansible 部署换成 k8s 的 Deployment,Agent 用动态 Pod。

八、总结

8.1 最终流水线

开发者修改代码 → git push 到 Gitea
  → Gitea 通过 Webhook 通知 Jenkins
    → Jenkins 在 Server2/3/4 中分配构建节点
      → 拉取代码 → 构建 Docker 镜像
        → Trivy 安全扫描(报告漏洞)
          → 推送到私有 Registry
            → Jenkins 调用 Ansible Playbook
              → Ansible 在目标节点拉取镜像并启动容器
                → 对外提供服务

8.2 覆盖的技术点

技术用途
Docker容器运行时
Docker Registry私有镜像仓库
Gitea代码仓库
JenkinsCI/CD 调度引擎
Jenkins Agent分布式构建节点
Trivy镜像安全扫描
Ansible批量运维与部署
Webhook自动化触发
NginxWeb 服务

8.3 这套架构的关键设计

  • 基础环境自动化:Java、DNS、时钟、Docker 配置、Python SDK 全部由 Ansible 统一推送,避免逐台登录
  • 镜像安全前置:Trivy 在推送前扫描,早发现早处理
  • 部署配置集中:Ansible Playbook 集中在 Server1 的 /opt/ansible,可纳入版本控制
  • 参数外置:Jenkinsfile 通过 -e 传参给 Playbook,配置与代码分离
  • 容器网络打通:Gitea Webhook 用 Docker 网关地址,绕开容器网络隔离

整套系统规模不大,但已经具备生产环境 CI/CD 的核心要素。后续可在此基础上继续优化,逐步完善为一个更健壮、易维护的自动化交付平台。