DevOps 入门实战(一):从 CI/CD 概念到 Jenkins 自动构建部署

目标:在一台 Windows 开发机 + 三台 CentOS 虚拟机上,把「写代码 → 提交 → 自动构建 → 自动部署」这条链路完整跑通。

读完可以独立完成:Spring Boot 项目、GitLab 私有仓库、Jenkins 三次构建、SSH 推送 jar、目标机 Docker 运行。

文章目录

    • [一、先搞清楚概念:SDLC、瀑布、敏捷与 CI/CD](#一、先搞清楚概念:SDLC、瀑布、敏捷与 CI/CD)
    • 二、实验环境规划
    • [三、第一步:准备一个能跑起来的 Web 应用](#三、第一步:准备一个能跑起来的 Web 应用)
    • [四、第二步:GitLab 私有代码仓库](#四、第二步:GitLab 私有代码仓库)
      • [4.1 安装](#4.1 安装)
      • [4.2 建组、建用户、分权限](#4.2 建组、建用户、分权限)
      • [4.3 把本地代码推上去](#4.3 把本地代码推上去)
    • [五、第三步:Jenkins 部署与初始化](#五、第三步:Jenkins 部署与初始化)
      • [5.1 用 Docker 起 Jenkins](#5.1 用 Docker 起 Jenkins)
      • [5.2 插件与全局工具](#5.2 插件与全局工具)
    • [六、第四步:Publish over SSH 打通推送通道](#六、第四步:Publish over SSH 打通推送通道)
    • 七、第五步:第一次构建------把代码拉下来
    • [八、第六步:第二次构建------Maven 打成 jar](#八、第六步:第二次构建——Maven 打成 jar)
    • [九、第七步:第三次构建------用 SSH 把 jar 送到 web](#九、第七步:第三次构建——用 SSH 把 jar 送到 web)
    • [十、第八步:Docker 化并真正跑起来](#十、第八步:Docker 化并真正跑起来)
      • [10.1 把 Docker 文件也纳入版本库](#10.1 把 Docker 文件也纳入版本库)
      • [10.2 web 机器准备 Docker](#10.2 web 机器准备 Docker)
      • [10.3 第 5 次构建:让目标机自己部署](#10.3 第 5 次构建:让目标机自己部署)
    • [十一、第九步:版本迭代 v1.0 → v2.0 → v3.0](#十一、第九步:版本迭代 v1.0 → v2.0 → v3.0)
    • 十二、踩坑清单
    • 十三、总结

一、先搞清楚概念:SDLC、瀑布、敏捷与 CI/CD

软件开发生命周期(SDLC)是所有 DevOps 讨论的起点:需求分析 → 设计 → 实现 → 测试 → 进化,五个阶段首尾相接,形成闭环。

传统的瀑布模型 把这条链路做成单向流水:上一个阶段完全结束后才进入下一阶段。优点是文档齐全、节点清晰;缺点是用户要等到项目末期才能看到成果,风险后置、无法响应需求变化。

后来演化出敏捷开发,核心是两点:

  • 迭代开发:把一年的大开发拆成多个小周期,每个周期走一遍完整流程;
  • 增量开发:每个迭代交付一个用户可感知的完整功能,而不是所有功能都做一半。

在此基础上才有持续集成(CI):开发者频繁地把代码合并到主干,每一次合并都自动触发「构建 + 测试」,用例不通过就不许合并。CI 的完整链路是:

复制代码
提交 → 测试(第一轮) → 构建 → 测试(第二轮) → 部署 → 回滚

而一个 CI 体系必须有三样东西:

组成要素 作用 本实验中的角色
自动构建过程 检出、编译、测试、记录结果全自动 Jenkins + Maven
代码存储库 版本控制 + 构建素材来源 GitLab
持续集成服务器 调度构建、展示结果 Jenkins(Docker 运行)

二、实验环境规划

整个实验用 4 台机器(1 台 Windows 开发机 + 3 台 CentOS 虚拟机,建议 2C4G 起):

主机名 IP 配置 软件
gitlab 192.168.100.153 2C2G/200G gitlab-ce 12.4.2(端口 82)
jenkins 192.168.100.154 2C4G/200G JDK 1.8、Maven 3.6.2、Docker、Jenkins 2.346.3、SonarQube、Harbor
web 192.168.100.155 2C4G/200G Docker(部署目标机)
Windows 192.168.100.132 8G JDK 1.8、Maven 3.8.4、IDEA 2019.3.1、Git

整体数据流:
#mermaid-svg-QDsUL33QqZvaTfYd{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-QDsUL33QqZvaTfYd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-QDsUL33QqZvaTfYd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-QDsUL33QqZvaTfYd .error-icon{fill:#552222;}#mermaid-svg-QDsUL33QqZvaTfYd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-QDsUL33QqZvaTfYd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-QDsUL33QqZvaTfYd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-QDsUL33QqZvaTfYd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-QDsUL33QqZvaTfYd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-QDsUL33QqZvaTfYd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-QDsUL33QqZvaTfYd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-QDsUL33QqZvaTfYd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-QDsUL33QqZvaTfYd .marker.cross{stroke:#333333;}#mermaid-svg-QDsUL33QqZvaTfYd svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-QDsUL33QqZvaTfYd p{margin:0;}#mermaid-svg-QDsUL33QqZvaTfYd .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-QDsUL33QqZvaTfYd .cluster-label text{fill:#333;}#mermaid-svg-QDsUL33QqZvaTfYd .cluster-label span{color:#333;}#mermaid-svg-QDsUL33QqZvaTfYd .cluster-label span p{background-color:transparent;}#mermaid-svg-QDsUL33QqZvaTfYd .label text,#mermaid-svg-QDsUL33QqZvaTfYd span{fill:#333;color:#333;}#mermaid-svg-QDsUL33QqZvaTfYd .node rect,#mermaid-svg-QDsUL33QqZvaTfYd .node circle,#mermaid-svg-QDsUL33QqZvaTfYd .node ellipse,#mermaid-svg-QDsUL33QqZvaTfYd .node polygon,#mermaid-svg-QDsUL33QqZvaTfYd .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-QDsUL33QqZvaTfYd .rough-node .label text,#mermaid-svg-QDsUL33QqZvaTfYd .node .label text,#mermaid-svg-QDsUL33QqZvaTfYd .image-shape .label,#mermaid-svg-QDsUL33QqZvaTfYd .icon-shape .label{text-anchor:middle;}#mermaid-svg-QDsUL33QqZvaTfYd .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-QDsUL33QqZvaTfYd .rough-node .label,#mermaid-svg-QDsUL33QqZvaTfYd .node .label,#mermaid-svg-QDsUL33QqZvaTfYd .image-shape .label,#mermaid-svg-QDsUL33QqZvaTfYd .icon-shape .label{text-align:center;}#mermaid-svg-QDsUL33QqZvaTfYd .node.clickable{cursor:pointer;}#mermaid-svg-QDsUL33QqZvaTfYd .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-QDsUL33QqZvaTfYd .arrowheadPath{fill:#333333;}#mermaid-svg-QDsUL33QqZvaTfYd .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-QDsUL33QqZvaTfYd .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-QDsUL33QqZvaTfYd .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QDsUL33QqZvaTfYd .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-QDsUL33QqZvaTfYd .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QDsUL33QqZvaTfYd .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-QDsUL33QqZvaTfYd .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-QDsUL33QqZvaTfYd .cluster text{fill:#333;}#mermaid-svg-QDsUL33QqZvaTfYd .cluster span{color:#333;}#mermaid-svg-QDsUL33QqZvaTfYd div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-QDsUL33QqZvaTfYd .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-QDsUL33QqZvaTfYd rect.text{fill:none;stroke-width:0;}#mermaid-svg-QDsUL33QqZvaTfYd .icon-shape,#mermaid-svg-QDsUL33QqZvaTfYd .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QDsUL33QqZvaTfYd .icon-shape p,#mermaid-svg-QDsUL33QqZvaTfYd .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-QDsUL33QqZvaTfYd .icon-shape .label rect,#mermaid-svg-QDsUL33QqZvaTfYd .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QDsUL33QqZvaTfYd .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-QDsUL33QqZvaTfYd .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-QDsUL33QqZvaTfYd :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} git push
第1次构建 拉取
第2次构建 mvn package
第3次构建 Publish over SSH
第4/5次构建 docker-compose
Windows IDEA

demo_web
GitLab

192.168.100.153:82
Jenkins

192.168.100.154:8080
web

192.168.100.155
容器运行

:8080/test


三、第一步:准备一个能跑起来的 Web 应用

Windows 侧安装 JDK 1.8、Maven 3.8.4、IDEA 2019.3.1,并把 Maven 的 conf/settings.xml 换成阿里云镜像(放在 <mirrors> 里,注意把注释符号去掉):

xml 复制代码
<mirror>
  <id>aliyunmaven</id>
  <mirrorOf>central</mirrorOf>
  <name>aliyun maven</name>
  <url>https://maven.aliyun.com/repository/public</url>
</mirror>

用 IDEA 的 Spring Initializr 建一个 Maven 项目,组名 com.chenyan,项目名 demo_web,依赖只选 Spring Web (注意不是 spring-boot-starter-mvc,没有这个坐标)。

然后在 com.chenyan.demo.controller 下写一个最简单的接口:

java 复制代码
package com.chenyan.demo.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {
    @GetMapping("/test")
    public String test() {
        return "云原生测试";
    }
}

启动后浏览器访问 http://localhost:8080/test 能看到内容,说明应用本身没问题------这一步必须先本地跑通,否则后面全是玄学。


四、第二步:GitLab 私有代码仓库

4.1 安装

bash 复制代码
# 依赖与服务
yum -y install policycoreutils openssh-server openssh-clients postfix
systemctl enable sshd --now
systemctl enable postfix --now
firewall-cmd --add-service=ssh --permanent
firewall-cmd --add-service=http --permanent
firewall-cmd --reload

# 安装 GitLab CE
wget https://mirrors.tuna.tsinghua.edu.cn/gitlab-ce/yum/el6/gitlab-ce-12.4.2-ce.0.el6.x86_64.rpm
rpm -ivh gitlab-ce-12.4.2-ce.0.el6.x86_64.rpm

改两处配置后重载(关键是端口,因为 8080 要留给 Jenkins):

ruby 复制代码
# /etc/gitlab/gitlab.rb
external_url 'http://192.168.100.153:82'   # 第 23 行
nginx['listen_port'] = 82                  # 第 1112 行
bash 复制代码
gitlab-ctl reconfigure
gitlab-ctl restart
firewall-cmd --zone=public --add-port=82/tcp --permanent
firewall-cmd --reload

首次访问会要求设置 root 密码(实验中设为 abcd1234)。

4.2 建组、建用户、分权限

生产环境的做法是不要用 root 推代码 :新建组(如 chen_group,可见性设为 Private),新建普通用户 chen,再把用户加入组。

GitLab 的组权限有 5 档,理解它们对后面配 Developer 很关键:

角色 能做什么 典型对象
Guest 提 issue、评论,不能读写仓库 外部协作
Reporter 可以克隆,不能提交 QA / PM
Developer 克隆、开发、提交、push 普通开发
Maintainer 建项目、打 tag、保护分支、管成员 核心开发
Owner 改可见性、删项目、管组成员 组长

4.3 把本地代码推上去

在 IDEA 里 Add → Commit Directory 提交到本地仓库,再 Push 到 http://192.168.100.153:82/chen_group/web_demo.git,输入 GitLab 账号密码。

小技巧:IDEA 第一次 push 时如果弹窗要求输入账号,用「用户名 + 密码」,不要用 token;GitLab 12.x 对 http 账号直接支持。


五、第三步:Jenkins 部署与初始化

5.1 用 Docker 起 Jenkins

bash 复制代码
# 安装 docker(华为云源)
sudo yum install -y yum-utils device-mapper-persistent-data lvm2
wget -O /etc/yum.repos.d/docker-ce.repo https://mirrors.huaweicloud.com/docker-ce/linux/centos/docker-ce.repo
sudo sed -i 's+download.docker.com+mirrors.huaweicloud.com/docker-ce+' /etc/yum.repos.d/docker-ce.repo
sudo yum makecache fast && sudo yum install docker-ce -y
systemctl enable docker --now

docker-compose.yml(/usr/local/docker/jenkins_docker/):

yaml 复制代码
version: "3.1"
services:
  jenkins:
    image: jenkins/jenkins:2.346.3-lts-jdk8
    container_name: jenkins
    ports:
      - 8080:8080
      - 50000:50000
    volumes:
      - ./data/:/var/jenkins_home/
bash 复制代码
docker-compose up -d
docker logs -f jenkins

第一次启动大概率报:

复制代码
touch: cannot touch '/var/jenkins_home/copy_reference_file.log': Permission denied

原因是宿主机 data 目录权限和容器内 jenkins 用户(uid 1000)不匹配,最简单的做法:

bash 复制代码
chmod 777 data/
docker-compose restart

日志里会出现初始管理员密码,也可以直接读文件:

bash 复制代码
docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword

5.2 插件与全局工具

实验环境内网受限,采用离线插件 :把 plugins 目录用 Xftp 覆盖到 data/ 下,重启后 Jenkins 自动识别;再手动上传 Deploy、git-parameter 等插件。

然后是 系统管理 → Global Tool Configuration,把 JDK 与 Maven 指向容器内路径(这两个目录是从宿主机拷进 data 的):

复制代码
JDK  : /var/jenkins_home/jdk
Maven: /var/jenkins_home/maven

六、第四步:Publish over SSH 打通推送通道

「构建产物要送到另一台机器」靠 Publish over SSH 插件。配置入口:系统管理 → 系统配置 → Publish over SSH。

字段 值 说明
Name web 构建步骤里引用的名字
Hostname 192.168.100.155 目标机 IP,不要写主机名
Username root 登录用户
Remote Directory /usr/local/test 远程根目录,必须存在
高级 → 密码 123 目标机 root 密码
高级 → 跳板机(Jump host) 留空 填任何值都会先连跳板机,极易踩坑

配完点 Test Configuration ,成功会提示 Success。如果报 No such directory,去 web 上 mkdir -p /usr/local/test 再测。

⚠️ 高频坑:跳板机字段被误填(比如手滑填了密码 123),Java 会把它当成 IPv4 数字地址 0.0.0.123 去连接,报

java.net.SocketException: Invalid argument or cannot assign requested address。看到这个错先检查跳板机字段。


七、第五步:第一次构建------把代码拉下来

新建自由风格 任务 mytest,源码管理选 Git:

  • Repository URL:http://192.168.100.153:82/chen_group/web_demo.git
  • Credentials:添加 chen 的用户名密码凭据
  • 分支:*/master

点击「立即构建」,控制台出现 Checking out Revision ... 即成功。产物会落在:

bash 复制代码
/usr/local/docker/jenkins_docker/data/workspace/mytest/

八、第六步:第二次构建------Maven 打成 jar

在「构建」里增加 Invoke top-level Maven targets:

复制代码
Maven Version : maven
Goals         : clean package -DskipTests

三个参数的含义:clean 清掉旧 target、package 编译打包、-DskipTests 跳过单测(但仍会编译测试代码)。

构建完成后在宿主机可直接看到产物:

bash 复制代码
[root@jenkins target]# ls
classes  demo-0.0.1-SNAPSHOT.jar  demo-0.0.1-SNAPSHOT.jar.original  ...

九、第七步:第三次构建------用 SSH 把 jar 送到 web

「构建后操作 → Send build artifacts over SSH」:

配置项 值
SSH Server web
Source files target/*.jar
Remove prefix target/(可选,去掉目录层级)
Remote directory .(相对 SSH Server 的 /usr/local/test)
Exec command 见下
bash 复制代码
cd /usr/local/test
ls -l

构建成功后 web 上就有了 jar:

bash 复制代码
[root@web test]# ls
target
[root@web test]# ls target/
demo-0.0.1-SNAPSHOT.jar

十、第八步:Docker 化并真正跑起来

10.1 把 Docker 文件也纳入版本库

在项目里新建 docker/Dockerfile 与 docker/docker-compose.yml,提交到 GitLab,第 4 次构建就能把它们拉下来:

dockerfile 复制代码
FROM openjdk:8
COPY demo-0.0.1-SNAPSHOT.jar /usr/local/
WORKDIR /usr/local
CMD java -jar demo-0.0.1-SNAPSHOT.jar
yaml 复制代码
version: '3.1'
services:
  mytest:
    build:
      context: ./
      dockerfile: Dockerfile
    image: mytest:v1.0
    container_name: mytest
    ports:
      - 8080:8080

10.2 web 机器准备 Docker

bash 复制代码
yum install -y yum-utils device-mapper-persistent-data lvm2
yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
systemctl disable firewalld.service --now
setenforce 0
yum install docker-ce -y
systemctl enable docker --now

配好镜像加速(/etc/docker/daemon.json)后 systemctl daemon-reload && systemctl restart docker。

10.3 第 5 次构建:让目标机自己部署

传统的做法是 Jenkins 把文件推过去再手工执行;更工程化的做法是推送完成后远程执行一条命令。在「构建后操作」的 Exec command 中写:

bash 复制代码
cd /usr/local/test/docker
mv ../target/*.jar ./
docker-compose down
docker-compose up -d --build
docker image prune -f

构建完成,web 上:

bash 复制代码
[root@web docker]# docker ps -a
CONTAINER ID   IMAGE        COMMAND                  PORTS                  NAMES
525506d7cc30   mytest:v1.0  "/bin/sh -c 'java -j..."   0.0.0.0:8080->8080/tcp mytest

访问 http://192.168.100.155:8080/test,页面返回接口内容,第一条流水线闭环完成。


十一、第九步:版本迭代 v1.0 → v2.0 → v3.0

真正的价值在于改一次代码、点一次构建就能上线新版本:

  1. 修改 TestController 返回文案,pom.xml 版本号改为 2.0,提交并 push;
  2. Jenkins 立即构建(第 6~8 次构建);
  3. web 上 docker-compose down 后重建,页面变成 v2.0 的内容;
  4. 再迭代一版 v3.0,验证「代码 → 构建 → 部署」的重复性。

十二、踩坑清单

现象 原因 处理
Permission denied: copy_reference_file.log data 目录权限与容器内 uid 不匹配 chmod 777 data/ 后重启
浏览器打不开 Jenkins,本机正常 未开启 IP 转发 宿主机 net.ipv4.ip_forward=1 并 sysctl -p
No such directory 测试失败 远程目录不存在 目标机 mkdir -p /usr/local/test
java.net.SocketException: Invalid argument or cannot assign requested address 跳板机字段被填了数字(如 123 → 0.0.0.123) 跳板机留空
推送成功但 web 上看不到 jar Source files 与实际路径不一致 用 target/*.jar,必要时配 Remove prefix
docker-compose up 报端口占用 8080 被 Jenkins 或旧容器占用 换 8081:8080
mvn 报 无效的源发行版 IDEA/Maven 的 JDK 版本不一致 File → Project Structure 统一 1.8

十三、总结

这一篇做完,你已经具备一条可重复执行的 CI/CD 最小闭环:

  • 代码托管在私有 GitLab;
  • Jenkins 负责拉取、构建、产物投递;
  • 目标机 Docker 化运行,一条命令完成发布。

但它有明显短板:所有步骤都写在 Jenkins 页面的「构建后操作」里,配置不可版本化、不可评审、也无法并行。下一篇把这条链路升级为 Pipeline(Jenkinsfile 即代码),并接入 SonarQube 代码审计与 Harbor 私有镜像仓库。


本系列共三篇:

  1. DevOps 入门实战:从 CI/CD 概念到 Jenkins 自动构建部署(本篇)
  2. DevOps 进阶实战:SonarQube 代码审计 + Harbor 私有镜像仓库
  3. Jenkins Pipeline 实战:把流水线写成代码
相关推荐
xyzzklk22 分钟前
变相解决华为Mate 8锁屏后无法建立入站网络连接
linux·运维·服务器·网络·华为·go·安卓
叼烟扛炮2 小时前
Linux 学习笔记(五十九):epoll 的生死抉择——LT 与 ET 模式
linux·运维·服务器·网络·笔记·学习·php
飞飞传输10 小时前
飞驰云联文件交换建设方法论:从单点传输到持续治理
大数据·运维·安全
Ruiery10 小时前
Linux 6.6内核内存管理深度解析(十五):CMA 连续内存分配器
linux·运维·服务器
测试界的小码农11 小时前
【Linux 命令实战(01):认识 Linux 终端,从零掌握第一个命令】
linux·运维·服务器
布裘12 小时前
【银河麒麟】重启后时区丢失?排查发现 localtime 被业务程序替换成硬链接
linux·运维·网络
huainingning13 小时前
锐捷10.X路由器版本升级不生效问题排查
运维·网络·智能路由器
荣合技术服务13 小时前
HPC 超算集群故障维修实录:找荣合科算维护后,模拟仿真计算又能跑了
运维·服务器·超算服务器·荣合科算
阿里云云原生14 小时前
让复杂任务由团队完成:阿里云 AgentCore 多 Agent 协作架构及实践
云原生