ROS Jenkins插件与Git集成最佳实践

更新时间:
复制为 MD 格式

Alibaba Cloud ROS Plugin for Jenkins 与 Git 仓库集成,可构建代码变更驱动的基础设施自动部署流水线,实现 ROS 模板版本化管理与多环境隔离部署。

业务场景说明

ROS 模板和参数文件托管在 Git 仓库中做版本化管理。模板或参数变更后,基础设施需要同步完成创建或更新。手动登录控制台部署效率低下,且容易因操作遗漏或环境混淆引发故障。开发与生产环境的参数配置需要严格隔离,阿里云 AccessKey 等凭证不能以明文出现在代码仓库或构建日志中。

Jenkins Pipeline 监听 Git 仓库变更,自动读取对应环境的 ROS 模板和参数文件,调用 Alibaba Cloud ROS Plugin 完成资源栈创建或更新。凭证通过 Jenkins Credential Store 统一管理,全程不暴露敏感信息。代码合并后,基础设施在数分钟内自动完成变更,无需人工干预。

方案架构

整体架构图

Jenkins 通过 pollSCM('H/5 * * * *') 定时轮询 Git 仓库(也可配置 GitHub Webhook 实现实时触发),发现新提交后自动启动构建流水线。Git SSH Key 存储在 Jenkins Credential Store 中,与其他凭证隔离管理。

流水线先尝试调用 CreateRosStack 创建资源栈;若资源栈已存在,则自动切换为 UpdateRosStack 执行增量更新。不同环境通过独立的参数文件(如 parameters/dev.json)实现配置隔离,开发环境参数仅包含对应权限范围内的值。

所有 ROS 操作通过 credentialsId 参数引用 Jenkins 中预配置的阿里云凭证,AccessKey 不会出现在流水线脚本或构建日志中。

实施步骤

步骤一:组织 Git 仓库与 ROS 模板

开始之前,需要完成 ROS Jenkins 插件的安装和配置。具体操作,请参见ROS Jenkins插件工具介绍

仓库采用模板与参数分离的目录结构:

my-project/
├── templates/             # ROS 模板目录
│   └── vpc-template.json  # 基础设施定义
├── parameters/            # 环境参数隔离
│   ├── dev.json           # 开发环境参数
│   └── prod.json          # 生产环境参数(权限严格管控)
└── Jenkinsfile  

