生产环境 DevOps 实战 | 第 9 天:Tekton Task 与 Step 实战——编写第一个构建任务

第 9/60 天

引言

在上一篇文章中,我们完成了 Tekton Pipelines 的安装部署。但安装只是第一步——真正让 Tekton 发挥价值的是编写自定义的 Task 和 Pipeline。Task 是 Tekton 中最基本的执行单元,而 Step 是 Task 中的最小操作步骤。理解 Task 与 Step 的编写方式,是掌握 Tekton 流水线开发的基础。

本文将从一个完整的实战角度出发,从零编写一个 Git 代码检出 → 单元测试 → 构建镜像的多步骤 Task,涵盖参数传递、Workspace 挂载、Results 输出等核心机制。你将学会如何像编写程序一样,结构化和可复用地编排 CI 任务。

核心概念

Task 与 Step 的关系

概念 类比 说明
Task 函数 一个可重复使用的 CI 任务单元,包含多个 Step
Step 函数中的语句 在容器中执行的最小操作,每个 Step 运行在一个独立的容器中
Pipeline 主程序 编排多个 Task 的执行顺序与依赖关系
PipelineRun 函数调用 Pipeline 的一次具体执行实例

Task 的关键要素

  • params:定义输入参数,支持默认值,类型包括 string、array 等
  • workspaces:定义共享存储卷,使多个 Step 之间可以共享文件
  • results:定义输出结果,供后续 Task 消费
  • steps:实际执行的操作序列,每个 Step 是一个容器
  • sidecars:辅助容器,如日志收集器、代理等

