用 Jenkins 实现自动化 CI/CD

场景设定(可替换为你的技术栈)

  • 语言:Node.js(示例简单,任何语言思路一致)
  • 制品:Docker 镜像(推送到 Docker Hub/Harbor)
  • 运行环境:Kubernetes(staging + prod 两个命名空间)
  • 流程:Push 触发 → 单元测试 → Lint → 构建镜像 → Trivy 安全扫描 → 推送镜像 → Helm 部署到 Staging → 人工审批 → 部署到 Prod → 通知

1. 环境准备

1.1 服务器要求

  • 一台 Linux 主机(Ubuntu 22.04 例子)
  • 已安装:docker、docker compose、kubectl、helm(Jenkins 也会用到)
  • 有一个可访问的镜像仓库(Docker Hub/Harbor)
  • 有可访问的 K8s 集群(含 staging 与 prod 两个 namespace)

# Docker / Compose(示例)

sudo apt-get update && sudo apt-get install -y ca-certificates curl gnupg

# …(略:Docker 官方安装步骤)

docker –version

docker compose version

1.2 用 Docker Compose 启动 Jenkins(含 Docker 构建能力)

这种方式简单、隔离好、可快速恢复。

docker-compose.yml

services:

jenkins:

image: jenkins/jenkins:lts-jdk17

container_name: jenkins

user: root

ports:

– “8080:8080”

– “50000:50000” # JNLP agent 端口

volumes:

– jenkins_home:/var/jenkins_home


/var/run/docker.sock:/var/run/docker.sock # 让 Jenkins 内能使用宿主的 docker

– /usr/local/bin/kubectl:/usr/local/bin/kubectl

– /usr/local/bin/helm:/usr/local/bin/helm

environment:

– JAVA_OPTS=-Dorg.jenkinsci.plugins.durabletask.BourneShellScript.LAUNCH_DIAGNOSTICS=true

volumes:

jenkins_home:

docker compose up -d

docker logs -f jenkins # 取初始管理员密码

# 初始密码文件路径(容器内):

# /var/jenkins_home/secrets/initialAdminPassword

1.3 Jenkins 插件提议安装

  • Pipeline, Git, Blue Ocean, Credentials Binding
  • Docker Pipeline, Docker, Kubernetes, Kubernetes CLI
  • Pipeline: Stage View, Timestamper
  • Warnings NG(代码质量)
  • Generic Webhook Trigger(或使用 GitHub 插件)
  • Slack(或企业微信/钉钉通知插件)

1.4 Jenkins 凭据准备(Credentials)

在 Manage Jenkins → Credentials 新建:

  • dockerhub-cred:类型 Username/Password,用于 docker login
  • kubeconfig:Secret file(上传 kubeconfig)
  • slack-webhook:Secret text(可选)
  • git-ssh:SSH Username with private key(如果仓库用 SSH)

2. 示例项目源码

2.1 目录结构

demo-ci-cd/

├─ app/

│ ├─ app.js

│ ├─ package.json

│ └─ __tests__/app.test.js

├─ Dockerfile

├─ helm/

│ └─ demo/

│ ├─ Chart.yaml

│ ├─ values.yaml

│ └─ templates/

│ ├─ deployment.yaml

│ ├─ service.yaml

│ └─ ingress.yaml # 可选

└─ Jenkinsfile

2.2 Node.js 应用与测试

app/app.js

const express = require(‘express’);

const app = express();

app.get(‘/’, (_, res) => res.send(‘Hello CI/CD v1’));

app.get(‘/healthz’, (_, res) => res.status(200).send(‘ok’));

const port = process.env.PORT || 3000;

if (require.main === module) {

app.listen(port, () => console.log(`Server on :${port}`));

}

module.exports = app;

app/package.json

