Drone CI/CD流水线实战:从Jenkins迁移到10秒构建
1. 真实场景:我被Jenkins困住了
2023年接手一个微服务项目,22个服务全部跑在Jenkins上。构建一次平均5分钟,并发只能开5个Agent,每天早上10点大家排队等构建。配置文件长达800行,全是freestyle job,维护一次要改20个地方。最痛苦的是每次修改Jenkinsfile必须重启Master。
老板说:「优化一下,构建时间压到1分钟内。」我翻了翻Jenkins的插件列表,决定换掉它。调研后选择了Drone —— 一个基于容器、轻量级、与GitLab原生集成的CI/CD系统。下面是我的迁移全记录。
2. 方案对比:Jenkins vs Drone
| 维度 | Jenkins (v2.387.1) | Drone (v2.23.3) |
|---|---|---|
| 安装方式 | WAR包或Docker,配置复杂 | Docker单容器启动,3条命令 |
| 流水线定义 | Jenkinsfile (Groovy) 或 Freestyle UI | .drone.yml (YAML) 纯声明式 |
| 构建环境 | Agent节点,需预装JDK/Maven等 | 每个step独立容器,镜像自包含 |
| 并发扩展 | 通过增加Agent节点,资源隔离差 | 通过增加Runner容器,天然隔离 |
| 缓存机制 | 需插件或手动配置 | 内置volume缓存,支持S3/MinIO |
| 与GitLab集成 | 需单独安装插件,webhook配置繁琐 | 原生OAuth,一键挂接 |
| 资源占用 | Master 2C4G,每Agent 1C2G | Server 0.5C1G,Runner 0.2C512M |
| 构建耗时(示例Go项目) | 4分30秒(无缓存) | 1分10秒(含缓存) |
结论:Drone在轻量、声明式、容器化方面完胜。但Drone的社区插件质量参差不齐,部分定制化需求不如Jenkins灵活。如果你的团队全是Groovy高手且已深度绑定Jenkins,迁移成本需要考虑。但对于新项目或想拥抱容器化的团队,Drone是更好的选择。
3. 完整实现:从零搭建Drone并配置流水线
3.1 安装Drone Server + Runner(Docker Compose版)
环境:Ubuntu 22.04,Docker 24.0.5,Docker Compose 2.20.0。
# docker-compose.yml
version: '3.8'
services:
drone-server:
image: drone/drone:2.23.3
container_name: drone-server
ports:
- "8080:80"
- "443:443"
volumes:
- ./data:/data
- /var/run/docker.sock:/var/run/docker.sock
environment:
- DRONE_GITLAB_SERVER=https://gitlab.yourcompany.com
- DRONE_GITLAB_CLIENT_ID=your_client_id
- DRONE_GITLAB_CLIENT_SECRET=your_client_secret
- DRONE_RPC_SECRET=your_shared_secret # 与runner通信密钥
- DRONE_SERVER_HOST=drone.yourcompany.com
- DRONE_SERVER_PROTO=https
- DRONE_LOGS_TRACE=true # 调试时开启,生产关掉
restart: always
drone-runner:
image: drone/drone-runner-docker:1.8.5
container_name: drone-runner
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- DRONE_RPC_PROTO=https
- DRONE_RPC_HOST=drone.yourcompany.com
- DRONE_RPC_SECRET=your_shared_secret
- DRONE_RUNNER_NAME=runner-1
- DRONE_RUNNER_CAPACITY=5 # 最大并发step数
- DRONE_RUNNER_VOLUMES=/tmp/cache:/tmp/cache # 共享缓存目录
- DRONE_LIMIT_MEMORY=1024m # 每个step最大内存
- DRONE_LIMIT_CPUS=0.5 # 每个step最大CPU核数
restart: always
启动:docker compose up -d。然后在GitLab中创建一个OAuth应用,回调地址填https://drone.yourcompany.com/login。第一次访问Drone用GitLab账号登录,自动同步仓库。
3.2 Go微服务项目的流水线配置
项目结构:services/user-service,使用Go 1.21,依赖Redis、PostgreSQL(测试用testcontainers)。我们需要完成:lint → test → build → push镜像 → 部署到K8s。
# .drone.yml (位于 user-service 根目录)
kind: pipeline
type: docker
name: user-service
# 全局环境变量
environment:
GO_VERSION: "1.21.5"
DOCKER_REGISTRY: registry.yourcompany.com
DOCKER_IMAGE: ${DOCKER_REGISTRY}/user-service
DOCKER_TAG: ${DRONE_COMMIT_SHA:0:7}
# 缓存配置(共享volume)
volumes:
- name: gomod
host:
path: /tmp/cache/gomod
- name: build
host:
path: /tmp/cache/go-build
steps:
- name: lint
image: golang:${GO_VERSION}
pull: if-not-exists
volumes:
- name: gomod
path: /go/pkg/mod
- name: build
path: /root/.cache/go-build
commands:
- go vet ./...
- go install honnef.co/go/tools/cmd/staticcheck@latest
- staticcheck ./...
when:
event: [push, pull_request]
- name: test
image: golang:${GO_VERSION}
pull: if-not-exists
volumes:
- name: gomod
path: /go/pkg/mod
- name: build
path: /root/.cache/go-build
environment:
POSTGRES_URL: postgres://test:test@postgres:5432/test?sslmode=disable
REDIS_URL: redis://redis:6379/0
commands:
# 启动测试容器(使用testcontainers-go)
- go test -v -count=1 -race -timeout 120s ./...
when:
event: [push, pull_request]
- name: build
image: golang:${GO_VERSION}
volumes:
- name: gomod
path: /go/pkg/mod
- name: build
path: /root/.cache/go-build
commands:
- CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/user-service ./cmd/
- ls -lh bin/
when:
event: [push, tag]
- name: docker
image: plugins/docker:20.13.0
volumes:
- name: docker-sock
path: /var/run/docker.sock
settings:
registry: ${DOCKER_REGISTRY}
repo: ${DOCKER_IMAGE}
tags:
- ${DOCKER_TAG}
- latest
dockerfile: Dockerfile
build_args:
- GO_VERSION=${GO_VERSION}
cache_from: "${DOCKER_IMAGE}:latest"
insecure: false
when:
event: [push, tag]
- name: deploy
image: alpine/helm:3.12.0
commands:
- apk add --no-cache kubectl curl
- curl -LO "https://dl.k8s.io/release/v1.28.0/bin/linux/amd64/kubectl"
- chmod +x kubectl && mv kubectl /usr/local/bin/
- kubectl config use-context prod
- helm upgrade --install user-service ./helm \
--set image.repository=${DOCKER_IMAGE} \
--set image.tag=${DOCKER_TAG} \
--namespace production
environment:
KUBECONFIG: /drone/src/.kube/config # 通过Secret注入
when:
event: [push, tag]
branch: main
# 服务定义(用于测试步骤中的依赖)
services:
- name: postgres
image: postgres:16-alpine
environment:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: test
- name: redis
image: redis:7-alpine
# 触发条件
trigger:
branch:
- main
- develop
- feature/*
关键点说明:
- 使用
volumes挂载宿主目录作为Go module和build缓存,第二次构建时无需重新下载依赖,大幅提速。 - 服务
postgres和redis仅用于test步骤,Drone会自动在Runner网络上启动它们,测试代码连接服务名即可。 - docker步骤使用官方
plugins/docker插件,支持缓存层,但要注意Dockerfile中必须显式使用--cache-from参数,这里通过settings.cache_from指定。 - deploy步骤中直接使用kubectl和helm,Kubeconfig文件通过Drone Secret注入(在UI中创建),路径固定为
/drone/src/.kube/config。
3.3 Node.js前端项目的流水线配置
另一个项目是React前端(Node 18,yarn 3)。核心步骤:install → lint → test → build → 上传CDN。
kind: pipeline
type: docker
name: frontend-app
environment:
NODE_VERSION: "18.18.0"
CDN_BUCKET: my-bucket
CDN_REGION: cn-north-1
volumes:
- name: yarn-cache
host:
path: /tmp/cache/yarn-cache
- name: node-modules
host:
path: /tmp/cache/node-modules
- name: build-output
temp: {} # 临时空卷,每个步运行一次
steps:
- name: install
image: node:${NODE_VERSION}
commands:
- yarn install --frozen-lockfile
volumes:
- name: yarn-cache
path: /home/node/.yarn/berry/cache
- name: node-modules
path: /app/node_modules
when:
event: [push, pull_request]
- name: lint
image: node:${NODE_VERSION}
commands:
- yarn lint
volumes:
- name: node-modules
path: /app/node_modules
depends_on: [install]
- name: test
image: node:${NODE_VERSION}
commands:
- yarn test --coverage
volumes:
- name: node-modules
path: /app/node_modules
depends_on: [install]
- name: build
image: node:${NODE_VERSION}
commands:
- yarn build
volumes:
- name: node-modules
path: /app/node_modules
- name: build-output
path: /app/build
depends_on: [install]
- name: upload
image: amazon/aws-cli:2.13.0
commands:
- aws s3 sync /app/build s3://${CDN_BUCKET}/frontend/${DRONE_COMMIT_SHA:0:7}/ --region ${CDN_REGION} --delete
volumes:
- name: build-output
path: /app/build
environment:
AWS_ACCESS_KEY_ID:
from_secret: aws_access_key_id
AWS_SECRET_ACCESS_KEY:
from_secret: aws_secret_access_key
depends_on: [build]
when:
event: [push, tag]
branch: main
注意:这里使用了temp: {}创建了一个临时卷,只在当前pipeline内有效,用于在build和upload之间传递产物。yarn缓存和node_modules目录挂载宿主目录,避免每次重装。
4. 效果数据:迁移前后对比
我们选取了三个典型项目进行压测,分别在Jenkins(5个Agent,每个2C4G)和Drone(单Runner,并发5个step,每个step分配1C2G)上运行相同的流水线。测试环境:同机房内网,GitLab为同一实例,包含Redis缓存。结果取10次平均值。
| 项目类型 | Jenkins平均耗时 | Drone平均耗时 | 加速比 | 备注 |
|---|---|---|---|---|
| Go微服务 (user-service) | 4分30秒 | 1分10秒 | 3.8x | Drone缓存了go module和build cache |
| Node前端 (frontend-app) | 3分20秒 | 1分02秒 | 3.2x | yarn cache命中,跳过install |
| Python爬虫 (spider, pip + pytest) | 2分50秒 | 45秒 | 3.8x | pip cache+多阶段构建 |
此外,Drone的并发能力明显更强。Jenkins在5个Agent上最多同时跑5个job,而Drone单Runner配置DRONE_RUNNER_CAPACITY=5可以同时运行5个step,但不同pipeline之间无上限(取决于机器资源)。实际生产中发现,一个4C8G的节点跑Drone Runner可以同时处理10-15个step(大部分step是轻量命令),而Jenkins的Agent容易被大型构建任务打满。
5. 避坑指南(亲测踩过的)
5.1 Runner内存限制导致OOM被杀死
踩坑:某次前端项目yarn build占用内存过多,Runner容器被Docker OOM Killer杀死。查看日志发现exit code 137。解决方案:在Runner环境变量中设置DRONE_LIMIT_MEMORY=2048m(默认512m)。同时可以在.drone.yml中为特定step指定资源限制:
- name: build
image: node:18
resources:
memory: 2048
cpu: 1
commands:
- yarn build
注意:resources字段在Drone 2.0+才支持,旧版本需要全局限制。
5.2 分支触发条件写错,生产环境被PR构建覆盖
踩坑:有个同事在feature分支推送后,触发了deploy步骤(因为trigger里写了event: [push])。结果feature分支的镜像覆盖了生产环境的latest标签。修复方法:在deploy步骤增加when.branch: main,同时在全局trigger中限制主要分支。另外,不要使用latest标签作为生产版本,改用Git commit SHA。
5.3 Docker构建缓存不生效,每次都全量构建
踩坑:使用了plugins/docker但每次构建都从头开始,耗时3分钟。查文档发现cache_from需要配合Dockerfile的多阶段构建和--cache-from参数。正确的写法:在Dockerfile中这样写:
## Dockerfile (Go项目)
FROM golang:1.21.5 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download # 这一层会被缓存
COPY . .
RUN CGO_ENABLED=0 go build -o bin/user-service ./cmd/
FROM alpine:3.19
COPY --from=builder /app/bin/user-service /user-service
EXPOSE 8080
CMD ["/user-service"]
同时在.drone.yml的docker步骤中设置cache_from: "${DOCKER_IMAGE}:latest"。但要注意:如果latest镜像已经不存在或者版本相差太大,缓存会miss。更稳妥的做法:同时缓存上一次的构建层,使用--cache-from指定多个镜像:
settings:
cache_from:
- "${DOCKER_IMAGE}:${DOCKER_TAG}"
- "${DOCKER_IMAGE}:latest"
build_args:
- BUILDKIT_INLINE_CACHE=1
5.4 Secret管理:不要明文写在YAML中
踩坑:有人直接把数据库密码写在.drone.yml的环境变量里,然后上传到GitLab。后果是所有人都能看到。解决方案:在Drone UI中创建Secret(Setting → Secrets),然后在YAML中引用:
environment:
DB_PASSWORD:
from_secret: db_password
注意:Secret的值在UI中设置后不可再查看,可以更新。另外,Secret仅对指定的仓库和步骤可见,根步骤(如deploy)可以使用全部secret。
5.5 中文路径/文件导致构建失败
踩坑:项目中有个中文名字的目录资源文件,在Drone容器内挂载时路径乱码。解决方案:所有文件和目录名必须使用ASCII字符。如果必须用中文,确保宿主机的locale为en_US.UTF-8且Docker版本支持。
5.6 网络问题:克隆超时
踩坑:公司内网GitLab SSL证书自签,Drone克隆仓库报certificate signed by unknown authority。解决:在Server环境变量中添加DRONE_GITLAB_SKIP_VERIFY=false(不要跳过验证),或者将CA证书放到容器内。更推荐做法:在Runner中挂载自定义CA:
# drone-runner volumes
volumes:
- /etc/ssl/certs/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt:ro
6. 总结
Drone用得好,构建效率翻倍。本文给出了适用于Go和Node项目的完整流水线配置,覆盖了缓存、测试、多阶段构建、部署等环节。核心要点:
- 合理使用volume挂载缓存,加速依赖安装和Go build。
- 精准控制触发条件,避免误部署。
- 利用Docker多阶段构建和缓存层,缩小镜像体积。
- 注意Runner资源限制,避免OOM。
如果你还在用Jenkins抱怨构建慢,不妨花一天时间试试Drone。按照本文的配置,你也能在10分钟内跑通第一个流水线。