Step 执行特性

  • 每个 Step 运行在独立的容器中,但共享 Task 的 Pod 网络和存储
  • Step 按顺序执行,前一个失败则后续不执行
  • 默认容器镜像需要包含 shell 环境(如 busyboxalpine
  • 可以通过 script 字段嵌入多行脚本

实战步骤

步骤 1:编写第一个最简单的 Task

首先创建一个基本的 “Hello World” Task,熟悉 Task 的声明式结构:

apiVersion: tekton.dev/v1
kind: Task
metadata:
  name: hello-world
  namespace: tekton-pipelines
spec:
  params:
    - name: username
      type: string
      description: 被问候的用户名
      default: "Tekton"
  steps:
    - name: greet
      image: alpine:3.18
      script: |
        #!/bin/sh
        echo "Hello, $(params.username)! Welcome to Tekton!"
        echo "当前时间: $(date -u)"

创建并运行该 Task:

# 创建 Task
kubectl apply -f hello-world-task.yaml

# 创建 TaskRun 手动触发执行
kubectl create -f - <<EOF
apiVersion: tekton.dev/v1
kind: TaskRun
metadata:
  name: hello-world-run
  namespace: tekton-pipelines
spec:
  taskRef:
    name: hello-world
  params:
    - name: username
      value: "DevOps Engineer"
EOF

# 查看执行日志
kubectl logs --selector=tekton.dev/taskRun=hello-world-run --all-containers --tail=20

步骤 2:带 Workspace 的代码检出 Task

Workspace 是 Tekton 中共享文件的机制,相当于 Pod 中的 Volume。这里的 Task 使用 Git Workspace 来检出代码:

apiVersion: tekton.dev/v1
kind: Task
metadata:
  name: git-clone
  namespace: tekton-pipelines
spec:
  params:
    - name: url
      type: string
      description: Git 仓库地址
    - name: revision
      type: string
      description: 分支、标签或 commit SHA
      default: "main"
    - name: subdirectory
      type: string
      description: 检出到 Workspace 的子目录
      default: ""
  workspaces:
    - name: source
      description: 代码存放的工作区
  steps:
    - name: clone
      image: alpine/git:2.40.1
      script: |
        #!/bin/sh
        set -e

        PARAM_URL=$(params.url)
        PARAM_REVISION=$(params.revision)
        PARAM_SUBDIR=$(params.subdirectory)

        if [ -n "$PARAM_SUBDIR" ]; then
          mkdir -p "$(workspaces.source.path)/$PARAM_SUBDIR"
          cd "$(workspaces.source.path)/$PARAM_SUBDIR"
        else
          cd "$(workspaces.source.path)"
        fi

        git init
        git remote add origin "$PARAM_URL"
        git fetch --depth=1 origin "$PARAM_REVISION"
        git checkout -f FETCH_HEAD

        echo "Git 检出完成: $PARAM_URL @ $PARAM_REVISION"
        git log --oneline -1

步骤 3:带 Results 输出的单元测试 Task

Results 允许 Task 输出数据供下游 Task 使用。这里创建一个运行 Go 测试并输出测试结果的 Task:

apiVersion: tekton.dev/v1
kind: Task
metadata:
  name: go-unit-test
  namespace: tekton-pipelines
spec:
  params:
    - name: package
      type: string
      description: 测试的 Go 包路径
      default: "./..."
    - name: test-flags
      type: string
      description: 额外的 go test 参数
      default: "-v -count=1"
  workspaces:
    - name: source
      description: 包含源代码的工作区
  results:
    - name: test-status
      description: 测试结果状态(PASS/FAIL)
    - name: test-report
      description: 测试报告摘要
  steps:
    - name: run-tests
      image: golang:1.21-alpine
      script: |
        #!/bin/sh
        set -e

        cd "$(workspaces.source.path)"

        # 运行测试并捕获输出
        go test $(params.package) $(params.test-flags) 2>&1 | tee /tmp/test-output.txt

        # 检查测试结果
        if [ ${PIPESTATUS[0]} -eq 0 ]; then
          echo -n "PASS" > "$(results.test-status.path)"
          echo -n "所有测试通过" > "$(results.test-report.path)"
        else
          echo -n "FAIL" > "$(results.test-status.path)"
          head -c 4096 /tmp/test-output.txt > "$(results.test-report.path)"
          exit 1
        fi

步骤 4:完整的构建流水线 Task(组合使用)

现在我们将以上概念整合,编写一个真正的 CI 构建 Task:从 Git 检出代码 → 单元测试 → 使用 kaniko 构建 Docker 镜像 → 输出镜像摘要:

apiVersion: tekton.dev/v1
kind: Task
metadata:
  name: build-push-image
  namespace: tekton-pipelines
spec:
  params:
    - name: repo-url
      type: string
      description: Git 仓库地址
    - name: revision
      type: string
      description: Git 分支或 tag
      default: "main"
    - name: image-name
      type: string
      description: 镜像名称(不含标签)
    - name: image-tag
      type: string
      description: 镜像标签
      default: "latest"
    - name: dockerfile
      type: string
      description: Dockerfile 路径
      default: "./Dockerfile"
    - name: context
      type: string
      description: 构建上下文路径
      default: "."
  workspaces:
    - name: source
      description: 源代码工作区
    - name: dockerconfig
      description: Docker 认证配置(可选)
      optional: true
  results:
    - name: image-digest
      description: 构建的镜像摘要
    - name: image-url
      description: 完整的镜像 URL
  steps:
    - name: git-clone
      image: alpine/git:2.40.1
      script: |
        #!/bin/sh
        set -e
        cd "$(workspaces.source.path)"
        git init
        git remote add origin "$(params.repo-url)"
        git fetch --depth=1 origin "$(params.revision)"
        git checkout -f FETCH_HEAD
        echo "Commit: $(git log --oneline -1)"

    - name: unit-test
      image: golang:1.21-alpine
      script: |
        #!/bin/sh
        set -e
        cd "$(workspaces.source.path)"
        go test ./... -v -count=1
        echo "单元测试通过"

    - name: build-and-push
      image: gcr.io/kaniko-project/executor:v1.14.0
      script: |
        #!/bin/sh
        set -e

        # 构建镜像参数
        DESTINATION="$(params.image-name):$(params.image-tag)"

        if [ -d "$(workspaces.dockerconfig.path)" ]; then
          /kaniko/executor 
            --context="$(workspaces.source.path)/$(params.context)" 
            --dockerfile="$(workspaces.source.path)/$(params.dockerfile)" 
            --destination="$DESTINATION" 
            --docker-config="$(workspaces.dockerconfig.path)/config.json"
        else
          /kaniko/executor 
            --context="$(workspaces.source.path)/$(params.context)" 
            --dockerfile="$(workspaces.source.path)/$(params.dockerfile)" 
            --destination="$DESTINATION"
        fi

        # 输出镜像摘要
        echo -n "$DESTINATION" > "$(results.image-url.path)"
        echo "镜像构建并推送完成: $DESTINATION"

步骤 5:使用 Pipeline 编排多个 Task

通过 Pipeline 将上述多个 Task 串联起来,形成完整的 CI 流水线:

apiVersion: tekton.dev/v1
kind: Pipeline
metadata:
  name: ci-pipeline
  namespace: tekton-pipelines
spec:
  params:
    - name: repo-url
      type: string
    - name: revision
      type: string
      default: "main"
    - name: image-name
      type: string
    - name: image-tag
      type: string
      default: "latest"
  workspaces:
    - name: shared-workspace
    - name: docker-credentials
      optional: true
  tasks:
    - name: clone
      taskRef:
        name: git-clone
      params:
        - name: url
          value: "$(params.repo-url)"
        - name: revision
          value: "$(params.revision)"
      workspaces:
        - name: source
          workspace: shared-workspace

    - name: run-tests
      taskRef:
        name: go-unit-test
      runAfter:
        - clone
      workspaces:
        - name: source
          workspace: shared-workspace

    - name: build
      taskRef:
        name: build-push-image
      params:
        - name: repo-url
          value: "$(params.repo-url)"
        - name: revision
          value: "$(params.revision)"
        - name: image-name
          value: "$(params.image-name)"
        - name: image-tag
          value: "$(params.image-tag)"
      workspaces:
        - name: source
          workspace: shared-workspace
        - name: dockerconfig
          workspace: docker-credentials

步骤 6:创建 PipelineRun 执行完整流水线

# 创建 PipelineRun
kubectl create -f - <<EOF
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
  name: ci-pipeline-run-001
  namespace: tekton-pipelines
spec:
  pipelineRef:
    name: ci-pipeline
  params:
    - name: repo-url
      value: "https://github.com/example/my-app.git"
    - name: revision
      value: "main"
    - name: image-name
      value: "harbor.stellardata.top/library/my-app"
    - name: image-tag
      value: "v1.0.0-$(date +%Y%m%d-%H%M%S)"
  workspaces:
    - name: shared-workspace
      volumeClaimTemplate:
        spec:
          accessModes:
            - ReadWriteOnce
          resources:
            requests:
              storage: 1Gi
EOF

# 查看 PipelineRun 状态
tkn pr describe ci-pipeline-run-001 -n tekton-pipelines

# 实时查看日志
tkn pr logs ci-pipeline-run-001 -n tekton-pipelines -f

# 按 Task 分层查看
tkn pr logs ci-pipeline-run-001 -n tekton-pipelines -a

常见问题

Q1: Task 和 Step 执行失败时如何排查?

使用 tkn tr logs <taskrun-name> -n <namespace> -f 查看实时日志。如果 Step 失败,明确失败之前的最后一个输出。更深入的方式是 kubectl get pods -n tekton-pipelines | grep <taskrun-name> 获取 Pod 名称,然后 kubectl describe pod <pod-name> 查看容器退出状态码。

Q2: Workspace 挂载失败怎么办?

检查 Workspace 名称是否在 Task 声明中定义(workspaces 字段),且在 PipelineRun 中正确绑定。常见错误:使用了 optional: true 但未提供实际存储,而代码中又尝试访问该路径。使用 volumeClaimTemplate 自动创建 PVC 是最稳妥的方式。

Q3: 如何让 Task 支持动态参数(如构建时间戳)?

在 PipelineRun 中通过 $(params.xxx) 引用,也可以在 Shell 脚本中生成。如镜像标签中使用 $(date +%Y%m%d) 需要写成 Shell 表达式,在 Step 的 script 中直接执行即可。注意 Tekton 的变量替换只支持 $(params.xxx)$(workspaces.xxx.path) 等内置变量,不支持 Shell 内联。

Q4: Step 之间的环境变量如何传递?

每个 Step 运行在独立的容器中,环境变量不共享。需要使用 Workspace 文件系统传递:Step A 写入文件,Step B 读取。或者使用 results 输出,但 results 只能存储字符串(默认 4KB 限制)。对于大文件(如构建产物),始终使用 Workspace。

Q5: Task 多个 Step 能否并行执行?

不能。同一个 Task 内的 Step 是顺序执行的。如果需要并行,需要拆分为多个 Task,在 Pipeline 中用 runAfterfinally 控制编排。Tekton 的并行粒度是 Task 级别,而非 Step 级别。

总结

  1. Task 是 Tekton 的核心构建块:一个 Task 包含多个 Step,每个 Step 运行在独立容器中,共享 Workspace 和网络
  2. 参数化设计提高复用性:通过 paramsworkspacesresults 三要素,Task 可以像函数一样被任意 Pipeline 调用
  3. Workspace 是数据共享的基石:Git 代码、构建产物、配置信息都通过 Workspace 在 Step 之间传递
  4. Results 实现 Task 间通信:上游 Task 的 Results 可被下游 Task 通过 $(tasks.<task-name>.results.<result-name>) 引用
  5. Pipeline 编排驱动完整 CI 流程:通过 runAfter 控制依赖关系,finally 确保清理步骤始终执行

下一篇文章我们将深入 Tekton Pipeline 的多阶段编排与依赖管理,学习如何构建更复杂的生产级 CI 流水线。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片快捷回复

    暂无评论内容