{

“name”: “demo-ci-cd”,

“version”: “1.0.0”,

“main”: “app.js”,

“scripts”: {

“start”: “node app.js”,

“test”: “jest –runInBand”,

“lint”: “npx eslint . || true”

},

“dependencies”: {

“express”: “^4.19.2”

},

“devDependencies”: {

“jest”: “^29.7.0”,

“supertest”: “^6.3.3”,

“eslint”: “^9.8.0”

}

}

app/tests/app.test.js

const request = require(‘supertest’);

const app = require(‘../app’);

describe(‘GET /’, () => {

it(‘returns hello’, async () => {

const res = await request(app).get(‘/’);

expect(res.statusCode).toBe(200);

expect(res.text).toMatch(/Hello CI/CD/);

});

});

describe(‘GET /healthz’, () => {

it(‘returns ok’, async () => {

const res = await request(app).get(‘/healthz’);

expect(res.statusCode).toBe(200);

});

});

2.3 Dockerfile

FROM node:20-alpine AS deps

WORKDIR /app

COPY app/package*.json ./

RUN npm ci –only=production

FROM node:20-alpine

WORKDIR /app

ENV NODE_ENV=production

COPY –from=deps /app/node_modules ./node_modules

COPY app/ .

EXPOSE 3000

HEALTHCHECK –interval=30s –timeout=3s CMD wget -qO- http://localhost:3000/healthz || exit 1

CMD [“node”, “app.js”]

2.4 Helm Chart(K8s 部署)

helm/demo/Chart.yaml

apiVersion: v2

name: demo

version: 0.1.0

appVersion: “1.0.0”

helm/demo/values.yaml

image:

repository: your-dockerhub-username/demo-ci-cd

tag: “latest”

pullPolicy: IfNotPresent

replicaCount: 2

service:

type: ClusterIP

port: 80

targetPort: 3000

ingress:

enabled: false

className: “”

hosts:

– host: demo.example.com

paths:

– path: /

pathType: Prefix

resources: {}

helm/demo/templates/deployment.yaml

apiVersion: apps/v1

kind: Deployment

metadata:

name: {{ include “demo.fullname” . }}

labels: { app: {{ include “demo.name” . }} }

spec:

replicas: {{ .Values.replicaCount }}

selector:

matchLabels: { app: {{ include “demo.name” . }} }

template:

metadata:

labels: { app: {{ include “demo.name” . }} }

spec:

containers:

– name: app

image: “{{ .Values.image.repository }}:{{ .Values.image.tag }}”

imagePullPolicy: {{ .Values.image.pullPolicy }}

ports:

– containerPort: 3000

readinessProbe:

httpGet: { path: /healthz, port: 3000 }

initialDelaySeconds: 3

livenessProbe:

httpGet: { path: /healthz, port: 3000 }

initialDelaySeconds: 10

helm/demo/templates/service.yaml

apiVersion: v1

kind: Service

metadata:

name: {{ include “demo.fullname” . }}

spec:

type: {{ .Values.service.type }}

selector: { app: {{ include “demo.name” . }} }

ports:

– port: {{ .Values.service.port }}

targetPort: {{ .Values.service.targetPort }}


helm/demo/templates/ingress.yaml(可选)

{{- if .Values.ingress.enabled }}

apiVersion: networking.k8s.io/v1

kind: Ingress

metadata:

name: {{ include “demo.fullname” . }}

spec:

ingressClassName: {{ .Values.ingress.className }}

rules:

{{- range .Values.ingress.hosts }}

– host: {{ .host }}

http:

paths:

{{- range .paths }}

– path: {{ .path }}

pathType: {{ .pathType }}

backend:

service:

name: {{ include “demo.fullname” $ }}

port: { number: {{ $.Values.service.port }} }

{{- end }}

{{- end }}

{{- end }}

3. Jenkins Pipeline(Jenkinsfile)

Jenkinsfile(Declarative,含并行测试、Trivy 扫描、Helm 部署与人工审批)

