实践篇:16-使用 Jenkins 进行 CI/CD

持续集成 (CI) 和持续部署 (CD) 是现代软件开发的核心实践,能够自动化构建、测试和部署流程,提高效率和质量。 Jenkins 是一个功能强大且广泛使用的开源自动化服务器,非常适合用于搭建 CI/CD 流水线。

本篇将指导你如何使用 Docker 部署 Jenkins,并配置一个基本的 Pipeline (流水线),用于自动化构建 Docker 镜像并将其推送到我们之前部署的 Harbor 仓库。

使用 Docker 部署 Jenkins

  1. 创建目录:
mkdir -p /data/jenkins/jenkins_home
cd /data/jenkins
  1. Docker Compose 配置文件 (/data/jenkins/docker-compose.yml):
services:
  jenkins:
    image: jenkins/jenkins:2.504.1-lts
    container_name: jenkins
    user: root
    privileged: true
    ports:
      - 8086:8080
      - 50000:50000
    volumes:
      - ./jenkins_home:/var/jenkins_home
      - /var/run/docker.sock:/var/run/docker.sock
      - /usr/bin/docker:/usr/bin/docker
      - /usr/libexec/docker/cli-plugins/docker-buildx:/usr/libexec/docker/cli-plugins/docker-buildx
      - /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime
    environment:
      - TZ=Asia/Shanghai
      - LANG=en_US.UTF-8
      - 'JAVA_OPTS=-Dsun.jnu.encoding=UTF-8 -Dfile.encoding=UTF-8 -Djava.awt.headless=true -Dhudson.model.DownloadService.noSignatureCheck=true -Dhudson.model.UpdateCenter.updateCenterUrl=https://cdn.jsdelivr.net/gh/lework/jenkins-update-center/updates/tencent/'
      - JENKINS_UC=https://cdn.jsdelivr.net/gh/lework/jenkins-update-center/updates/tencent
      - JENKINS_UC_DOWNLOAD=https://mirrors.tuna.tsinghua.edu.cn/jenkins/
    extra_hosts:
      - 'git.leops.local=192.168.77.140'
      - 'harbor.leops.local=192.168.77.140'
      - 'nexus.leops.local=192.168.77.140'
      - 'verdaccio.leops.local=192.168.77.140'
      - 'goproxy.leops.local=192.168.77.140'
    restart: always

说明:这里使用了自定义域名,因此需要配置 extra_hosts,否则在构建时会出现域名解析错误。如果你的环境中已配置了 DNS 解析,可以忽略此项配置。volumes 挂载中包含了 Docker 相关文件,这是为了让 Jenkins 容器能够使用宿主机的 Docker 进行构建操作(Docker out of Docker 模式)。

  1. 启动服务:
docker compose up -d  # 后台运行服务
  1. 配置 Nginx 反向代理:
    创建 Nginx 配置文件 /data/nginx/nginx_data/conf.d/jenkins.conf:
server {
    listen 80;
    server_name jenkins.leops.local;  # 服务域名,需要在DNS或hosts文件中配置

    access_log /var/log/nginx/jenkins_access.log;
    error_log /var/log/nginx/jenkins_error.log;

    location / {
        proxy_pass http://192.168.77.140:8086;  # jenkins服务地址和端口
        proxy_set_header  Host              $http_host;
        proxy_set_header  X-Real-IP         $remote_addr;
        proxy_set_header  X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header  X-Forwarded-Proto $scheme;

        # 关闭缓冲,适用于文件传输场景
        proxy_buffering off;
        proxy_request_buffering off;

        # 设置较长的超时时间,适合大文件传输
        proxy_send_timeout 900;
        proxy_read_timeout 900;
    }
}

说明:配置反向代理可以让我们通过域名访问 Jenkins,同时可以隐藏内部服务细节,提高安全性。proxy_buffering 和 proxy_request_buffering 关闭可以改善文件上传和构建过程中的性能。

