使用 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,并在失败时自动回滚。
选择交付路径
使用下表选择最适合你的环境和变更范围的模式。
快速导航:
关于 ACP Application
ACP Application(GVK:applications.app.k8s.io/v1beta1)是一个 Kubernetes 对象,它将一组相关资源——Deployment、Service、ConfigMap 等——归为一个统一的生命周期单元。平台会协调该对象,并基于底层工作负载汇总出统一的健康状态(Running、Pending、Failed 等)。
ACP 控制台支持多种创建 Application 的方式——从容器镜像、Helm chart、原始 YAML manifest、源代码(S2I)以及 Operator 托管服务创建。本指南使用的 kubectl application 插件操作的是相同的底层 Application 对象。当你使用 --source-type oci 创建 Application 时,平台还会在内部创建对应的 HelmRequest(app.alauda.io/v1)资源来协调 chart——你无需直接管理该资源。
本指南中使用的关键能力:
每次更新操作都会创建一个 ApplicationHistory 快照(GVK:applicationhistories.app.k8s.io/v1beta1),可用于回滚。
kubectl-app-manager 镜像包含什么
kubectl-app-manager 镜像是专为 Application 生命周期管理设计的工具镜像。它已注册到 catalog 中,可在 run-script Task UI 的工具镜像选择器中选用。
本指南中使用的关键命令:
# 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 必须具备管理 Application 和 ApplicationHistory 对象以及底层 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 使用 camelCase,workspaces 和 results 使用 kebab-case。
Script args(位置参数)
run-script Task 会将 args 数组按顺序作为位置参数 $1、$2、$3、… 传递给脚本。例如,args: [demo-app, my-ns, ""] 会在脚本中映射为 APP_NAME=$1、APP_NS=$2、KUBECONFIG_PATH=$3。
请参考各场景中的 args 列表以获得准确的位置映射。
Workspaces(run-script Task)
Results(run-script Task)
场景 1 — 部署或更新原生应用
该脚本会检测 Application 是否存在,并据此调用 create 或 update。结果 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 中的 args 和 script 参数替换为以下内容:
- 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 将上面的三个脚本组合为一个适用于生产环境的部署流程:
deploy-app — 创建或更新 Application
wait-app — 等待直到达到 Running,或在超时后失败(在 deploy-app 之后运行)
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 将会:
deploy-app — 成功(将错误的 spec 写入 Application)
wait-app — 在 waitTimeout 秒后失败(Application 保持 Pending)
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 在目标命名空间中具有带有 applications 和 applicationhistories 权限的 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-app 被 Skipped(例如因为 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 的失败,并写入 NoSnapshot 到 string-result 后正常退出。
在这种情况下,PipelineRun 会因 wait-app 进入 Failed 状态而结束,但 rollback-app 会成功完成(跳过)。请先修复部署失败的根因,然后手动删除损坏的 Application 并重新运行。
deploy-app 本身失败 — 没有触发回滚
如果 deploy-app 失败(例如 RBAC 错误或网络问题),wait-app 的状态会是 Skipped 而不是 Failed。rollback-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>
另请参阅