pipeline {

agent any

options {

timestamps()

ansiColor(‘xterm’)

buildDiscarder(logRotator(numToKeepStr: ’30’))

}

environment {

REGISTRY = ‘docker.io’

IMAGE = ‘
your-dockerhub-username/demo-ci-cd’ // ← 改成你的仓库

COMMIT = sh(returnStdout: true, script: ‘git rev-parse –short HEAD’).trim()

DOCKER_CREDS = ‘dockerhub-cred’

KUBECONFIG_CRED = ‘kubeconfig’

CHART_DIR = ‘helm/demo’

APP_NS_STG = ‘staging’

APP_NS_PROD = ‘prod’

TRIVY_CACHE_DIR = “${WORKSPACE}/.trivycache”

}

stages {

stage(‘Checkout’) {

steps {

checkout scm

sh ‘node -v || true’

}

}

stage(‘Install deps & Lint & Test’) {

parallel {

stage(‘Install deps’) {

steps { sh ‘cd app && npm ci’ }

}

stage(‘Lint’) {

steps { sh ‘cd app && npm run lint’ }

}

stage(‘Unit Test’) {

steps { sh ‘cd app && npm test — –ci –reporters=default’ }

post { always { junit ‘**/junit*.xml’ /* 如果用 jest-junit 可生成 */ } }

}

}

}

stage(‘Build Docker Image’) {

steps {

script {

docker.withServer(‘unix:///var/run/docker.sock’) {

withCredentials([usernamePassword(credentialsId: DOCKER_CREDS, usernameVariable: ‘DOCKER_USER’, passwordVariable: ‘DOCKER_PASS’)]) {

sh ”’

echo “$DOCKER_PASS” | docker login -u “$DOCKER_USER” –password-stdin

docker build -t $IMAGE:$COMMIT -t $IMAGE:latest .

”’

}

}

}

}

}

stage(‘Security Scan (Trivy)’) {

steps {

sh ”’

mkdir -p “$TRIVY_CACHE_DIR”

docker run –rm -v /var/run/docker.sock:/var/run/docker.sock -v “$TRIVY_CACHE_DIR”:/root/.cache/ aquasec/trivy:0.54.1 image –exit-code 0 –severity HIGH,CRITICAL “$IMAGE:$COMMIT”

”’

}

}

stage(‘Push Image’) {

steps {

withCredentials([usernamePassword(credentialsId: DOCKER_CREDS, usernameVariable: ‘DOCKER_USER’, passwordVariable: ‘DOCKER_PASS’)]) {

sh ”’

echo “$DOCKER_PASS” | docker login -u “$DOCKER_USER” –password-stdin

docker push $IMAGE:$COMMIT

docker push $IMAGE:latest

”’

}

}

}

stage(‘Deploy to Staging’) {

steps {

withCredentials([file(credentialsId: KUBECONFIG_CRED, variable: ‘KUBECONFIG_FILE’)]) {

sh ”’

export KUBECONFIG=”$KUBECONFIG_FILE”

helm upgrade –install demo-stg “$CHART_DIR”

–namespace “$APP_NS_STG” –create-namespace

–set image.repository=$IMAGE –set image.tag=$COMMIT

kubectl -n “$APP_NS_STG” rollout status deploy/demo

kubectl -n “$APP_NS_STG” get svc,po -o wide

”’

}

}

}

stage(‘Manual Approval for Prod’) {

steps {

timeout(time: 30, unit: ‘MINUTES’) {

input message: “是否发布到生产?镜像: $IMAGE:$COMMIT”, ok: ‘发布’

}

}

}

stage(‘Deploy to Prod’) {

steps {

withCredentials([file(credentialsId: KUBECONFIG_CRED, variable: ‘KUBECONFIG_FILE’)]) {

sh ”’

export KUBECONFIG=”$KUBECONFIG_FILE”

helm upgrade –install demo-prod “$CHART_DIR”

–namespace “$APP_NS_PROD” –create-namespace

–set image.repository=$IMAGE –set image.tag=$COMMIT

–set replicaCount=3

kubectl -n “$APP_NS_PROD” rollout status deploy/demo

”’

}

}

}

}

post {

success {

echo “✅ 成功发布:$IMAGE:$COMMIT”

// 可接入 Slack/企业微信

// slackSend channel: ‘#devops’, color: ‘good’, message: “Build ${env.BUILD_URL} success: $IMAGE:$COMMIT”

}

failure {

echo “❌ 失败,触发回滚提示”

}

}

}