创建完配置文件后,重新加载 Nginx 配置:

# 在运行 Nginx 的服务器上执行
docker exec nginx-proxy sh -c "nginx -t && nginx -s reload"

配置 Jenkins (首次启动)

  1. 访问 Jenkins: 在浏览器中打开 http://jenkins.leops.local。
  2. 解锁 Jenkins: Jenkins 首次启动需要输入初始管理员密码。根据页面提示,在运行 Jenkins 的服务器上执行以下命令获取密码:
docker compose exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword

复制输出的密码并粘贴到网页中的密码框内,点击"继续"按钮。

  1. 安装插件: 选择"安装推荐的插件"选项(Install suggested plugins)。此过程会自动安装一系列常用插件,如 Git 集成、凭证管理等。请耐心等待安装完成,这可能需要几分钟时间。

  2. 创建管理员用户: 设置你的管理员用户名、密码和邮箱等信息。请使用强密码并妥善保管这些信息。

  3. 实例配置: 确认 Jenkins URL (通常保持默认即可,确保这个 URL 能够在你的网络环境中正常访问)。

  4. Jenkins 配置完成!你将被重定向到 Jenkins 的仪表盘页面。

安装必要插件

虽然初始设置已安装了一些基础插件,但我们还需要一些用于 Docker 和 Pipeline 的核心插件。进入 “Manage Jenkins”(管理 Jenkins) -> “Plugins”(插件管理) -> “Available plugins”(可用插件),搜索并安装以下插件:

  • Docker Pipeline: 用于在 Pipeline 中与 Docker 交互,支持 Docker Agent。安装后会自动包含 Docker plugin 和 docker-commons 依赖。
  • Git: (通常已安装) 用于从 Git 仓库拉取代码。
  • Pipeline: (核心插件,通常已安装) 提供 Pipeline 流水线功能,是 Jenkins 声明式和脚本式流水线的基础。
  • Kubernetes: 如果你需要部署到 Kubernetes 环境,请安装此插件。
  • Role-based Authorization Strategy: 用于实现更细粒度的用户权限控制,适用于多团队使用同一 Jenkins 实例的场景。
  • Active Choices: 提供动态参数选择功能,可在构建参数表单中实现级联选择等高级功能。
  • AnsiColor: 支持在 Jenkins 控制台输出中显示带颜色的日志,提高日志可读性。
  • Rebuilder: 允许使用相同参数重新运行之前的构建,提高工作效率。

提示:插件安装后,系统会提示重启 Jenkins 以应用更改。点击"安装后重启 Jenkins"选项可以自动完成重启。

配置凭证

为了让 Jenkins Pipeline 能够安全地访问 Gitlab 和 Harbor,我们需要在 Jenkins 中配置访问凭证。这些凭证将用于代码拉取和镜像推送操作。

进入 “Manage Jenkins”(管理 Jenkins) -> “Credentials”(凭据) -> “System”(系统) -> “Global credentials (unrestricted)”(全局凭据) -> “Add Credentials”(添加凭据):

  1. Harbor 凭证:
    • Kind (类型): Secret text (推荐使用 Harbor Robot Account 的 Token) 或 Username with password (使用你的 Harbor 用户名密码)。
    • Secret/Password (密码): 输入你的 Harbor Token 或密码。
    • Username (用户名): 如使用密码方式,则输入你的 Harbor 用户名。
    • ID: harbor-credentials (或其他你容易识别的 ID,Pipeline 脚本中将引用这个 ID)。
    • Description (描述): (可选) 添加描述,如"用于访问 Harbor 镜像仓库的凭证"。
  2. Gitlab 凭证:
    • Kind (类型): Username with password (使用你的 Gitlab 用户名和密码或 Access Token)。
    • Username (用户名): 你的 Gitlab 用户名。
    • Password (密码): 你的 Gitlab 密码或生成的 Access Token。
    • ID: gitlab-credentials (或其他你容易识别的 ID)。
    • Description (描述): (可选) 添加描述,如"用于拉取 Gitlab 代码的凭证"。

