第一次接触Jenkins Pipeline as Code时,我也被这个看似高大上的概念唬住了。后来在实际项目中摸爬滚打才发现,它其实就是把构建流程像写代码一样管理起来。想象一下,你平时写的Python脚本可以版本控制、可以复用、可以团队协作,现在你的构建流程也能享受同等待遇了。
传统Jenkins作业有个致命伤——配置都藏在Jenkins后台,换个环境就得重新配置。而Pipeline as Code把整个CI/CD流程写进Jenkinsfile,跟着项目代码一起存到Git仓库。我去年负责的一个微服务项目,十几个服务用同一套Jenkinsfile模板,只改几个参数就能复用,团队新成员接手时直呼内行。
这里有个真实案例:我们有个Java服务要同时部署到测试环境和生产环境。传统方式得维护两个Jenkins作业,现在只需要一个Jenkinsfile,通过参数控制部署目标。有次紧急回滚,直接git revert上个版本的Jenkinsfile就搞定了,这在以前得手动操作至少半小时。
建议直接用Docker跑Jenkins,省去一堆依赖问题。这是我用了两年的启动命令:
bash复制docker run -d \
-p 8080:8080 -p 50000:50000 \
-v jenkins_home:/var/jenkins_home \
-v /var/run/docker.sock:/var/run/docker.sock \
jenkins/jenkins:lts-jdk11
注意要挂载docker.sock,这样Jenkins才能用宿主机的Docker引擎。我吃过亏,没挂这个导致镜像构建一直失败。安装完成后,记得在插件管理里装上Pipeline、Git、Docker Pipeline这些必备插件。
以典型的Spring Boot项目为例,在项目根目录创建Jenkinsfile。文件结构应该是这样的:
code复制my-service/
├── src/
├── Dockerfile
├── deploy/
│ └── k8s.yaml
└── Jenkinsfile
重点说下Jenkinsfile的版本兼容性。有次我本地Jenkins版本是2.289,而服务器是2.263,结果语法不兼容导致流水线报错。建议在文件开头声明:
groovy复制#!groovy
properties([parameters([
string(name: 'DEPLOY_ENV', defaultValue: 'dev', description: '部署环境')
])])
一个完整的Jenkinsfile通常包含这些部分:
groovy复制pipeline {
agent any // 指定执行节点
options {
timeout(time: 30, unit: 'MINUTES') // 超时设置
}
environment {
IMAGE_NAME = "myapp-${env.BUILD_NUMBER}"
}
stages {
stage('Build') {
steps {
sh 'mvn clean package'
}
}
// 更多stage...
}
post {
always {
echo '构建结束,清理工作...'
}
}
}
特别提醒:environment块里定义的变量在整个pipeline都有效。有次我在stage内部重复定义同名变量,调试了半天才发现作用域问题。
groovy复制parameters {
choice(name: 'DEPLOY_REGION', choices: ['east', 'west'], description: '部署区域')
}
groovy复制stage('Deploy') {
steps {
retry(3) {
timeout(time: 5) {
sh 'kubectl apply -f deploy/'
}
}
}
}
groovy复制stage('Test') {
parallel {
stage('Unit Test') {
steps { sh 'mvn test' }
}
stage('Integration Test') {
steps { sh 'mvn verify' }
}
}
}
以我之前负责的用户服务为例,完整Jenkinsfile长这样:
groovy复制pipeline {
agent {
label 'docker-node' // 指定有docker环境的节点
}
environment {
REGISTRY = 'registry.mycompany.com'
KUBECONFIG = '/etc/kubernetes/admin.conf'
}
stages {
stage('Checkout') {
steps {
checkout scm // 拉取代码
sh 'git log -1 --pretty=%B > commit_msg.txt'
}
}
stage('Build') {
steps {
sh 'mvn clean package -DskipTests'
script {
def pom = readMavenPom()
ARTIFACT_VERSION = pom.version
}
}
}
stage('Docker Build') {
steps {
sh """
docker build -t ${REGISTRY}/user-service:${ARTIFACT_VERSION} .
docker push ${REGISTRY}/user-service:${ARTIFACT_VERSION}
"""
}
}
stage('Deploy to Dev') {
when {
branch 'develop'
}
steps {
sh "sed -i 's/__VERSION__/${ARTIFACT_VERSION}/g' deploy/dev.yaml"
sh "kubectl apply -f deploy/dev.yaml"
}
}
}
}
生产环境部署需要更谨慎,我通常这样做:
groovy复制stage('Approve Production') {
when {
branch 'main'
}
steps {
timeout(time: 2, unit: 'HOURS') {
input message: '确认部署到生产环境?'
}
}
}
groovy复制stage('Deploy to Prod') {
when {
branch 'main'
}
steps {
sh """
kubectl apply -f deploy/prod.yaml
kubectl rollout status deployment/user-service --timeout=300s
"""
}
}
当你有多个相似项目时,一定要用共享库。在Jenkins管理界面配置共享库路径后,Jenkinsfile可以简化为:
groovy复制@Library('my-shared-lib@v1') _
pipeline {
agent any
stages {
stage('Build') {
steps {
buildMaven()
}
}
stage('Deploy') {
steps {
deployToK8s('user-service')
}
}
}
}
共享库里的方法定义示例:
groovy复制// vars/buildMaven.groovy
def call() {
sh 'mvn clean package'
archiveArtifacts 'target/*.jar'
}
groovy复制stage('Build') {
steps {
sh 'mvn -Dmaven.repo.local=.m2/repository clean package'
stash includes: '.m2/repository/**', name: 'm2cache'
}
}
dockerfile复制FROM maven:3.8-jdk-11 as builder
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src/ src/
RUN mvn package
FROM openjdk:11-jre
COPY --from=builder target/*.jar app.jar
groovy复制agent {
docker {
image 'maven:3.8-jdk-11'
args '-v $HOME/.m2:/root/.m2 -m 2g --cpus 1'
}
}
我习惯用Slack通知构建状态:
groovy复制post {
failure {
slackSend channel: '#ci-alerts',
color: 'danger',
message: "构建失败: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
}
success {
slackSend channel: '#ci-alerts',
color: 'good',
message: "构建成功: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
}
}
groovy复制post {
always {
archiveArtifacts artifacts: '**/target/surefire-reports/**/*.xml', allowEmptyArchive: true
junit '**/target/surefire-reports/**/*.xml'
}
}
groovy复制stage('Logging') {
steps {
sh """
docker run --rm \
-v ${WORKSPACE}/target:/logs \
elastic/filebeat:7.14.0 \
filebeat -e -E output.elasticsearch.hosts=["elk-server:9200"]
"""
}
}
千万不要把密码硬编码在Jenkinsfile里!用Jenkins的凭据管理:
groovy复制environment {
DOCKER_CREDS = credentials('docker-hub-account')
}
steps {
sh """
docker login -u ${DOCKER_CREDS_USR} -p ${DOCKER_CREDS_PSW}
"""
}
在流水线中加入安全检查:
groovy复制stage('Security Scan') {
steps {
sh 'mvn org.owasp:dependency-check-maven:check'
dependencyCheckPublisher pattern: '**/dependency-check-report.xml'
}
}
对于Docker镜像:
groovy复制stage('Image Scan') {
steps {
sh 'docker scan --file Dockerfile --exclude-base .'
}
}