使用 run-script Task 管理 ACP 原生应用

功能概述

本指南演示如何使用内置的 run-script Task 和 kubectl-app-manager 工具镜像,管理 ACP Application 的完整生命周期——部署、更新、等待就绪、回滚以及失败自动恢复。

为什么使用 run-script 而不是专用 Task?

本指南不为每个操作提供一个固定参数的 Task,而是采用通用的 run-script Task 搭配 kubectl-app-manager 工具镜像。这种方式让你能够完全控制脚本逻辑——可以自定义参数、添加条件分支、组合多个操作,并根据自己的部署流程进行调整,而无需等待新的 Task 发布。

其代价是你需要编写一小段 shell 脚本,而不是填写表单。如果这种模式在团队中被广泛采用,这些脚本可以提升为专用的 catalog Pipeline,从而无需在项目之间复制。

主要能力

  • 从 pipeline 中部署、更新、等待和回滚 ACP 原生应用。
  • 同时支持原始 Kubernetes manifest 和 OCI Helm chart(支持自定义 values)。
  • 跨集群 / 跨命名空间部署——通过挂载 kubeconfig Secret,可以目标指向任意集群或命名空间,不受 pipeline 所在集群和命名空间的限制。
  • 将单个 TaskRun 组合成完整 Pipeline,并在失败时自动回滚。

选择交付路径

使用下表选择最适合你的环境和变更范围的模式。

路径适用场景额外要求本仓库中的验证状态
原始资源 manifest你希望采用最直接、无需 controller 的方式进行创建/更新/回滚只需 ACP Application CRD已完成端到端验证
OCI Helm chart你的应用已经打包为 OCI chart,并且希望由 ACP 根据 chart 源进行协调目标集群中需要 chart-deploy controller已验证 chart 打包 + Harbor 推送;集群侧部署仍依赖 controller 是否可用
通用 manifest patch你只需要修改某个局部字段,例如 replicas、annotations 或某个容器镜像已完成端到端验证

快速导航:

我想…跳转到
部署或更新原生应用场景 1
等待原生应用变为就绪场景 2
回滚到上一个快照场景 3
带自动回滚的完整 Pipeline场景 4
Patch 某个特定资源字段场景 5
目标指向其他集群跨集群部分

关于 ACP Application

ACP Application(GVK:applications.app.k8s.io/v1beta1)是一个 Kubernetes 对象,它将一组相关资源——Deployment、Service、ConfigMap 等——归为一个统一的生命周期单元。平台会协调该对象,并基于底层工作负载汇总出统一的健康状态(RunningPendingFailed 等)。

ACP 控制台支持多种创建 Application 的方式——从容器镜像、Helm chart、原始 YAML manifest、源代码(S2I)以及 Operator 托管服务创建。本指南使用的 kubectl application 插件操作的是相同的底层 Application 对象。当你使用 --source-type oci 创建 Application 时,平台还会在内部创建对应的 HelmRequestapp.alauda.io/v1)资源来协调 chart——你无需直接管理该资源。

本指南中使用的关键能力:

能力使用位置
通过原始资源 manifest 创建 / 更新场景 1(主路径)
通过 OCI Helm chart 创建 / 更新(支持自定义 values)场景 1(高级路径)
等待就绪(kubectl application status --watch场景 2
基于快照的回滚(kubectl application snapshot rollback场景 3、4
跨集群 / 跨命名空间目标指向跨集群部分
通用资源 patch场景 5

每次更新操作都会创建一个 ApplicationHistory 快照(GVK:applicationhistories.app.k8s.io/v1beta1),可用于回滚。

kubectl-app-manager 镜像包含什么

kubectl-app-manager 镜像是专为 Application 生命周期管理设计的工具镜像。它已注册到 catalog 中,可在 run-script Task UI 的工具镜像选择器中选用。

字段
Registryregistry-dev.alauda.io/alauda-pipelines-catalog/hub/kubectl-app-manager
推荐 tagv0.1(建议固定以保证可复现;latest 始终指向最新的固定 tag)
主要工具kubectl-application 插件、kubectl 1.33
内置工具yq 4.47

本指南中使用的关键命令:

# Create a new Application from a raw-resource manifest
kubectl application create <appName> -n <namespace> -r resources.yaml

# Update an existing Application
kubectl application update <appName> -n <namespace> -r resources.yaml

# Create from an OCI Helm chart (requires chart-deploy controller in cluster)
kubectl application create <appName> -n <namespace> \
  --source-type oci \
  --source-address <registry>/<repo> \
  -v <chartVersion> \
  [-f values.yaml] \
  [--source-secret-ref <pullSecret>]

# Poll current state (one-shot, no waiting)
kubectl application status -n <namespace> <appName> --watch=false -o json

# Wait for Running state with timeout
kubectl application status -n <namespace> <appName> --watch=true --timeout=300s -o json

# Roll back to the previous snapshot
kubectl application snapshot rollback -n <namespace> <appName>

如需查看可用工具镜像的完整列表以及如何选择它们,请参阅 Tool Images

如需了解 hub resolver 语法,请参阅 Using Hub Resolvers 指定远程任务

前提条件

开始之前

  • 集群中已安装 Tekton Pipelines v1。
  • 你有一个用于运行 pipeline task 的命名空间。
  • 已安装 ACP Application CRD(applications.app.k8s.io/v1beta1) (它是 ACP 平台安装的一部分)。
  • hub resolver 已配置为使用名为 catalog 的 catalog。使用以下命令验证:
    kubectl get configmap hubresolver-config -n tekton-pipelines-resolvers \
      -o jsonpath='{.data.default-tekton-hub-catalog}'
    # Expected output: catalog

RBAC

运行 pipeline task 的 ServiceAccount 必须具备管理 ApplicationApplicationHistory 对象以及底层 Kubernetes 资源(Deployment、Service 等)的权限。

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: acp-app-manager
  namespace: <YOUR_NAMESPACE>
rules:
  - apiGroups: [app.k8s.io]
    resources: [applications, applicationhistories]
    verbs: [get, list, watch, create, update, patch, delete]
  - apiGroups: [apps]
    resources: [deployments, replicasets, statefulsets, daemonsets]
    verbs: [get, list, watch, create, update, patch, delete]
  - apiGroups: [""]
    resources: [services, configmaps, secrets, serviceaccounts, persistentvolumeclaims]
    verbs: [get, list, watch, create, update, patch, delete]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: acp-app-manager
  namespace: <YOUR_NAMESPACE>
subjects:
  - kind: ServiceAccount
    name: <YOUR_SERVICE_ACCOUNT>
    namespace: <YOUR_NAMESPACE>
roleRef:
  kind: Role
  name: acp-app-manager
  apiGroup: rbac.authorization.k8s.io

注意:通过 ACP pipeline gateway 运行时,--as impersonation 标志不会应用到 kubectl application 子命令。请直接使用具备所需权限的 ServiceAccount,而不要依赖 gateway impersonation。

跨集群 / 跨命名空间部署

本指南中的所有场景都接受一个可选的 kubeconfigPath 参数。当设置后,每次 kubectl application … 调用都会使用该 kubeconfig 文件所描述的集群和命名空间,而不是使用 Pod 自身的 ServiceAccount 凭证。

这样,一个 pipeline 就可以在多个集群和命名空间之间管理应用,而无需修改任何代码——只需要更改绑定即可。

重要:对于 kubectl application ... 命令,请导出 KUBECONFIG,而不是在命令前加 --kubeconfig。该插件会拒绝放在插件名称之前的全局标志。

如何配置

步骤 1 — 将目标集群的 kubeconfig 存储到 Secret 中:

kubectl create secret generic target-cluster-kubeconfig \
  --from-file=config=./kubeconfig-target.yaml \
  -n <PIPELINE_NAMESPACE>

步骤 2 — 将该 Secret 绑定到 run-script Task 的 secret workspace:

workspaces:
  - name: secret
    secret:
      secretName: target-cluster-kubeconfig

步骤 3 — 将挂载路径作为 kubeconfigPath 参数传入:

- name: args
  value:
    - <appName>
    - <appNamespace>
    - /workspace/secret/config   # kubeconfigPath

在脚本内部,在调用 kubectl application 之前导出该路径:

[ -n "${KUBECONFIG_PATH}" ] && export KUBECONFIG="${KUBECONFIG_PATH}"
kubectl application status -n "${APP_NS}" "${APP_NAME}" ...

kubeconfigPath 为空(默认值)时,Pod 会使用自身投影的 ServiceAccount token——同集群 pipeline 不需要 Secret 绑定。

输入参考

下表列出了本指南各场景中使用的参数、workspace 和结果。命名遵循 catalog Task 约定: params 使用 camelCaseworkspacesresults 使用 kebab-case

Script args(位置参数)

run-script Task 会将 args 数组按顺序作为位置参数 $1$2$3、… 传递给脚本。例如,args: [demo-app, my-ns, ""] 会在脚本中映射为 APP_NAME=$1APP_NS=$2KUBECONFIG_PATH=$3

位置名称默认值说明
$1appNameACP Application 对象名称
$2appNamespaceApplication 所在命名空间
$3因场景而异kubeconfigPath(原始资源、等待、回滚)或 chartAddress(OCI)
$4+场景相关例如:用于等待/回滚的 timeoutSeconds;OCI chart 路径的 chartVersionchartPullSecretkubeconfigPathvaluesYAML

请参考各场景中的 args 列表以获得准确的位置映射。

Workspaces(run-script Task)

名称是否必需用途
source源文件(manifest、values 文件)。绑定后,脚本的当前工作目录会切换到此处。
secret凭证文件,例如用于跨集群访问的 kubeconfig Secret
config额外配置文件
cache构建缓存(此处通常很少需要)

Results(run-script Task)

名称说明
string-result脚本写入的 JSON payload 或纯字符串
overview-markdown在 run overview 标签页中渲染的 Markdown

场景 1 — 部署或更新原生应用

该脚本会检测 Application 是否存在,并据此调用 createupdate。结果 payload 会写入 string-result,以便下游 task 检查实际执行的操作。

原始资源 manifest 路径(主路径)

当你的工作负载以普通 Kubernetes manifest 描述时使用此路径。不需要 chart-deploy 引擎。

apiVersion: tekton.dev/v1
kind: TaskRun
metadata:
  generateName: deploy-app-
  namespace: <YOUR_NAMESPACE>
spec:
  serviceAccountName: <YOUR_SERVICE_ACCOUNT>
  taskRef:
    resolver: hub
    params:
      - { name: kind,    value: task    }
      - { name: type,    value: artifact }
      - { name: catalog, value: catalog }
      - { name: name,    value: run-script }
      - { name: version, value: "0.1"  }
  params:
    - name: image
      value: registry-dev.alauda.io/alauda-pipelines-catalog/hub/kubectl-app-manager:v0.1
    - name: imagePullPolicy
      value: IfNotPresent
    - name: args
      value:
        - "<APP_NAME>"       # 1: appName
        - "<APP_NAMESPACE>"  # 2: appNamespace
        - ""                 # 3: kubeconfigPath (empty = in-cluster)
    - name: script
      value: |
        # Deploy or update ACP Application from an inline resources manifest.
        # Args: 1=appName 2=appNamespace 3=kubeconfigPath
        set -euo pipefail
        APP_NAME="${1:?appName required}"
        APP_NS="${2:?appNamespace required}"
        KUBECONFIG_PATH="${3:-}"

        # Export KUBECONFIG instead of prepending --kubeconfig because
        # kubectl-application rejects global flags before the plugin name.
        [ -n "${KUBECONFIG_PATH}" ] && export KUBECONFIG="${KUBECONFIG_PATH}"

        # Keep resource names aligned with appName so users only edit one field
        # for the common case where the Application and its main workload share a name.
        cat > /tmp/resources.yaml <<RESOURCES_EOF
        apiVersion: apps/v1
        kind: Deployment
        metadata:
          name: ${APP_NAME}
          labels:
            app.kubernetes.io/name: ${APP_NAME}
        spec:
          replicas: 1
          selector:
            matchLabels:
              app.kubernetes.io/name: ${APP_NAME}
          template:
            metadata:
              labels:
                app.kubernetes.io/name: ${APP_NAME}
            spec:
              containers:
                - name: app
                  image: <YOUR_IMAGE>
                  ports:
                    - containerPort: 80
        ---
        apiVersion: v1
        kind: Service
        metadata:
          name: ${APP_NAME}
          labels:
            app.kubernetes.io/name: ${APP_NAME}
        spec:
          selector:
            app.kubernetes.io/name: ${APP_NAME}
          ports:
            - {port: 80, targetPort: 80, name: http}
        RESOURCES_EOF

        STATE_JSON=$(kubectl application status -n "${APP_NS}" "${APP_NAME}" --watch=false -o json 2>/dev/null || true)
        [ -z "${STATE_JSON}" ] && STATE_JSON='{"state":"NotFound"}'
        STATE=$(printf '%s' "${STATE_JSON}" | yq e '.state' -)

        if [ "${STATE}" = "NotFound" ]; then
          kubectl application create "${APP_NAME}" -n "${APP_NS}" -r /tmp/resources.yaml
          ACTION="created"
        else
          kubectl application update "${APP_NAME}" -n "${APP_NS}" -r /tmp/resources.yaml
          ACTION="updated"
        fi
        echo "==> ${ACTION} ${APP_NS}/${APP_NAME}"

        printf '{"appNamespace":"%s","appName":"%s","action":"%s"}' \
          "${APP_NS}" "${APP_NAME}" "${ACTION}" > "$(results.string-result.path)"

        cat > "$(results.overview-markdown.path)" <<EOF
        ### ACP Application ${ACTION}

        | Field | Value |
        | --- | --- |
        | Namespace | \`${APP_NS}\` |
        | Application | \`${APP_NAME}\` |
        | Action | \`${ACTION}\` |
        EOF

使用 git 仓库存放资源:你可以不在此内联 manifest,而是将 source workspace 绑定到 git-clone 步骤的输出,并直接引用文件路径:

# When source workspace is bound, the script CWD switches to /workspace/source
kubectl application create "${APP_NAME}" -n "${APP_NS}" -r ./k8s/resources.yaml

OCI Helm Chart 路径(高级)

当你的应用已打包为 OCI Helm chart 时使用此路径。该路径要求集群中已安装并运行 chart-deploy controller(captain)。

将上面 TaskRun 中的 argsscript 参数替换为以下内容:

    - name: args
      value:
        - "<APP_NAME>"                              # 1: appName
        - "<APP_NAMESPACE>"                         # 2: appNamespace
        - "<OCI_REGISTRY>/<PROJECT>/<CHART>"        # 3: chartAddress (without oci://)
        - "<CHART_VERSION>"                         # 4: chartVersion
        - ""                                        # 5: chartPullSecret (empty = public)
        - ""                                        # 6: kubeconfigPath (empty = in-cluster)
        - ""                                        # 7: valuesYAML (empty = keep current values)
    - name: script
      value: |
        # Deploy or update ACP Application from an OCI Helm chart.
        # Args: 1=appName 2=appNamespace 3=chartAddress 4=chartVersion
        #       5=chartPullSecret 6=kubeconfigPath 7=valuesYAML
        set -euo pipefail
        APP_NAME="${1:?appName required}"
        APP_NS="${2:?appNamespace required}"
        SOURCE_ADDRESS="${3:?chartAddress required}"
        CHART_VERSION="${4:?chartVersion required}"
        CHART_PULL_SECRET="${5:-}"
        KUBECONFIG_PATH="${6:-}"
        VALUES_YAML="${7:-}"

        [ -n "${KUBECONFIG_PATH}" ] && export KUBECONFIG="${KUBECONFIG_PATH}"

        # Write custom Helm values to a temp file when provided.
        VALUES_ARGS=()
        if [ -n "${VALUES_YAML}" ]; then
          printf '%s' "${VALUES_YAML}" > /tmp/values.yaml
          yq eval --inplace --prettyPrint /tmp/values.yaml
          echo "==> Custom values:"
          cat /tmp/values.yaml
          VALUES_ARGS=(-f /tmp/values.yaml)
        fi

        STATE_JSON=$(kubectl application status -n "${APP_NS}" "${APP_NAME}" --watch=false -o json 2>/dev/null || true)
        [ -z "${STATE_JSON}" ] && STATE_JSON='{"state":"NotFound"}'
        STATE=$(printf '%s' "${STATE_JSON}" | yq e '.state' -)

        if [ "${STATE}" = "NotFound" ]; then
          PREV_VERSION=""
          CREATE_ARGS=(application create "${APP_NAME}" -n "${APP_NS}" \
            --source-type oci --source-address "${SOURCE_ADDRESS}" -v "${CHART_VERSION}")
          [ -n "${CHART_PULL_SECRET}" ] && CREATE_ARGS+=(--source-secret-ref "${CHART_PULL_SECRET}")
          kubectl "${CREATE_ARGS[@]}" ${VALUES_ARGS[@]+"${VALUES_ARGS[@]}"}
          ACTION="created"
        else
          PREV_VERSION=$(kubectl get application.app.k8s.io -n "${APP_NS}" "${APP_NAME}" \
            -o jsonpath='{.metadata.annotations.app\.cpaas\.io/chart\.version}' 2>/dev/null || true)
          UPDATE_ARGS=(application update "${APP_NAME}" -n "${APP_NS}" \
            --source-type oci --source-address "${SOURCE_ADDRESS}" -v "${CHART_VERSION}")
          [ -n "${CHART_PULL_SECRET}" ] && UPDATE_ARGS+=(--source-secret-ref "${CHART_PULL_SECRET}")
          kubectl "${UPDATE_ARGS[@]}" ${VALUES_ARGS[@]+"${VALUES_ARGS[@]}"}
          ACTION="updated"
        fi

        CURR_VERSION=$(kubectl get application.app.k8s.io -n "${APP_NS}" "${APP_NAME}" \
          -o jsonpath='{.metadata.annotations.app\.cpaas\.io/chart\.version}' 2>/dev/null || true)

        printf '{"appNamespace":"%s","appName":"%s","action":"%s","previousVersion":"%s","currentVersion":"%s"}' \
          "${APP_NS}" "${APP_NAME}" "${ACTION}" "${PREV_VERSION:-}" "${CURR_VERSION:-}" \
          > "$(results.string-result.path)"

要求:OCI Helm Chart 部署需要集群中存在 chart-deploy(captain)controller。如果没有它,Application 对象虽然会被创建,但会无限期停留在 Pending 状态。如果你的集群中未安装 captain,请使用原始资源路径。

HTTP chart 仓库:如果你的 chart 来源于 HTTP 仓库而不是 OCI registry,请将 --source-type oci 改为 --source-type HTTP,并将 --source-address 设置为 HTTP 仓库 URL。

场景 2 — 等待原生应用变为就绪

该脚本使用 kubectl application status --watch=true --timeout 等待 Application 达到 Running 状态。该插件会在内部处理轮询,并在超时时返回非零退出码,从而使 TaskRun(以及任何外层 Pipeline task)失败——以便 finally 块可以做出响应。

apiVersion: tekton.dev/v1
kind: TaskRun
metadata:
  generateName: wait-app-
  namespace: <YOUR_NAMESPACE>
spec:
  serviceAccountName: <YOUR_SERVICE_ACCOUNT>
  taskRef:
    resolver: hub
    params:
      - { name: kind,    value: task    }
      - { name: type,    value: artifact }
      - { name: catalog, value: catalog }
      - { name: name,    value: run-script }
      - { name: version, value: "0.1"  }
  params:
    - name: image
      value: registry-dev.alauda.io/alauda-pipelines-catalog/hub/kubectl-app-manager:v0.1
    - name: imagePullPolicy
      value: IfNotPresent
    - name: args
      value:
        - "<APP_NAME>"       # 1: appName
        - "<APP_NAMESPACE>"  # 2: appNamespace
        - ""                 # 3: kubeconfigPath (empty = in-cluster)
        - "300"              # 4: timeoutSeconds
    - name: script
      value: |
        # Wait for ACP Application to reach Running state.
        # Args: 1=appName 2=appNamespace 3=kubeconfigPath 4=timeoutSeconds
        set -euo pipefail
        APP_NAME="${1:?appName required}"
        APP_NS="${2:?appNamespace required}"
        KUBECONFIG_PATH="${3:-}"
        TIMEOUT="${4:-300}"

        [ -n "${KUBECONFIG_PATH}" ] && export KUBECONFIG="${KUBECONFIG_PATH}"

        echo "==> Waiting up to ${TIMEOUT}s for ${APP_NS}/${APP_NAME} to reach Running state"
        STATUS_JSON=$(kubectl application status -n "${APP_NS}" "${APP_NAME}" \
          --timeout="${TIMEOUT}s" --watch=true -o json || true)

        STATE=$(printf '%s' "${STATUS_JSON}" | yq e '.state' -)
        printf '%s' "${STATE}" > "$(results.string-result.path)"

        if [ "${STATE}" != "Running" ]; then
          echo "ERROR: ${APP_NS}/${APP_NAME} did not reach Running (last state: ${STATE})"
          exit 1
        fi
        echo "Application ${APP_NS}/${APP_NAME} is Running."

预期输出 — 正常路径:

==> Waiting up to 300s for devops-doc-test/demo-app to reach Running state
Application devops-doc-test/demo-app is Running.

预期输出 — 超时路径:

==> Waiting up to 30s for devops-doc-test/demo-app to reach Running state
ERROR: devops-doc-test/demo-app did not reach Running (last state: Pending)

场景 3 — 回滚到上一个快照

该脚本会触发 kubectl application snapshot rollback,将 Application 回退到最近的 ApplicationHistory 快照,然后等待 Application 恢复到 Running

apiVersion: tekton.dev/v1
kind: TaskRun
metadata:
  generateName: rollback-app-
  namespace: <YOUR_NAMESPACE>
spec:
  serviceAccountName: <YOUR_SERVICE_ACCOUNT>
  taskRef:
    resolver: hub
    params:
      - { name: kind,    value: task    }
      - { name: type,    value: artifact }
      - { name: catalog, value: catalog }
      - { name: name,    value: run-script }
      - { name: version, value: "0.1"  }
  params:
    - name: image
      value: registry-dev.alauda.io/alauda-pipelines-catalog/hub/kubectl-app-manager:v0.1
    - name: imagePullPolicy
      value: IfNotPresent
    - name: args
      value:
        - "<APP_NAME>"       # 1: appName
        - "<APP_NAMESPACE>"  # 2: appNamespace
        - ""                 # 3: kubeconfigPath (empty = in-cluster)
        - "300"              # 4: timeoutSeconds
    - name: script
      value: |
        # Rollback ACP Application to its previous snapshot.
        # Args: 1=appName 2=appNamespace 3=kubeconfigPath 4=timeoutSeconds
        set -euo pipefail
        APP_NAME="${1:?appName required}"
        APP_NS="${2:?appNamespace required}"
        KUBECONFIG_PATH="${3:-}"
        TIMEOUT="${4:-300}"

        [ -n "${KUBECONFIG_PATH}" ] && export KUBECONFIG="${KUBECONFIG_PATH}"

        echo "==> Triggering snapshot rollback for ${APP_NS}/${APP_NAME}"
        if ! kubectl application snapshot rollback -n "${APP_NS}" "${APP_NAME}"; then
          echo "==> Rollback command failed (application may have no previous snapshot)."
          printf 'NoSnapshot' > "$(results.string-result.path)"
          exit 0
        fi
        echo "==> Rollback triggered; waiting up to ${TIMEOUT}s for Running state"

        STATUS_JSON=$(kubectl application status -n "${APP_NS}" "${APP_NAME}" \
          --timeout="${TIMEOUT}s" --watch=true -o json || true)

        STATE=$(printf '%s' "${STATUS_JSON}" | yq e '.state' -)
        printf '%s' "${STATE}" > "$(results.string-result.path)"

        if [ "${STATE}" != "Running" ]; then
          echo "ERROR: rollback failed, ${APP_NS}/${APP_NAME} entered state: ${STATE}"
          exit 1
        fi
        echo "==> Rollback succeeded; ${APP_NS}/${APP_NAME} is Running."

预期输出:

==> Triggering snapshot rollback for devops-doc-test/demo-app
==> Rollback triggered; waiting up to 300s for Running state
==> Rollback succeeded; devops-doc-test/demo-app is Running.

要列出可用快照:

# List all ApplicationHistory snapshots for a namespace
kubectl get applicationhistory -n <namespace>

# Rollback rolls back to the previous (n-1) snapshot by default.
kubectl application snapshot rollback -n <namespace> <appName>

场景 4 — 生产 Pipeline:部署、等待与自动回滚

该 Pipeline 将上面的三个脚本组合为一个适用于生产环境的部署流程:

  1. deploy-app — 创建或更新 Application
  2. wait-app — 等待直到达到 Running,或在超时后失败(在 deploy-app 之后运行)
  3. rollback-app(位于 finally 中)— 如果 wait-app 失败,则自动回滚

finally task 上的 when 守卫确保只有在失败时才会触发回滚。 retries: 2 会在首次回滚失败时再提供两次重试机会。

apiVersion: tekton.dev/v1
kind: Pipeline
metadata:
  name: acp-app-deploy-wait-rollback
  namespace: <YOUR_NAMESPACE>
spec:
  params:
    - name: appName
      type: string
      description: Name of the ACP Application object.
    - name: appNamespace
      type: string
      description: Namespace where the ACP Application lives.
    - name: resourcesYAML
      type: string
      description: |
        Multi-document YAML string containing all Kubernetes resources
        (Deployments, Services, ConfigMaps, …) to associate with the Application.
    - name: waitTimeout
      type: string
      default: "300"
      description: Seconds to wait for the Application to reach Running state.
    - name: rollbackTimeout
      type: string
      default: "300"
      description: Seconds to wait for the rollback to complete.
    - name: scriptImage
      type: string
      default: registry-dev.alauda.io/alauda-pipelines-catalog/hub/kubectl-app-manager:v0.1
      description: |
        Tool image containing kubectl and the kubectl-application plugin.
        Pin to a specific tag (e.g. v0.1) for reproducible runs.
        If using imagePullPolicy: IfNotPresent, ensure the tag is pinned — using
        'latest' with IfNotPresent may serve a stale cached image after upgrades.
    - name: kubeconfigPath
      type: string
      default: ""
      description: |
        Path to a kubeconfig file inside the task pod. Leave empty to use
        the pod's in-cluster ServiceAccount credentials.
        Typically /workspace/secret/config when the kubeconfig workspace is bound.

  workspaces:
    - name: kubeconfig
      optional: true
      description: |
        Optional Secret containing a kubeconfig file for cross-cluster targeting.
        When bound, the file is available at /workspace/secret/<key> inside the task
        pod (because it maps to the run-script Task's 'secret' workspace).
        Set kubeconfigPath to /workspace/secret/<key>.

  tasks:
    - name: deploy-app
      taskRef:
        resolver: hub
        params:
          - { name: kind,    value: task    }
          - { name: type,    value: artifact }
          - { name: catalog, value: catalog }
          - { name: name,    value: run-script }
          - { name: version, value: "0.1"  }
      workspaces:
        - name: secret
          workspace: kubeconfig
      params:
        - name: image
          value: $(params.scriptImage)
        - name: imagePullPolicy
          value: IfNotPresent
        - name: args
          value:
            - $(params.appName)
            - $(params.appNamespace)
            - $(params.kubeconfigPath)
        - name: script
          value: |
            set -euo pipefail
            APP_NAME="${1:?appName required}"
            APP_NS="${2:?appNamespace required}"
            KUBECONFIG_PATH="${3:-}"

            [ -n "${KUBECONFIG_PATH}" ] && export KUBECONFIG="${KUBECONFIG_PATH}"

            cat > /tmp/resources.yaml <<'RESOURCES_EOF'
            $(params.resourcesYAML)
            RESOURCES_EOF

            STATE_JSON=$(kubectl application status -n "${APP_NS}" "${APP_NAME}" --watch=false -o json 2>/dev/null || true)
            [ -z "${STATE_JSON}" ] && STATE_JSON='{"state":"NotFound"}'
            STATE=$(printf '%s' "${STATE_JSON}" | yq e '.state' -)

            if [ "${STATE}" = "NotFound" ]; then
              kubectl application create "${APP_NAME}" -n "${APP_NS}" -r /tmp/resources.yaml
              ACTION="created"
            else
              kubectl application update "${APP_NAME}" -n "${APP_NS}" -r /tmp/resources.yaml
              ACTION="updated"
            fi
            echo "==> ${ACTION} ${APP_NS}/${APP_NAME}"
            printf '{"appNamespace":"%s","appName":"%s","action":"%s"}' \
              "${APP_NS}" "${APP_NAME}" "${ACTION}" > "$(results.string-result.path)"

    - name: wait-app
      runAfter: [deploy-app]
      taskRef:
        resolver: hub
        params:
          - { name: kind,    value: task    }
          - { name: type,    value: artifact }
          - { name: catalog, value: catalog }
          - { name: name,    value: run-script }
          - { name: version, value: "0.1"  }
      workspaces:
        - name: secret
          workspace: kubeconfig
      params:
        - name: image
          value: $(params.scriptImage)
        - name: imagePullPolicy
          value: IfNotPresent
        - name: args
          value:
            - $(params.appName)
            - $(params.appNamespace)
            - $(params.kubeconfigPath)
            - $(params.waitTimeout)
        - name: script
          value: |
            set -euo pipefail
            APP_NAME="${1:?appName required}"
            APP_NS="${2:?appNamespace required}"
            KUBECONFIG_PATH="${3:-}"
            TIMEOUT="${4:-300}"

            [ -n "${KUBECONFIG_PATH}" ] && export KUBECONFIG="${KUBECONFIG_PATH}"

            echo "==> Waiting up to ${TIMEOUT}s for ${APP_NS}/${APP_NAME} to reach Running state"
            STATUS_JSON=$(kubectl application status -n "${APP_NS}" "${APP_NAME}" \
              --timeout="${TIMEOUT}s" --watch=true -o json || true)

            STATE=$(printf '%s' "${STATUS_JSON}" | yq e '.state' -)
            printf '%s' "${STATE}" > "$(results.string-result.path)"

            if [ "${STATE}" != "Running" ]; then
              echo "ERROR: ${APP_NS}/${APP_NAME} did not reach Running (last state: ${STATE})"
              exit 1
            fi
            echo "Application ${APP_NS}/${APP_NAME} is Running."

  finally:
    - name: rollback-app
      # Only trigger when wait-app failed — skip on a clean success path.
      when:
        - input: $(tasks.wait-app.status)
          operator: in
          values: ["Failed"]
      taskRef:
        resolver: hub
        params:
          - { name: kind,    value: task    }
          - { name: type,    value: artifact }
          - { name: catalog, value: catalog }
          - { name: name,    value: run-script }
          - { name: version, value: "0.1"  }
      retries: 2
      workspaces:
        - name: secret
          workspace: kubeconfig
      params:
        - name: image
          value: $(params.scriptImage)
        - name: imagePullPolicy
          value: IfNotPresent
        - name: args
          value:
            - $(params.appName)
            - $(params.appNamespace)
            - $(params.kubeconfigPath)
            - $(params.rollbackTimeout)
        - name: script
          value: |
            set -euo pipefail
            APP_NAME="${1:?appName required}"
            APP_NS="${2:?appNamespace required}"
            KUBECONFIG_PATH="${3:-}"
            TIMEOUT="${4:-300}"

            [ -n "${KUBECONFIG_PATH}" ] && export KUBECONFIG="${KUBECONFIG_PATH}"

            echo "==> Triggering snapshot rollback for ${APP_NS}/${APP_NAME}"
            if ! kubectl application snapshot rollback -n "${APP_NS}" "${APP_NAME}"; then
              echo "==> Rollback command failed (application may have no previous snapshot)."
              printf 'NoSnapshot' > "$(results.string-result.path)"
              exit 0
            fi

            STATUS_JSON=$(kubectl application status -n "${APP_NS}" "${APP_NAME}" \
              --timeout="${TIMEOUT}s" --watch=true -o json || true)

            STATE=$(printf '%s' "${STATUS_JSON}" | yq e '.state' -)
            printf '%s' "${STATE}" > "$(results.string-result.path)"

            if [ "${STATE}" != "Running" ]; then
              echo "ERROR: rollback failed, ${APP_NS}/${APP_NAME} entered state: ${STATE}"
              exit 1
            fi
            echo "==> Rollback succeeded; ${APP_NS}/${APP_NAME} is Running."

运行 Pipeline

成功路径 — 镜像正常,Application 达到 Running

apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
  generateName: acp-app-deploy-
  namespace: <YOUR_NAMESPACE>
spec:
  taskRunTemplate:
    serviceAccountName: <YOUR_SERVICE_ACCOUNT>
  pipelineRef:
    name: acp-app-deploy-wait-rollback
  params:
    - name: appName
      value: my-app
    - name: appNamespace
      value: <YOUR_NAMESPACE>
    - name: waitTimeout
      value: "300"
    - name: rollbackTimeout
      value: "300"
    - name: resourcesYAML
      value: |
        apiVersion: apps/v1
        kind: Deployment
        metadata:
          name: my-app
          labels:
            app.kubernetes.io/name: my-app
        spec:
          replicas: 2
          selector:
            matchLabels:
              app.kubernetes.io/name: my-app
          template:
            metadata:
              labels:
                app.kubernetes.io/name: my-app
            spec:
              containers:
                - name: app
                  image: <YOUR_IMAGE>:<YOUR_TAG>
                  ports:
                    - containerPort: 8080
        ---
        apiVersion: v1
        kind: Service
        metadata:
          name: my-app
        spec:
          selector:
            app.kubernetes.io/name: my-app
          ports:
            - {port: 80, targetPort: 8080, name: http}

预期结果Tasks Completed: 2 (Failed: 0), Skipped: 1 (因为 wait-app 成功,所以 rollback-app 会被跳过。)

失败路径 — 镜像有问题,wait-app 超时,触发回滚:

为了测试这条路径,请将 resourcesYAML 参数改为使用一个不存在的镜像 tag,并缩短 waitTimeout,以便快速暴露失败:

  params:
    - name: appName
      value: my-app
    - name: appNamespace
      value: <YOUR_NAMESPACE>
    - name: waitTimeout
      value: "60"       # short timeout so the test completes quickly
    - name: rollbackTimeout
      value: "300"
    - name: resourcesYAML
      value: |
        apiVersion: apps/v1
        kind: Deployment
        metadata:
          name: my-app
        spec:
          template:
            spec:
              containers:
                - name: app
                  image: <YOUR_REGISTRY>/my-app:nonexistent-tag   # intentionally bad

该 pipeline 将会:

  1. deploy-app — 成功(将错误的 spec 写入 Application)
  2. wait-app — 在 waitTimeout 秒后失败(Application 保持 Pending
  3. rollback-app(finally)— 触发并回滚到上一个快照

预期结果Tasks Completed: 3 (Failed: 1), Skipped: 0 回滚完成后,Application 会恢复到 Running

跨集群变体

要目标指向其他集群,请绑定 kubeconfig workspace,并将 kubeconfigPath 设置为挂载后的文件路径:

spec:
  taskRunTemplate:
    serviceAccountName: <YOUR_SERVICE_ACCOUNT>
  workspaces:
    - name: kubeconfig         # matches the Pipeline workspace name
      secret:
        secretName: target-cluster-kubeconfig
  params:
    - name: kubeconfigPath
      value: /workspace/secret/config   # mounted by run-script's 'secret' workspace
    # ... other params unchanged

Pipeline 会将其 kubeconfig workspace 映射到每个 task 的 secret workspace。该文件在 task Pod 内的路径为 /workspace/secret/<key>,其中 <key> 是你在创建 Secret 时使用的 Secret 键名 (例如,使用 --from-file=config=./kubeconfig-target.yaml 时,键名为 config)。

场景 5 — 通用 manifest patch(备用方案)

当你需要进行 kubectl application update 无法覆盖的细粒度字段更新时使用此路径——例如,只 patch 一个环境变量或 annotation,而不替换整个资源 manifest。

apiVersion: tekton.dev/v1
kind: TaskRun
metadata:
  generateName: patch-app-resource-
  namespace: <YOUR_NAMESPACE>
spec:
  serviceAccountName: <YOUR_SERVICE_ACCOUNT>
  taskRef:
    resolver: hub
    params:
      - { name: kind,    value: task    }
      - { name: type,    value: artifact }
      - { name: catalog, value: catalog }
      - { name: name,    value: run-script }
      - { name: version, value: "0.1"  }
  params:
    - name: image
      value: registry-dev.alauda.io/alauda-pipelines-catalog/hub/kubectl-app-manager:v0.1
    - name: imagePullPolicy
      value: IfNotPresent
    - name: args
      value:
        - "<APP_NAME>"       # 1: appName
        - "<APP_NAMESPACE>"  # 2: appNamespace
        - ""                 # 3: kubeconfigPath (empty = in-cluster)
        - "Deployment"       # 4: resourceKind
        - "<RESOURCE_NAME>"  # 5: resourceName (defaults to appName if empty)
    - name: script
      value: |
        # Generic JSON merge patch on a resource owned by an ACP Application.
        # Args: 1=appName 2=appNamespace 3=kubeconfigPath 4=resourceKind 5=resourceName
        set -euo pipefail
        APP_NAME="${1:?appName required}"
        APP_NS="${2:?appNamespace required}"
        KUBECONFIG_PATH="${3:-}"
        RESOURCE_KIND="${4:-Deployment}"
        RESOURCE_NAME="${5:-${APP_NAME}}"

        [ -n "${KUBECONFIG_PATH}" ] && export KUBECONFIG="${KUBECONFIG_PATH}"

        # Customize this patch JSON for your use case.
        # --type=merge uses RFC 7386 JSON Merge Patch: scalar fields are merged,
        # but arrays are REPLACED entirely. For array-safe patching (e.g., updating
        # one container in a multi-container pod), use --type=strategic instead.
        # Examples:
        #   Scale replicas:  '{"spec":{"replicas":3}}'
        #   Add annotation:  '{"metadata":{"annotations":{"deploy.env":"staging"}}}'
        PATCH='{"spec":{"replicas":3}}'

        echo "==> Patching ${RESOURCE_KIND} ${APP_NS}/${RESOURCE_NAME}"
        kubectl patch "${RESOURCE_KIND}" "${RESOURCE_NAME}" \
          -n "${APP_NS}" --type=merge --patch "${PATCH}"

        printf '{"namespace":"%s","kind":"%s","name":"%s"}' \
          "${APP_NS}" "${RESOURCE_KIND}" "${RESOURCE_NAME}" \
          > "$(results.string-result.path)"

从新制品触发

你可以使用 ACP 制品触发机制,让场景 4 的 Pipeline 在新的 OCI image 或 Helm chart 推送到 registry 时自动运行。

该触发器会基于模板生成 PipelineRun,并将制品元数据(registry URL、digest、tag)作为参数注入:

# PipelineRun template — placeholders are filled by the artifact trigger
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
  generateName: acp-app-deploy-
  namespace: <YOUR_NAMESPACE>
spec:
  taskRunTemplate:
    serviceAccountName: <YOUR_SERVICE_ACCOUNT>
  pipelineRef:
    name: acp-app-deploy-wait-rollback
  params:
    - name: appName
      value: my-app
    - name: appNamespace
      value: <YOUR_NAMESPACE>
    - name: waitTimeout
      value: "300"
    - name: rollbackTimeout
      value: "300"
    - name: resourcesYAML
      value: |
        apiVersion: apps/v1
        kind: Deployment
        metadata:
          name: my-app
        spec:
          template:
            spec:
              containers:
                - name: app
                  # $(ARTIFACT_URL) and $(ARTIFACT_TAG) are injected by the trigger
                  image: $(ARTIFACT_URL):$(ARTIFACT_TAG)

如需完整的触发器配置,请参阅你所在平台 how-to 指南中的 ACP 制品触发文档。

验证

在提交 PipelineRun 或 TaskRun 后,可使用以下命令查看进度:

# List recent PipelineRuns (newest last)
kubectl get pipelinerun -n <namespace> --sort-by=.metadata.creationTimestamp

# List TaskRuns created by a specific PipelineRun
kubectl get taskrun -n <namespace> -l tekton.dev/pipelineRun=<pipelineRunName>

# Check Application state (requires kubectl-application plugin installed locally)
kubectl application status -n <namespace> <appName> --watch=false -o json
# Without the plugin, check the ACP Application object directly:
kubectl get application.app.k8s.io -n <namespace> <appName> -o jsonpath='{.status}'

# Inspect task results from a TaskRun (use the name from the list above)
kubectl get taskrun <taskRunName> -n <namespace> \
  -o jsonpath='{.status.taskResults}'

# View Application history snapshots
kubectl get applicationhistory -n <namespace>

手动回滚

要在 pipeline 之外回滚 Application:

# List available snapshots
kubectl get applicationhistory -n <namespace>

# Roll back to the previous (n-1) snapshot
kubectl application snapshot rollback -n <namespace> <appName>

# Verify the result
kubectl application status -n <namespace> <appName> --watch=false -o json

snapshot rollback 总是回滚到最近的上一个快照。 如果要指定某个快照,请先检查 ApplicationHistory 对象, 然后直接使用 kubectl apply 将该快照中的 .spec.resources 字段对应的特定资源 spec 恢复回来。

故障排查

Forbidden / 权限错误

请确保 ServiceAccount 在目标命名空间中具有带有 applicationsapplicationhistories 权限的 Role。请参见 前提条件 部分。

ACP pipeline gateway 的 --as impersonation 不会传递到 kubectl application 子命令。请直接为 ServiceAccount 分配权限。

OCI chart 部署后 Application 一直处于 Pending

这意味着集群中没有 chart-deploy(captain)controller。你可以选择以下任一方式:

  • 切换到原始资源 manifest 路径(-r resources.yaml),或
  • 在集群中安装 chart-deploy controller。

chart pull secret 未生效

请确保 Secret 与 Application 位于同一个命名空间,并且其名称与 --source-secret-ref 参数完全一致。使用以下命令验证:

kubectl get secret <pullSecret> -n <namespace>

wait-app 意外超时

请直接检查底层工作负载状态:

kubectl get deployment <appName> -n <namespace>
kubectl describe pod -n <namespace> -l app.kubernetes.io/name=<appName>

常见原因:镜像拉取错误、readiness probe 失败、资源配额超限。

rollback-app(finally)未触发

finally task 的 when 条件为:

- input: $(tasks.wait-app.status)
  operator: in
  values: ["Failed"]

只有当 wait-app 实际运行且失败时,该条件才会匹配。如果 wait-appSkipped(例如因为 deploy-app 也失败了),Tekton 会将 $(tasks.wait-app.status) 计算为 None,这与 "Failed" 不匹配,因此 rollback-app 也会被跳过。

可通过检查 PipelineRun 来验证:

kubectl get pipelinerun <pipelinerunName> -n <namespace> \
  -o jsonpath='{.status.childReferences[*]}'

rollback-app 被跳过 — 不存在上一个快照

这通常发生在第一次部署时:Application 刚创建,还没有历史记录。回滚脚本会捕获 kubectl application snapshot rollback 的失败,并写入 NoSnapshotstring-result 后正常退出。

在这种情况下,PipelineRun 会因 wait-app 进入 Failed 状态而结束,但 rollback-app 会成功完成(跳过)。请先修复部署失败的根因,然后手动删除损坏的 Application 并重新运行。

deploy-app 本身失败 — 没有触发回滚

如果 deploy-app 失败(例如 RBAC 错误或网络问题),wait-app 的状态会是 Skipped 而不是 Failedrollback-app 上的 when 条件检查的是 $(tasks.wait-app.status) == Failed,因此回滚也会被跳过。

在这种情况下,Application 并未被更新,因此不需要回滚。请修复 deploy-app 失败的根因,然后重新运行 PipelineRun。

跨集群 kubeconfig 无效或已过期

如果 kubeconfig Secret 已轮换或证书已过期,task 会因认证错误而失败。请使用新的 kubeconfig 重新创建该 Secret:

kubectl delete secret target-cluster-kubeconfig -n <namespace>
kubectl create secret generic target-cluster-kubeconfig \
  --from-file=config=./kubeconfig-new.yaml \
  -n <namespace>

另请参阅