如果你的 Jenkins 控制节点无法使用 Docker,可在 agent 节点安装 Docker 并用 agent { label ‘docker’ } 指定标签。

4. 创建 Jenkins 任务并联动 Git

4.1 创建 Multibranch Pipeline(推荐)

  • New Item → Multibranch Pipeline
  • Branch Sources 选择你的 Git(GitHub/GitLab),配置凭据
  • Build Configuration:按仓库根目录的 Jenkinsfile
  • 开启 Webhook(GitHub/GitLab):Push/PR 自动触发

4.2 单分支 Pipeline(简化)

  • New Item → Pipeline
  • Pipeline script from SCM,指向仓库和分支

5. 运行与验证

  1. 首次运行将拉依赖、构建镜像、Trivy 扫描、推送、Helm 部署到 staging。
  2. 打开 kubectl get svc -n staging 查看服务 IP/Ingress;访问 / 应返回 “Hello CI/CD v1”。
  3. 在 Jenkins 审批步骤点“发布”,流水线继续将生产环境 prod 升级。
  4. 灰度/回滚:使用 Helm 历史与回滚命令:

# 查看历史

helm history demo-prod -n prod

# 回滚到上一个版本

helm rollback demo-prod 1 -n prod

6. 常见扩展与优化

  • 并行化:更多语言的测试矩阵(Node 18/20、不同 OS)
  • 缓存:Docker BuildKit + 多阶段 + npm ci 缓存
  • 质量门禁:接入 SonarQube(withSonarQubeEnv)
  • 镜像瘦身:使用 distroless/alpine,锁定 CVE
  • 蓝绿/金丝雀:在 Helm 值里加 label/selector,或用 Istio/NGINX Ingress 分权重
  • 自动回滚:结合 Prometheus 监控、错误率门限 + Jenkins 失败后脚本
  • 密钥管理:K8s Secret/External Secrets/HashiCorp Vault

7. 一键初始化脚本(可选)

scripts/bootstrap.sh(在 Jenkins 容器里执行,装 Node/Trivy CLI 等)

#!/usr/bin/env bash

set -euo pipefail

# Node & npm 缓存目录(可选)

mkdir -p /var/jenkins_home/.npm

npm config set cache /var/jenkins_home/.npm –global || true

# 安装 trivy CLI 到宿主/容器(也可直接用 docker 运行 trivy)

which trivy || {

echo “Use docker run aquasec/trivy … in Jenkinsfile”

}

echo “Bootstrap done.”

8. 你可以直接用的命令清单(运维侧)

# 手动部署到 staging(本地验证)

helm upgrade –install demo-stg helm/demo -n staging –create-namespace

–set image.repository=your-dockerhub-username/demo-ci-cd –set image.tag=latest

# 查看部署状态

kubectl -n staging get deploy,po,svc

# 查看 Pod 日志

kubectl -n staging logs -f deploy/demo

# 检查服务可达性

kubectl -n staging port-forward svc/demo 8080:80

curl http://localhost:8080/

结语

你目前拿到了一整套可运行的 Jenkins CI/CD 工程模板:

  • Jenkins 容器化部署
  • 完整 Jenkinsfile 流程(测试/扫描/打包/推送/Helm 部署/审批/发布)
  • 示例应用与 Helm Chart
© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
none
暂无评论...