安全提示:建议使用访问令牌(Token)而非直接使用密码,并根据最小权限原则设置令牌的权限范围。例如,Harbor 令牌只需要推送镜像的权限,Gitlab 令牌只需要读取代码的权限。

创建 Jenkins Pipeline 项目

现在我们可以创建一个 Pipeline 项目来定义 CI/CD 流程。Pipeline 是一系列的阶段和步骤,用于自动化软件交付流程。

  1. 返回 Jenkins 主页,点击 “New Item”(新建任务) 按钮。

  2. 输入项目名称(例如 deploy-demo),选择 “Pipeline”(流水线) 类型,点击 “OK”(确定) 按钮。

  3. 配置 Pipeline:

    • Description (描述): (可选) 添加项目描述,如"示例应用的 CI/CD 流水线"。
    • Pipeline -> Definition (定义): 选择 Pipeline script (直接在界面编写脚本)。

    注意:在实际项目中,更推荐使用 Pipeline script from SCM 选项,从代码库中读取 Jenkinsfile,实现代码化配置。

    • 粘贴 Pipeline 脚本: 将下面的示例脚本粘贴到 “Script” 文本框中。

声明式 Pipeline 脚本示例 (Jenkinsfile):

pipeline {
    agent any // 可以在任意可用 agent 上执行

    options {
        // 保留构建历史记录数量
        buildDiscarder logRotator(artifactDaysToKeepStr:'30', artifactNumToKeepStr:'100', daysToKeepStr:'30', numToKeepStr:'100')
        // 开启执行时间记录
        timestamps()
        // 使用 xterm ansi 颜色映射显示颜色
        ansiColor('xterm')
        // 当 master 重启后,不允许恢复流水线
        disableResume()
        // 整体构建超时
        timeout(time:120, unit:'MINUTES')
        // 安静周期
        quietPeriod(0)
    }

    // === 环境变量 ===
    environment {
        // Docker 配置
        DOCKER_BUILDKIT = 1
        DOCKER_REGISTRY = 'harbor.leops.local'
        DOCKER_REGISTRY_SCHEME = 'http://'
        DOCKER_CREDENTIAL_ID = 'harbor-credentials'// Jenkins 中配置的凭证 ID
        DOCKER_BUILD_IMAGE = ''

        // Gitlab 配置
        GITLAB_CREDENTIAL_ID = 'gitlab-credentials'// Jenkins 中配置的凭证 ID

        // app 配置
        APP_ENV = 'dev'
        APP_BUILD_TIME = ''
        APP_IMAGE = ''
        APP_IMAGE_TAG = 'latest'
        APP_DOCKERFILE_PATH = 'Dockerfile'
    }

    // === 构建参数 ===
    parameters {
        choice(name:'APP_ENV', choices: ['dev', 'test', 'prod'], description:'环境')
        string(name:'APP', defaultValue:'', description:'应用名称', trim:true)
        string(name:'APP_GIT_URL', defaultValue:'', description:'应用仓库', trim:true)
        string(name:'APP_GIT_BRANCH', defaultValue:'refs/heads/master', description:'Git 分支; 分支 refs/heads/*, Tag refs/tags/*,  commitId 提交记录 5062ac84', trim:true)
        string(name:'GUID', defaultValue:'', description:'GUID', trim:true)
    }

    // === 流水线阶段 ===
    stages {

        // 1. 初始化变量
        stage('Initialize the variables') {
            steps {
                script {
                    currentBuild.displayName = "${BUILD_NUMBER}-${params.APP_ENV}-${params.APP}"
                    currentBuild.description = "git_branch: ${params.APP_GIT_BRANCH}"

                    APP = APP.replaceAll( '_', '-').replaceAll( '\\.', '-').toLowerCase()

                    if (params.APP == '' || params.APP_GIT_URL == '' || params.APP_GIT_BRANCH == '') {
                        error('APP, APP_GIT_URL, APP_GIT_BRANCH 不能为空')
                    }

                    DOCKER_BUILD_IMAGE = ''
                }
            }
        }
        // 2. 拉取代码
        stage('Checkout') {
           when {
                expression { params.APP_GIT_URL != '' }
            }
            steps {
                retry(2) {
                    checkout([$class:'GitSCM',
                          branches:  [[name:"${params.APP_GIT_BRANCH}"]],
                          doGenerateSubmoduleConfigurations:false,
                              extensions: [
                                [
                                  $class:'SubmoduleOption',
                                  depth:1,
                                  disableSubmodules:false,
                                  parentCredentials:true,
                                  recursiveSubmodules:true,
                                  reference:'',
                                  timeout:30,
                                  shallow:true,
                                  trackingSubmodules:false
                                ],
                                [$class:'CloneOption', depth:1,shallow:true,timeout:30],
                                [$class:'LocalBranch', localBranch:'**'],
                                [$class:'RelativeTargetDirectory', relativeTargetDir:"${params.APP}"],
                              ],
                              submoduleCfg: [],
                            gitTool:'Default',
                          userRemoteConfigs: [[credentialsId: GITLAB_CREDENTIAL_ID, url: params.APP_GIT_URL]]
                        ])
                }
            }

            post {
                failure {
                    script {
                        println 'pull-code failure'
                    }
                }
            }
        }

        // 3. 构建 Docker 镜像
        stage('Build Image') {
            steps {
                dir("${params.APP}") {
                    retry(2) {
                        script {
                            APP_GIT_COMMITID = sh(label:'getBranchCommitid', returnStdout:true, script:'git rev-parse --short HEAD').trim()
                            if (env.BRANCH_NAME) {
                                APP_GIT_BRANCH = env.BRANCH_NAME
                            } else {
                                    APP_GIT_BRANCH = sh(label:'getBranchName', returnStdout:true, script:'git rev-parse --abbrev-ref HEAD').trim()
                                }

                            APP_GIT_BRANCH = APP_GIT_BRANCH.replaceAll( 'heads/', '' )
                            APP_GIT_BRANCH = APP_GIT_BRANCH.replaceAll( 'refs/tags/', '' )
                            APP_BUILD_TIME = "${new Date().format('yyyyMMddHHmm',TimeZone.getTimeZone('Asia/Shanghai'))}"

                            APP_IMAGE_TAG = APP_GIT_BRANCH + '-' + APP_GIT_COMMITID + '-' + APP_BUILD_TIME
                            APP_IMAGE = DOCKER_REGISTRY + '/' + APP_ENV + '/' +  APP.replaceAll( '_', '-').replaceAll( '\\.', '-')  + ':' + APP_IMAGE_TAG
                            echo "Build image: ${APP_IMAGE}"

                            docker.withRegistry(DOCKER_REGISTRY_SCHEME + DOCKER_REGISTRY, DOCKER_CREDENTIAL_ID) {
                                DOCKER_BUILD_IMAGE = docker.build( APP_IMAGE, "-f ${APP_DOCKERFILE_PATH} --progress=plain --build-arg APP=${APP} --build-arg APP_ENV=${APP_ENV} --build-arg GIT_BRANCH=${APP_GIT_BRANCH} --build-arg GIT_COMMIT_ID=${APP_GIT_COMMITID} --add-host goproxy.leops.local=192.168.77.140 --add-host nexus.leops.local=192.168.77.140 --add-host verdaccio.leops.local=192.168.77.140 .")
                            }
                        }
                    }
                }
            }

            post {
                failure {
                    script {
                        println 'build-image failure'
                    }
                }
            }
        }

        // 4. 推送镜像到 Harbor
        stage('Push Image') {
            steps {
                script {
                    docker.withRegistry(DOCKER_REGISTRY_SCHEME + DOCKER_REGISTRY, DOCKER_CREDENTIAL_ID) {
                        DOCKER_BUILD_IMAGE.push()
                        if (params.APP_ENV == "prod") {
                          DOCKER_BUILD_IMAGE.push('latest')
                        }
                    }
                }
            }
            post {
                failure {
                    script {
                        println 'push-image failure'
                    }
                }
            }
        }

        // 5. (可选) 部署到 Kubernetes
        // stage('Deploy to K8s') { ... }
    }

    // === Post Actions  ===
    post {
        success {
            echo 'Pipeline succeeded!'
            // 发送成功通知
        }
        failure {
            echo 'Pipeline failed!'
            // 发送失败通知
        }
    }
}

