作为 Jenkins 构建过程的一部分,如何跨多个集群创建和修改 Kubernetes 资源?

How to create and modify Kubernetes resources across multiple clusters as part of a Jenkins build process?

具体来说,我想在现有 Kubernetes 集群中修改一些资源(目前用 YAML 定义)作为构建过程的一部分,还有一些我想从头开始创建。在每种情况下,我都想在多个区域执行此操作以保持所有区域同步。

有问题的资源是 Agones fleets,看起来像这样(实际值已删除,但具有代表性):

apiVersion: agones.dev/v1
kind: Fleet
metadata:
  annotations:
    agones.dev/sdk-version: 1.11.0
  name: test
  namespace: game-servers
  resourceVersion: "12324578"
  selfLink: /apis/agones.dev/v1/namespaces/game-servers/fleets/test
spec:
  replicas: 1
  scheduling: Packed
  strategy:
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 25%
    type: RollingUpdate
  template:
    metadata:
      creationTimestamp: null
      labels:
        branch: test
        git_commit: 1b12371827fdea31231230901876ffe
    spec:
      health:
        disabled: false
        failureThreshold: 5
        initialDelaySeconds: 10
        periodSeconds: 5
      ports:
      - containerPort: 1234
        name: default
        portPolicy: Dynamic
        protocol: UDP
      sdkServer:
        logLevel: Info
      template:
        metadata:
          creationTimestamp: null
          labels:
            role: game-servers
        spec:
          containers:
          - image: registry.example.com/gameserver:1b12371827fdea31231230901876ffe
            name: agones
            resources:
              limits:
                cpu: 500m
                memory: 512m
              requests:
                cpu: 100m
                memory: 256Mi
          nodeSelector:
            role: game-servers

如果有现有的 fleet,我想将最新的 git commit 以及要使用的图像注入到标签中(假设它将在注册表中正确标记) .

如果不存在 fleet,对于某些值,我想遍历并从头开始创建具有与上述类似特征的新队列。我尝试了几种不同的方法但都失败了——从集群权限问题到尝试在 Jenkins/Groovy.

中使用非常直接的 for 循环时出现的奇怪错误

与 Jenkins 相关的许多事情一样,要完成这项工作,需要几个插件,具体来说:

假设:

  • 这些操作将 运行 在基于 Linux 的容器上正确配置以执行 Jenkins 构建
  • 几个必需的“变量”将从本地文件系统中读取 - 这些可能是管道中早期的构建工件,或者 ENV 变量,具体取决于设置
  • 集群上已存在 Kubernetes 服务帐户,具有足够的权限来执行任务
  • 这些帐户的凭据已在 Jenkins 中配置
  • fleetconfig.yaml 在本地文件系统上可用,并且是一个完整的队列配置,类似于问题中提出的

为了区别对待不同的舰队,需要根据舰队本身的名称应用一些 selection 标准,然后需要一个循环遍历每个区域等。

为了简单起见,类型之间将有一个基本的 if 到 select 的语句(这可以很容易地扩展),然后是一个 2 元素循环来遍历多个元素区域(同样可以轻松扩展到更多区域)。

用 Jenkins 的术语来说,这是作为一个完整的 Stage 编写的,但显然它本身并不是完全可执行的。它尽可能接近现有的、经过测试的、工作的配置,该配置已经 运行 每天多次,已经有一段时间了。

虽然这只是示例资源,但没有理由不能使用它来修改,一般在 Kubernetes 中创建其他资源。

stage('DeployFleets') {
  agent {
    node {
      label 'k8s-node-linux'
    }
  }
  steps {
    script {
      // assume we can read the Fleet name in from a file
      FLEET_NAME = readFile("/path/to/FLEET_NAME")
      // let's assume that test is one of the fleets that is being modified, not created, deal with that first
      container('jenkins-worker'){
        if (FLEET_NAME != 'test') {
          script {
            // again, assume we can read the commit from a file
            def GIT_COMMIT_TAG = readFile("/path/to/GIT_COMMIT")
            //  create a map of 2 fictional regions with an account to use and an cluster adddress for that region
            def DEPLOY_REGIONS = [
              "us-east-1": ["jenkins_service_acct_use1", 'https://useast1-cluster.example.com'],
              "us-east-2": ["jenkins_service_acct_use2", 'https://useast2-cluster.example.com'],
            ]
            // this each construction is needed in order to get around https://issues.jenkins-ci.org/browse/JENKINS-49732 which prevents using a for(element in DEPLOY_REGIONS)
            DEPLOY_REGIONS.each { element ->
              withKubeCredentials([[credentialsId: element.value[0], serverUrl: element.value[1]]]) {
                sh """
                kubectl patch fleet ${FLEET_NAME} -n game-servers --type=json -p='[{"op": "replace", "path": "/spec/template/spec/template/spec/containers/0/image", "value":"registry.example.com/gameserver/${FLEET_NAME}:${GIT_COMMIT_TAG}"}]'
                kubectl patch fleet ${FLEET_NAME} -n game-servers --type=json -p='[{"op": "replace", "path": "/spec/template/metadata/labels/git_commit", "value":"${GIT_COMMIT_TAG}"}]'
                """
              }
            }
          }
        } else {
          // rather than patching here, create a fleet from scratch using a source YAML file as a template
          script {
            def GIT_COMMIT_TAG = readFile("/path/to/GIT_COMMIT")
            def NUM_REPLICAS = 1
            def DEPLOY_REGIONS = [
              "us-east-1": ["jenkins_service_acct_use1", 'https://useast1-cluster.example.com'],
              "us-east-2": ["jenkins_service_acct_use2", 'https://useast2-cluster.example.com'],
            ]
            // see note above about each construct
            DEPLOY_REGIONS.each { element ->
              // assume template available on file system
              def FLEET_CONFIG = readYaml file: "/path/to/fleetconfig.yaml"
              FLEET_CONFIG.metadata.name = env.SOME_SANE_NAME
              FLEET_CONFIG.spec.template.metadata.labels.git_commit = GIT_COMMIT_TAG
              FLEET_CONFIG.spec.replicas = NUM_REPLICAS
              FLEET_CONFIG.spec.template.spec.template.spec.containers[0].image = "registry.example.com/gameserver/${FLEET_NAME}:${GIT_COMMIT_TAG}"
              writeYaml file: "${env.SOME_SANE_NAME}_fleet.yaml", data: FLEET_CONFIG, overwrite: true
              withKubeCredentials([[credentialsId: element.value[0], serverUrl: element.value[1]]]) {
                sh """
                kubectl -n game-servers apply -f "${env.SOME_SANE_NAME}_fleet.yaml"
                """
              }
            }
          }
        }
      }
    }
  }
}

考虑到 Jenkins 通过插件为我们提供的 YAML 操作实用程序,这并不是特别困难,但找到一种端到端的方法可能是一个挑战。在 kubectl 中使用 patch 命令使得补丁对 Jenkins 来说不那么“原生”,但这种便利是非常值得的(例如,替代方法是使用 REST API)。 foreach 结构看起来很奇怪,但需要它来避免 Jenkins 中长期存在的错误。