ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从Jenkins到Drone CI:GitLab+Drone实现轻量级持续集成与自动化部署实践

从Jenkins到Drone CI:GitLab+Drone实现轻量级持续集成与自动化部署实践 近两年我开始把团队里几个web项目的发布流程从手动登录服务器操作逐步迁移到 GitLab Drone CI 这套组合上。这篇文章就把整个过程完整写出来包括最简单高效的环境搭建方式、Drone CI 的核心概念、一份可以直接照抄的 .drone.yml以及跑了大半年之后遇到的各种坑。无论你是刚接触持续集成的开发者还是正在 Jenkins 和 Drone 之间犹豫的运维这篇文章应该都能给你一个相对清晰的答案。1. 为什么我把部署流程从 Jenkins 换成了 Drone CI先说一个可能和多数人直觉相反的结论如果你的团队规模不大、服务器资源有限、主要需求是“代码推到 GitLab 后自动构建并部署到指定服务器”那么 Jenkins 往往是杀鸡用牛刀而 Drone CI 这种轻量级方案反而更顺手。这不是说 Jenkins 不好而是“适合”两个字很重要。Jenkins 功能确实很强插件生态也成熟但它带来的维护成本往往被低估。我见过太多团队装了一个 Jenkins结果从忘记更新插件到构建环境越搞越乱到最后 Jenkins 本身变成了一个需要专人维护的“项目”。另外 Jenkins 默认的 Master/Slave 架构和系统配置界面在初期学习成本不低。而 Drone CI 的核心思路完全不同——它是“容器原生”的整个服务端是单个二进制文件可以通过 Docker 一键启动每条流水线里的每一步都是独立的容器配置就是项目仓库里的一个 .drone.yml 文件。这意味着你不需要在服务器上安装额外的构建环境Node、Java、Python 等依赖全部在流水线中用镜像解决构建环境天然隔离也不会出现“在我机器上是好的在 Jenkins 上就挂了”这种环境漂移问题。再聊一下为什么不直接用 GitLab 自带的 CI/CD。GitLab CI 也很成熟而且如果你本身已经重度使用 GitLab它几乎是零成本的。我当时放弃它的原因是 Runner 的管理和权限体系略显笨重尤其是当团队需要为不同项目配置不同的执行环境和部署目标时GitLab CI 的 runner tag、变量作用域、项目级 CI/CD 设置这一套配置要梳理半天。Drone 这边把一切都收敛到代码仓库里的 .drone.yml 和 Drone 后台的项目设置中每一个步骤用什么镜像、执行什么命令、环境变量从哪来一眼就能看全。从资源占用这个实际角度来看GitLab 本身已经很吃内存了社区版建议至少 4GB实测 2GB 机器会很卡如果再在上面叠一个 Jenkins对一台 4GB 内存的云服务器来说基本是灾难。而 Drone Server 是一个 Go 语言静态编译的二进制内存占用通常只有几十 MBRunner 是按需启动的跑完就销毁。我在这套方案里最终用一台 4GB 内存的服务器同时跑 GitLab、Drone Server 和 Drone Runner在没有大规模并发构建的前提下完全跑得动。下面是当时做选型对比时整理的一张简表可以很直观地看到差距对比维度JenkinsGitLab CIDrone CI资源占用高一个实例动辄 1GB中依赖 GitLab 主服务低Server 仅几十 MB配置文件位置Web 界面/Job DSL.gitlab-ci.yml.drone.yml构建环境需要手动维护节点Runner 管理较复杂每个步骤一个容器天然隔离插件机制插件生态庞大成熟内置关键字较多以 Docker 插件为主逻辑简洁适合场景企业级重度流程GitLab 重度用户中小团队轻量自动化当时我还考虑过直接写 Shell 脚本配合 GitLab Webhook 做自动部署后来放弃了。原因很简单没有流水线状态展示、没有构建日志汇总、没有分支维度的触发控制脚本一旦跑挂了排查问题全靠服务器日志效率太低。Drone 的流水线页面能直观看到每个步骤的绿色小勾或红色小叉点进去就是完整日志这个体验对团队协作非常重要。2. 让 GitLab 和 Drone 先“握手”环境搭建全流程这套方案的基础是两个服务能互相认证通信这一步没配置好后面全白搭。我用 Docker Compose 一次性把 GitLab、Drone Server、Drone Runner 三个服务编排起来整个部署过程大概是半小时。2.1 用 Docker Compose 起一套 GitLab 社区版首先假设你手头有一台 Linux 服务器已经装好了 Docker 和 Docker Compose。GitLab 安装最省心的方式是用官方镜像但有几个坑需要提前避开GitLab 默认会监听 80 端口如果服务器上已经有 Nginx 或者别的服务占了 80必须改端口映射。GitLab 内部存储了绝对的访问地址这个地址是通过环境变量external_url指定的必须在第一次启动前想清楚以后用什么域名访问它。如果服务器内存小于 4GB启动后体验会很差建议至少保证 4GB。我整理了一个最小可用的 Compose 文件。这里我把 GitLab 映射到了宿主机的一个高位端口避免和 Nginx 冲突version: 3.8 services: gitlab: image: gitlab/gitlab-ce:16.2.4-ce.0 container_name: gitlab restart: always hostname: gitlab.example.com environment: GITLAB_OMNIBUS_CONFIG: | external_url http://gitlab.example.com:8929 gitlab_rails[gitlab_shell_ssh_port] 2224 nginx[listen_port] 8929 ports: - 8929:8929 - 2224:22 volumes: - /srv/gitlab/config:/etc/gitlab - /srv/gitlab/logs:/var/log/gitlab - /srv/gitlab/data:/var/opt/gitlab shm_size: 256m几个关键点解释一下external_url要带着端口号写成完整地址否则 GitLab 在 CI/CD 设置里回写给 GitLab Runner 的 Webhook 地址会漏掉端口。SSH 端口映射到 2224是因为宿主机 22 端口往往已经被 SSH 服务占用映射到 2224 之后以后git clone时需要用ssh://git服务器IP:2224/...或者配置 SSH config 来访问。shm_size: 256m是个容易忽略的配置不设置的话 GitLab 在某些操作下会把/dev/shm打满导致 500 错误。启动完成后第一次访问http://服务器IP:8929会让你设置 root 密码设置完就能登录。这一步做完GitLab 就准备好了。2.2 在 GitLab 后台创建 OAuth ApplicationDrone CI 自身不维护用户体系它通过 OAuth2 协议让用户直接用 GitLab 账号登录。所以先在 GitLab 里创建一个 Application。登录 GitLab 后进入 Admin Area管理员后台找到 Applications 或 Applications 菜单点击 New Application。这里有几个字段需要特别注意Name填 Drone CI。Redirect URI必须填http://服务器IP:端口/login也就是 Drone Server 的登录回调地址。比如 Drone 跑在 8800 端口就填http://服务器IP:8800/login。不然后续登录一定会报 redirect_uri 不匹配的错误。Confidential保持勾选。Scopes勾选api和read_user。其中 api 权限是必须的因为 Drone 要代表用户去读取 GitLab 仓库信息、设置 webhookread_user 用来同步用户名和邮箱。提交之后GitLab 会生成一个 Application ID 和一个 Secret这俩值先复制保存马上要用。2.3 配置 Drone Server 和 Runner仍然是 Docker Compose在同一个文件里追加 Drone Server 和 Runner 两个服务。下面是一份我在生产环境正在用的配置注释部分是踩坑之后的经验总结drone-server: image: drone/drone:2.16.2 container_name: drone-server restart: always ports: - 8800:80 environment: - DRONE_GITLAB_SERVERhttp://gitlab.example.com:8929 - DRONE_GITLAB_CLIENT_ID填前面拿到的Application ID - DRONE_GITLAB_CLIENT_SECRET填前面拿到的Secret - DRONE_RPC_SECRET自己生成一段随机字符串至少16位 - DRONE_SERVER_HOST服务器IP:8800 - DRONE_SERVER_PROTOhttp - DRONE_USER_CREATEusername:root,admin:true - DRONE_GIT_ALWAYS_AUTHfalse volumes: - /srv/drone:/var/lib/drone/ drone-runner: image: drone/drone-runner-docker:1.8.2 container_name: drone-runner restart: always depends_on: - drone-server environment: - DRONE_RPC_PROTOhttp - DRONE_RPC_HOSTdrone-server - DRONE_RPC_SECRET填和上面一样的DRONE_RPC_SECRET - DRONE_RUNNER_CAPACITY2 - DRONE_RUNNER_NAMEdocker-runner volumes: - /var/run/docker.sock:/var/run/docker.sockDRONE_RPC_SECRET是 Server 和 Runner 之间的通信凭证必须保持一致。它不需要特别复杂但一定不要用默认值。DRONE_USER_CREATEusername:root,admin:true是为了把 GitLab 的 root 账号自动提升为 Drone 的管理员。如果你是普通用户登录的就不要加这个变量等第一次用 GitLab 账号登录 Drone 后去后台手动给用户加权限。Runner 这边最核心的是挂载了宿主机 Docker 的 socket。Drone Runner 本质上是 Docker-in-Docker 的模式它负责接收 Server 下发的任务然后通过 Docker API 动态创建容器执行流水线里的每个步骤。如果不挂载/var/run/docker.sockRunner 根本无法工作。这里存在一个安全隐患是 Runner 有了宿主机的 root 权限所以 Runner 所在的主机一般不要暴露不相关的外网服务。全部配置好之后执行docker-compose up -d等待容器启动。然后访问http://服务器IP:8800如果一切正常会跳转到 GitLab 的授权页面确认授权后就进入 Drone 的主界面。到这一步“握手”就完成了。3. 写一份能直接用的 .drone.ymlWeb 项目构建与部署配置Drone 的流水线配置是一个 YAML 文件放在项目仓库根目录文件名必须是.drone.yml。网上不少教程写的配置很抽象我这里用一个非常典型的 Node.js Nginx 的 Web 项目作为示例尽量把每个字段讲透。3.1 理解 Drone 的三种触发阶段clone、pipeline、pluginDrone 的流水线大致可以分为几个阶段。首先是 clone也就是代码检出这个阶段自动完成不需要在 .drone.yml 里显式写。然后是 pipeline这里是你自己定义的步骤比如安装依赖、跑测试、构建静态文件。最后可以挂载 pluginDrone 生态里的插件本质上是封装了特定功能的 Docker 镜像比如把构建产物传到远程服务器、发送钉钉通知等。理解这个模型很关键因为很多新人容易把 .drone.yml 写成一个普通的 Shell 脚本但实际它是声明式的步骤编排每一步对应一个独立容器容器之间默认不共享文件系统需要通过共享 volume 来传递产物。下面是一个前端项目的完整 .drone.yml 示例kind: pipeline type: docker name: frontend-build steps: - name: install-deps image: node:18-alpine pull: always volumes: - name: deps path: /drone/src/node_modules commands: - npm config set registry https://registry.npmmirror.com - npm install - name: build-dist image: node:18-alpine volumes: - name: deps path: /drone/src/node_modules commands: - npm run build - name: deploy image: appleboy/drone-scp:1.6.2 settings: host: from_secret: deploy_host username: from_secret: deploy_user password: from_secret: deploy_password port: 22 source: dist/* target: /var/www/myweb/dist - name: restart-nginx image: appleboy/drone-ssh:1.6.4 settings: host: from_secret: deploy_host username: from_secret: deploy_user password: from_secret: deploy_password port: 22 script: - cp -r /var/www/myweb/dist/* /usr/share/nginx/html/ - nginx -s reload volumes: - name: deps temp: {}3.2 一步一步解析配置里的关键信息上面这个配置虽然短但涵盖了几个核心设计逐一拆开讲。第一kind: pipeline和type: docker是固定写法表示这是一个 Docker 类型的流水线。如果是 Kubernetes 版本则用type: kubernetes但中小项目没必要。第二name仅仅是流水线名称不是项目名。在 Drone 后台看到的就是这个名字建议起得直观一点。第三我特意写了两个 Node 步骤分别是install-deps和build-dist并且用一个名为deps的临时 volume 挂载到了/drone/src/node_modules。这里解释一下Drone 默认会把代码克隆到容器的/drone/src目录然后执行commands。如果你不在多个步骤之间共享node_modules那第二个步骤又要重新npm install一遍非常浪费时间。temp: {}是声明一个临时 volume步骤开始时创建、流水线结束后自动销毁不需要手动清理。可能有读者会问为什么不在一个步骤里把 install 和 build 都做了当然可以更简单。我拆成两步是因为在真实项目中install-deps可能还需要做一些缓存处理拆开之后单步重试的时候会方便一些。对于小项目合并成一个步骤完全没问题。3.3 用 Secret 管理服务器密码不要写死在仓库里上面配置里出现了from_secret这是 Drone 的机密变量机制。很多人第一次用会问这个 deploy_password 写在哪答案不是写在 .drone.yml 里而是写在 Drone 后台的项目设置页面。操作路径进入 Drone 后台 → 找到对应项目 → Settings → Secrets → New Secret。需要把变量名和变量值都填进去。比如这里我定义了三个 SecretSecret 名称对应的值deploy_host目标服务器的 IP 或域名deploy_user部署时使用的 SSH 用户名通常是 root 或专门的应用用户deploy_password对应的 SSH 密码推荐用密钥方式下一节细说deploy_portSSH 端口如果改了默认端口需要设置这些 Secret 在流水线执行时会被注入到对应容器的环境变量或直接替换到 settings 里日志显示时会自动打码不会暴露明文。这个做法必须养成习惯严禁把运维凭证直接写在 .drone.yml 里提交到代码仓库。3.4 多环境部署怎么区分测试环境和生产环境实际项目中很少只有一个环境。我在另一套 Java Web 项目里需要同时部署到测试环境和生产环境做法是在 .drone.yml 里通过trigger按分支区分然后在 Drone 后台设置不同的 Secret。比如测试环境用develop分支生产环境用main分支steps: - name: build image: maven:3.8-openjdk-11 commands: - mvn clean package -DskipTests - name: deploy-test image: appleboy/drone-scp:1.6.2 when: branch: - develop settings: host: from_secret: test_host ... - name: deploy-prod image: appleboy/drone-scp:1.6.2 when: branch: - main settings: host: from_secret: prod_host ...when条件是 Drone 流水线里非常重要的一个关键字。除了 branch它还支持event比如 push、tag、pull_request、status比如 success、failure等条件。如果项目固定只用一条流水线建议把环境的切换逻辑尽量收敛在when里不要拆成多份 .drone.yml 文件维护起来能省不少事。4. 一次 push 从代码到上线的完整过程前几节把环境搭建和配置都讲清楚了这一节我来完整演示一次“改代码 → 推代码 → 自动部署 → 访问验证”的全过程并标注每一步在界面上应该看到什么。4.1 开发机上的标准操作流程我以一次修复前端页面 bug 为例。在开发机上的操作其实和平时没有区别# 1. 切换到功能分支 git checkout -b fix/login-bug # 2. 修改代码 vim src/pages/Login.vue # 3. 提交并推送 git add . git commit -m fix: 修复登录页在移动端样式错位 git push origin fix/login-bug关键在推送之后。如果仓库已经通过 Drone 激活GitLab 会自动向 Drone 发送一个 webhook 事件Drone 紧接着就会在 Runner 上调度一个流水线执行。注意新分支第一次推送时Drone 流水线会显示在仓库对应的 Drone 项目列表中。如果你发现推送后 Drone 没有反应最常见的两个原因一是项目没有被激活需要到 Drone 后台找到该仓库点击激活按钮二是 GitLab 侧没有自动创建 webhook需要到 GitLab 仓库的 Settings → Webhooks 里手动添加URL 格式是http://drone服务器地址/api/hookSecret 留空即可。4.2 Drone 界面上看到的流水线状态流水线启动后在 Drone 的项目主页能看到一个带状态标识的构建记录状态包括状态含义PendingRunner 正在排队创建流水线容器Running流水线正在执行Success所有步骤均成功Failure至少有一个步骤失败Canceled有人手动取消或超时中断点击某条构建记录就能看到每个步骤的实时日志。这一步非常直观如果build-dist步骤报错了点开日志会显示具体的编译错误排查思路跟本地构建完全一致。我这次演示的构建流程一共跑了四个步骤install-deps约 40 秒、build-dist约 60 秒、deployscp 复制文件约 10 秒、restart-nginx约 5 秒。从 push 到全部成功大概两分钟。如果公司网络状况好还可以更快。4.3 部署脚本在目标服务器上做了什么前面的 .drone.yml 显示了 deploy 和 restart 两个步骤但这一步真正的细节在restart-nginx的 SSH 脚本里。很多教程只是简单说“用 SSH 上传文件”但实际生产环境中还需要注意目录权限和站点配置。我的目标服务器上 Nginx 的站点配置大概是这样的server { listen 80; server_name myweb.example.com; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } }部署脚本的核心逻辑是先把 dist 目录传到/var/www/myweb/dist然后 rsync 到 Nginx 的根目录最后 reload。为什么不直接用 scp 复制到/usr/share/nginx/html这是为了避免中途写入失败导致用户访问到半成品。先传到临时目录再同步是更稳妥的做法。实际执行脚本的时候我建议把这一步从 .drone.yml 的script字段改成项目里的deploy.sh然后在流水线里执行远程bash deploy.sh#!/bin/bash set -e # 同步文件到临时目录再切到正式目录降低发布风险 rsync -av --delete /var/www/myweb/dist/ /tmp/myweb-release/ cp -r /tmp/myweb-release/* /usr/share/nginx/html/ # 检查配置之后才真正 reload nginx -t nginx -s reload echo deploy success: $(date)set -e是关键意味着任何一个命令失败脚本立刻终止避免后续命令在错误状态下继续执行。4.4 回滚怎么办一套部署流程如果没考虑回滚是走不到生产环境的。我的回滚思路是在远程服务器上保留最近几次发布的 tar 包。具体做法在 deploy.sh 里增加一层每次发布前把当前目录打成带时间戳的 tar 包存到 /backup 目录。如果线上出了问题手动在服务器执行cd /usr/share/nginx/html tar -xzf /backup/myweb-202401011200.tar.gz就能恢复。不要把这个自动化做得太复杂生产环境的回滚操作宁可慢一点也要尽量简单可靠。5. 跑了大半年之后我踩过的坑和排查思路这部分是我最想分享的因为只看官方文档基本学不到这些。每一条都是我实际遇到并解决的按“现象 → 排查过程 → 根因 → 解决方式”来写希望对你有帮助。5.1 Drone Runner 一直处于 Pending步数不执行这个现象是推送代码后Drone 创建了流水线但所有步骤一直卡在 Pending 状态。看 Runner 的日志会看到类似cannot find the docker daemon或者failed to ping docker daemon的错误。排查思路先确认 Runner 容器是否正常启动docker ps看状态再进入 Runner 容器测试 Docker 连接docker exec -it drone-runner sh ping docker其实最普遍的原因就是/var/run/docker.sock没有正确挂载或者挂载了但 Runner 容器内的用户没有权限访问 socket。解决方式就是确认挂载路径与本机 Docker socket 路径一致并确保 Runner 镜像版本与 Docker 版本兼容。还有一个相对隐蔽的问题如果 Runner 跑在 Docker 容器里而 Docker 版本和镜像不兼容会导致创建 volume 时失败同样表现为 Pending。5.2 SSH 插件连不上目标服务器报 permission denieddrone-scp或drone-ssh插件连接不上目标服务器最常见的原因是密码错误、端口错误或者目标服务器拒绝了当前来源 IP 的 SSH 连接。有一次我排查了很久最后发现是目标服务器配置了AllowUsers只允许特定用户从特定 IP 登录Runner 所在机器的 IP 不在白名单里。这类问题在插件 settings 里看日志很隐蔽只显示permission denied或handshake failed。建议一开始就用私钥认证而不是密码认证。做法是在 Drone 后台新增 Secret例如deploy_key内容填私钥文本然后把 settings 里的 password 相关配置换成 key 字段settings: host: from_secret: deploy_host username: from_secret: deploy_user key: from_secret: deploy_key使用密钥方式要确保目标服务器的~/.ssh/authorized_keys里已经放了对应的公钥并且私钥权限不能太高不过插件内部会自动处理权限。用drone-scp插件时如果私钥有 passphrase当前版本的插件是不支持交互式输入的这种情况建议直接生成一个无 passphrase 的专用部署密钥。5.3 构建步骤里 Node 版本不一致的问题这个问题特别隐蔽。如果你在 .drone.yml 里写了image: node:latest可能今天构建没问题过了几周 suddenly 构建失败因为官方 latest 镜像更新了涉及的行为发生了变化。npm install 失败、某些依赖包在新版 Node 下编译报错这类问题都属于“不可复现”的环境漂移。解决方式非常直接所有基础镜像都锁定到精确版本甚至用摘要digest锁定。比如node:18-alpine仍可能更新要想彻底锁定可以用nodesha256:...的形式但日常操作中锁到 minor 版本一般就够了。我在实际项目中统一改为node:18.20.2-alpine这种带完整补丁号的版本Stability 明显改善。还有一个相关的小坑npm install在不同时间执行会拉取到不同的依赖版本补上 package-lock.json 的提交很重要。代码仓库里如果没有 lock 文件建议立刻补一次提交。5.4 Drone 和 GitLab 版本兼容性Drone 1.x 和 GitLab 配合得较好但 Drone 2.x 对 GitLab 的版本有最低要求。如果你用的是比较旧的 GitLab 社区版很可能会出现 Drone 无法正确读取仓库列表、无法创建 webhook 等情况。我遇到过这样一个案例GitLab 是 13.xDrone Server 是 2.16结果激活项目后 webhook 一直创建不了。排查后确认是 GitLab 版本太老某些 API 字段不被识别。最终我选择把 GitLab 升级到 15.x 之后问题才消失。如果你不想升级 GitLab就建议锁定 Drone 1.x 版本这是一组经过验证的兼容组合。5.5 构建日志不实时刷新有时候流水线已经在跑了但 Drone 界面上的日志要等构建结束才一次性显示完全。这在步骤多而且耗时长的时候非常影响体验。这个问题的根源多半是 Runner 和 Server 之间的网络问题或者是 Drone Server 的DRONE_LOG_DEBUG相关配置没有打开。实际上 Drone 默认就是流式输出日志的如果出现日志延迟检查一下反向代理的缓冲配置。如果 Drone Server 前面挂了 Nginx需要在 Nginx 配置里关闭这个 location 的 proxy_bufferinglocation / { proxy_set_header Connection ; proxy_http_version 1.1; proxy_buffering off; proxy_cache off; proxy_pass http://127.0.0.1:8800; }这个问题我在部署到内网环境时遇到过改完 Nginx 配置后日志就恢复实时刷新了。5.6 流水线成功但代码没更新这是最让人头疼的情况因为所有步骤都是绿色对勾但线上访问还是旧版本。我刚开始也懵了后来才发现是deploy步骤的文件路径和restart-nginx脚本里的路径不一致。比如 scp 插件把 dist 目录传到了/var/www/myweb/dist而 Nginx 根目录是/usr/share/nginx/html如果 restart 脚本里写的是cp -r /var/www/myweb/dist/* /usr/share/nginx/html/但 dist 下还有一层子目录路径嵌套就不对了。我的建议是在部署脚本里加一行find /usr/share/nginx/html -type f | wc -l或ls -la /usr/share/nginx/html来确认文件数量把部署脚本的执行结果直接打到 SSH 步骤的日志里避免“假成功”。另外一个可能因素是浏览器/CDN 缓存。前端项目更新后浏览器可能还在用旧的 JS这不一定是部署失败。解决方式是在 Nginx 层面对带 hash 的静态资源设置长缓存对 index.html 设置 no-cache配合构建时给文件名加 hash是比较标准的方案。5.7 并发构建时互相覆盖默认情况下同一个项目的多个流水线可以并发跑但如果部署目标都是同一台服务器、同一个目录就会出现两个构建同时执行 scp 和 rsync最终部署结果混乱。解决方式有两个方向一是把 Runner 的并发数调小比如DRONE_RUNNER_CAPACITY1这样同一时间只执行一个流水线二是在流水线里增加并发控制。如果是单项目并且布署逻辑比较简单直接限定 Runner 并发数为 1 是最省心的方法。如果后续项目多了需要并行构建再考虑用更精细的队列机制。结合上面的经验我再分享一个最后的排查技巧遇到任何 Drone 相关的问题第一件事永远是看 Drone Server 和 Runner 两个容器的日志大多数问题都会在日志里留下线索。很多人在耐心排查之前就四处搜索反而效率更低。先看日志、再看配置、最后考虑版本兼容性这个顺序能解决九成以上的日常问题。
返回列表