templates/*.json 文件必须包含 ROSTemplateFormatVersion 字段,禁止在模板中硬编码环境变量,所有环境差异通过参数注入。parameters/*.json 文件仅包含 ParameterValue,禁止在其中存储密码等敏感信息。

ROS 模板文件

templates/vpc-template.json 定义了包含 VPC、VSwitch 和安全组的基础网络环境。模板通过 Parameters 接收外部参数,Fn::Sub 将环境标识拼接到资源名称中,确保不同环境的资源互不冲突:

{
  "ROSTemplateFormatVersion": "2015-09-01",
  "Description": {
    "en": "ROS Template: Creating a VPC.",
    "zh-cn": "ROS模板:创建VPC。"
  },
  "Parameters": {
    "Environment": {
      "Type": "String",
      "Default": "development",
      "AllowedValues": [
        "development",
        "staging",
        "production"
      ],
      "Description": {
        "en": "Environment Type",
        "zh-cn": "环境类型"
      }
    },
    "VpcName": {
      "Type": "String",
      "Default": "my-vpc",
      "Description": {
        "en": "Vpc Name",
        "zh-cn": "VPC名称"
      }
    },
    "CidrBlock": {
      "Type": "String",
      "Default": "192.168.0.0/16",
      "Description": {
        "en": "Cidr Block",
        "zh-cn": "VPC CIDR块"
      }
    }
  },
  "Resources": {
    "VPC": {
      "Type": "ALIYUN::ECS::VPC",
      "Properties": {
        "CidrBlock": {
          "Ref": "CidrBlock"
        },
        "VpcName": {
          "Fn::Sub": "${VpcName}-${Environment}"
        },
        "Description": "VPC for ${Environment} environment"
      }
    },
    "VSwitch": {
      "Type": "ALIYUN::ECS::VSwitch",
      "Properties": {
        "VpcId": {
          "Ref": "VPC"
        },
        "CidrBlock": "192.168.1.0/24",
        "ZoneId": {
          "Fn::Select": [
            0,
            {
              "Fn::GetAZs": {
                "Ref": "ALIYUN::Region"
              }
            }
          ]
        },
        "VSwitchName": {
          "Fn::Sub": "vswitch-${Environment}"
        }
      }
    },
    "SecurityGroup": {
      "Type": "ALIYUN::ECS::SecurityGroup",
      "Properties": {
        "VpcId": {
          "Ref": "VPC"
        },
        "SecurityGroupName": {
          "Fn::Sub": "sg-${Environment}"
        },
        "Description": "Security group for ${Environment} environment"
      }
    }
  },
  "Outputs": {
    "VpcId": {
      "Value": {
        "Ref": "VPC"
      },
      "Description": {
        "en": "VPC ID.",
        "zh-cn": "创建的VPC ID。"
      }
    }
  }
}

环境参数文件

parameters/dev.json 为开发环境定义了 VPC 名称、CIDR 段和环境标识:

[
  {
    "ParameterKey": "VpcName",
    "ParameterValue": "dev-name"
  },
  {
    "ParameterKey": "CidrBlock",
    "ParameterValue": "192.168.0.0/16"
  },
  {
    "ParameterKey": "Environment",
    "ParameterValue": "development"
  }
]

步骤二:配置 Jenkins 凭证与流水线

凭证需要在创建流水线之前配置完成。流水线脚本通过凭证 ID 引用凭证,不在代码中暴露敏感信息。

配置安全凭证

Jenkins > 系统管理 > 凭据 > 系统 > 全局凭据 中配置两项凭证。Git 凭证的类型选择 SSH Username with private key,ID 设置为 git-credentials,用于 Jenkins 拉取 Git 仓库代码。阿里云凭证的类型选择 Alibaba Cloud Credentials,ID 设置为 aliyun-ak-creds,供 ROS 插件调用阿里云 API。

创建 Pipeline 任务

  1. 在 Jenkins 控制台选择 New Item,输入任务名称,选择 Pipeline 类型,单击 OK

创建Pipeline任务

  1. Pipeline 配置区域配置 Git 源代码管理,credentialsId 引用上一步创建的 Git 凭证:

checkout([
    $class: 'GitSCM',
    branches: [[name: "${params.BRANCH}"]],
    userRemoteConfigs: [[
        url: 'https://github.com/your-org/my-project.git',  // [修改] 替换为实际仓库地址
        credentialsId: 'git-creds'  // [必须] 使用 Jenkins 凭据 ID
    ]]
])

配置流水线脚本

Pipeline 区域输入流水线脚本,单击 Save 保存:

pipeline {
    agent any

    parameters {
        string(name: 'BRANCH', defaultValue: 'master', description: 'The branch to build')
        string(name: 'CREDENTIALS_ID', defaultValue: 'test123', description: 'ros ak')
        string(name: 'REGION_ID', defaultValue: 'cn-shenzhen', description: 'stack region')
        string(name: 'STACK_NAME', defaultValue: 'my-jenkins-stack', description: 'stack name')
        booleanParam(name: 'PERFORM_DEPLOY', defaultValue: false, description: 'Check if you want to deploy')
    }

    triggers {
        pollSCM('H/5 * * * *') // 每 5 分钟检查 Git 仓库变更
    }

    stages {

        stage('① Checkout: 拉取代码') {
            steps {
                checkout([
                    $class: 'GitSCM',
                    branches: [[name: "${params.BRANCH}"]],
                    userRemoteConfigs: [[
                        url: 'https://github.com/your-repo.git', // <-- [修改] 替换为实际仓库 URL
                        credentialsId: 'git-credentials' // <-- [确认] 使用已配置的 Git 凭据 ID
                    ]]
                ])
                sh 'echo "代码已从分支 $BRANCH 拉取完毕"'
            }
        }
        stage('Build') {
            steps {
                echo 'Building the application...'
            }
        }


        stage('Deploy with ROS') {
            steps {
                script {
                    def templatePath = "${WORKSPACE}/templates/vpc-template.json"
                    def templateContent = readFile(templatePath)
                    def paramsFile = "${WORKSPACE}/parameters/${env.ENVIRONMENT ?: 'dev'}.json"
                    def tplParams = readFile(paramsFile)
                    
                    echo "Using template: ${templatePath}"

                    try {
                        def stackId = CreateRosStack(
                            stackName: "${params.STACK_NAME}",
                            regionId: "${params.REGION_ID}",
                            credentialsId: "${params.CREDENTIALS_ID}",
                            parameters: tplParams,
                            templateBody: templateContent
                        )
                        echo "Created stack with ID: ${stackId}"
                        env.STACK_ID = stackId
                    } catch (Exception e) {
                        echo "Stack already exists, will attempt to update it."
                        if (e.getMessage().contains("already exists")) {
                            def stacks = ListRosStacks(
                                stackName: ["${params.STACK_NAME}"],
                                regionId: "${params.REGION_ID}",
                                credentialsId: "${params.CREDENTIALS_ID}"
                            )
                            echo "stacks: ${stacks}"
                            if (stacks && stacks.TotalCount > 0) {
                                def stackId = stacks.Stacks[0].StackId
                                try {
                                    echo "Stack ${stackId} already exists, will attempt to update it."
                                    UpdateRosStack(
                                        stackId: stackId,
                                        regionId: "${params.REGION_ID}",
                                        credentialsId: "${params.CREDENTIALS_ID}",
                                        parameters: tplParams,
                                        templateBody: templateContent
                                    )
                                    env.STACK_ID = stackId
                                } catch (Exception ex) {
                                    echo ex.getMessage()
                                }
                            }
                        } else {
                            echo "An unexpected error occurred: ${e.getMessage()}"
                            currentBuild.result = 'FAILURE'
                            throw e
                        }
                    }
                }
            }
        }

        stage('Test') {
            steps {
                echo 'Running tests...'
            }
        }
        stage('Deploy') {
            steps {
                echo 'Deploying the application...'
            }
        }
    }

    post {
        always {
            echo 'Pipeline finished.'
        }
        success {
            echo 'Pipeline succeeded!'
        }
        failure {
            echo 'Pipeline failed!'
        }
    }
}

核心逻辑在 Deploy with ROS 阶段。pollSCM 每 5 分钟轮询 Git 仓库,检测到新提交后拉取代码,读取 templates/ 下的 ROS 模板和 parameters/ 下对应环境的参数文件。部署阶段先尝试调用 CreateRosStack 创建资源栈;如果捕获到 "already exists" 异常,则通过 ListRosStacks 查询现有资源栈 ID,再调用 UpdateRosStack 执行更新。这种先创建、失败则更新的模式确保了首次部署和后续变更都能自动处理。无论更新成功或失败,后续的测试和部署阶段仍会继续执行。

步骤三:验证首次部署

  1. 在 Jenkins 任务页单击 Build with Parameters,手动触发构建。

  2. 查看构建日志,确认输出类似如下内容:

[Deploy ROS] Created stack with ID: 4ee9****-****-****-****-************
[Post-build] ✅ dev 环境部署成功!资源栈: 4ee9****-****-****-****-************
  1. ROS 控制台确认资源栈状态为 CREATE_COMPLETE

步骤四:验证 Git 变更触发自动部署

修改参数文件并推送到 Git 仓库,验证 Jenkins 能否自动检测变更并完成资源栈更新。

提交参数变更

  1. 修改 parameters/dev.json,将 VpcName 参数值改为 dev-new-name

[
  {
    "ParameterKey": "VpcName",
    "ParameterValue": "dev-new-name"
  },
  {
    "ParameterKey": "CidrBlock",
    "ParameterValue": "192.168.0.0/16"
  },
  {
    "ParameterKey": "Environment",
    "ParameterValue": "development"
  }
]
  1. 提交变更并推送到远程仓库:

git add parameters/dev.json
git commit -m "调整 VpcName 为 dev-new-name"
git push origin master

确认自动部署结果

Jenkins 每 5 分钟轮询 Git 仓库,检测到新提交后自动触发构建。流水线拉取最新代码,读取更新后的参数文件,调用 UpdateRosStack 执行资源栈更新。整个过程通常在 2 分钟内完成,资源栈状态从 UPDATE_IN_PROGRESS 变为 UPDATE_COMPLETE

ROS 控制台查看资源栈事件,确认 VPC 的 VpcName 已更新为 dev-new-name-development(模板中 Fn::Sub 自动拼接了环境后缀)。同时在 Jenkins 构建日志中搜索 Updated stack 确认更新执行成功。