Pipeline 脚本解析:

  1. agent any:表示此流水线可以在任何可用的 Jenkins 节点上执行,不限制特定的执行环境。
  2. options 块:配置流水线的全局选项:
    • buildDiscarder:限制构建历史记录的保留数量,避免占用过多磁盘空间
    • timestamps:在日志中添加时间戳,方便跟踪执行时间
    • ansiColor:启用彩色日志输出,提高可读性
    • disableResume:指定 Jenkins 重启后不自动恢复中断的流水线
    • timeout:设置整体流水线的超时时间为 120 分钟
    • quietPeriod:设置安静期为 0,即立即执行而不等待
  3. environment 块:定义全局环境变量,包括:
    • Docker 相关配置(启用 BuildKit、仓库地址、凭证 ID 等)
    • Gitlab 凭证 ID
    • 应用相关变量(环境、构建时间、镜像信息等)
  4. parameters 块:定义可在启动构建时指定的参数:
    • 应用环境选择(dev/test/prod)
    • 应用名称、Git 仓库 URL、分支信息等
  5. stages 块:定义流水线的各个阶段:
    • 初始化阶段:设置构建显示名称、验证必要参数、处理应用名称格式化
    • 拉取代码阶段:从 Git 仓库克隆代码,使用 shallow clone 优化性能,并支持子模块
    • 构建镜像阶段:
    • 获取当前 Git 提交 ID 和分支名
    • 生成包含分支、提交 ID 和时间戳的镜像标签
    • 使用 Docker 插件构建镜像,并传入多个构建参数
    • 推送镜像阶段:将构建好的镜像推送到 Harbor 仓库,如果是生产环境则额外推送 latest 标签
  6. post 块:定义流水线完成后的操作,目前仅包含简单的成功/失败日志,可扩展为发送通知。
  7. 保存并运行 Pipeline:点击 “Save”(保存) 按钮保存配置,然后点击 “Build with Parameters”(使用参数构建),填写以下信息:

然后点击 “Build”(构建) 按钮开始执行流水线。

  • APP_ENV: 选择环境,如 “dev”
  • APP: 输入应用名称,如 “ci-demo-go”
  • APP_GIT_URL: 输入 Git 仓库地址,可以使用 https://github.com/lework/ci-demo-go.git 作为测试
  • APP_GIT_BRANCH: 输入分支名,如"refs/heads/master"
  1. 查看执行进度: 点击开始执行的构建任务,然后点击 “Console Output”(控制台输出) 查看详细的执行日志。

总结与后续

本篇我们搭建了基于 Docker 的 Jenkins 环境,并创建了一个基础的声明式 Pipeline,实现了完整的 CI 流程:从 Gitlab 拉取代码、构建 Docker 镜像、并将镜像推送到 Harbor 仓库。这个基础流程可以满足大多数小型团队的自动化构建需求。

通过 Jenkins Pipeline,你可以构建一套强大且灵活的自动化 CI/CD 流程,极大地提升团队的开发效率和软件交付能力,减少手动操作带来的错误,同时提高系统的可靠性和稳定性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

企鹅侠客

您的打赏是我创作旅程中的关键燃

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值