대부분의 CI 도구는 쿠버네티스 바깥에 살면서 클러스터를 원격으로 조종한다. Jenkins 마스터, GitLab 러너, GitHub Actions 워커가 kubectl apply나 헬름 명령을 던지는 구조다. 익숙하지만, 파이프라인의 상태·권한·확장이 전부 CI 도구 쪽에 묶여 있어 클러스터의 RBAC, 시크릿, 오토스케일링과 결이 어긋난다. Tekton은 이 관계를 뒤집어, 파이프라인 자체를 쿠버네티스 커스텀 리소스(CRD)로 정의하고 각 단계를 파드(Pod)로 실행한다.

즉 Tekton에서 빌드 한 번은 곧 파드 하나이고, 파이프라인 실행은 클러스터 안의 오브젝트다. 덕분에 kubectl get pipelinerun으로 상태를 조회하고, 네임스페이스로 격리하고, 오토스케일러가 빌드 부하에 맞춰 노드를 늘린다. 이 글에서는 리소스 모델, 워크스페이스로 데이터를 넘기는 법, 트리거로 깃 이벤트에 반응하는 법, 실무 함정까지 실제 매니페스트와 함께 정리한다.

Tekton의 리소스 모델: 정의와 실행의 분리

Tekton을 이해하는 첫 관문은 정의(definition)와 실행(run)이 별도의 리소스라는 점이다. Task·Pipeline은 “무엇을 하는가”를 기술하는 재사용 템플릿이고, TaskRun·PipelineRun은 그 템플릿을 특정 파라미터로 한 번 실행한 인스턴스다. Jenkins의 잡(정의)과 빌드(#42 같은 실행)의 관계와 같지만, Tekton에서는 둘 다 쿠버네티스 오브젝트라 kubectl로 직접 다룬다.

Task는 순차 실행되는 스텝(step)들의 묶음이다. 한 Task 안의 모든 스텝은 같은 파드의 컨테이너로 실행되어 같은 볼륨을 공유한다. 그래서 “함께 상태를 공유해야 하는 작업”은 한 Task의 여러 스텝으로, “독립적으로 스케줄돼도 되는 작업”은 서로 다른 Task로 나눈다.

apiVersion: tekton.dev/v1
kind: Task
metadata:
  name: build-image
spec:
  params:
    - name: image-ref        # 빌드할 이미지 태그
      type: string
  workspaces:
    - name: source           # 소스 코드가 담긴 공유 볼륨
  steps:
    - name: build            # 스텝 1: 이미지 빌드 (같은 파드의 컨테이너)
      image: gcr.io/kaniko-project/executor:latest
      workingDir: $(workspaces.source.path)
      args:
        - --dockerfile=Dockerfile
        - --destination=$(params.image-ref)
        - --context=dir://$(workspaces.source.path)

여기서 $(params.image-ref)$(workspaces.source.path)는 변수 치환 문법으로, 실행 시점에 실제 값으로 대체된다. 값을 하드코딩하지 않고 파라미터로 열어두는 것이 재사용의 핵심이다.

Pipeline: Task를 DAG로 엮기

Pipeline은 여러 Task를 실행 순서와 데이터 흐름으로 엮은 것이다. runAfter로 순서를 지정하고, 지정하지 않으면 병렬로 실행된다. Tekton은 이 의존 관계를 방향성 비순환 그래프(DAG)로 해석해 서로 의존하지 않는 Task를 동시에 돌린다.

apiVersion: tekton.dev/v1
kind: Pipeline
metadata:
  name: build-and-deploy
spec:
  params:
    - name: repo-url
    - name: image-ref
  workspaces:
    - name: shared-data      # 파이프라인 레벨 워크스페이스
  tasks:
    - name: fetch            # 1. 소스 체크아웃
      taskRef: { name: git-clone }
      params:
        - { name: url, value: $(params.repo-url) }
      workspaces:
        - { name: output, workspace: shared-data }

    - name: unit-test        # 2. fetch 이후 실행
      runAfter: [fetch]
      taskRef: { name: run-tests }
      workspaces:
        - { name: source, workspace: shared-data }

    - name: build            # 3. fetch 이후 (unit-test와 병렬)
      runAfter: [fetch]
      taskRef: { name: build-image }
      params:
        - { name: image-ref, value: $(params.image-ref) }
      workspaces:
        - { name: source, workspace: shared-data }

위 예시에서 unit-testbuild는 둘 다 fetch에만 의존하므로 동시에 실행된다. “테스트를 통과해야만 빌드한다”는 정책을 원하면 buildrunAfter[unit-test]로 바꾸면 된다. 빌드가 무겁고 테스트가 자주 실패한다면 이 게이트가 자원을 아끼고, 둘 다 빠르다면 병렬이 총 소요시간을 줄인다.

워크스페이스: 스텝과 Task 사이 데이터 전달

서로 다른 Task는 서로 다른 파드에서 실행되므로, 파일을 공유하려면 워크스페이스(workspace)가 필요하다. Task 정의에서는 “이런 이름의 볼륨이 필요하다”는 선언일 뿐이고, 무엇을 마운트할지는 실행 시점에 결정된다. 소스처럼 Task 간에 넘겨야 하는 데이터는 하나의 PVC를 여러 Task가 공유하도록 붙인다.

apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
  generateName: build-and-deploy-   # 실행마다 고유 이름 자동 생성
spec:
  pipelineRef: { name: build-and-deploy }
  params:
    - { name: repo-url, value: https://example.com/team/app.git }
    - { name: image-ref, value: registry.internal/app:$(context.pipelineRun.uid) }
  workspaces:
    - name: shared-data
      volumeClaimTemplate:      # 실행마다 임시 PVC 생성 → 끝나면 정리
        spec:
          accessModes: [ReadWriteOnce]
          resources:
            requests:
              storage: 1Gi

volumeClaimTemplate은 실무에서 특히 중요하다. 고정 PVC를 재사용하면 여러 PipelineRun이 동시에 같은 볼륨에 쓰기를 시도해 충돌하거나, ReadWriteOnce PVC가 여러 노드의 파드에 붙지 못해 멈춘다. 실행마다 임시 PVC를 만드는 이 방식이 격리를 보장한다. 대신 프로비저닝 시간이 매 실행에 더해지므로, 초경량 파이프라인이라면 emptyDir로 한 Task 안에서 끝내는 편이 빠르다.

커밋 SHA나 이미지 다이제스트 같은 작은 값을 Task 간에 넘길 때는 워크스페이스 대신 results를 쓴다. Task가 결과를 지정된 파일에 쓰면 Tekton이 그 값을 변수로 노출한다.

spec:
  results:
    - name: digest           # 다른 Task가 참조할 결과값
  steps:
    - name: build
      image: gcr.io/kaniko-project/executor:latest
      args:
        - --digest-file=/tekton/results/digest   # 여기 쓰면 result가 됨
        - --destination=$(params.image-ref)

이후 Pipeline에서 $(tasks.build.results.digest)로 배포 Task에 넘긴다. 한 줄짜리 문자열이라면 results가 워크스페이스보다 가볍다.

Triggers: 깃 이벤트에 반응하기

지금까지는 사람이 kubectl create로 PipelineRun을 만들어야 실행됐다. 실제 CI가 되려면 깃 푸시나 PR 이벤트가 자동으로 파이프라인을 트리거해야 한다. 이 역할은 Tekton Triggers가 세 조각으로 맡는다. EventListener(웹훅 수신), TriggerBinding(값 추출), TriggerTemplate(PipelineRun 생성).

kind: TriggerBinding
metadata: { name: git-push-binding }
spec:
  params:  # 웹훅 JSON 본문에서 값 추출 → TriggerTemplate이 주입
    - { name: git-url,      value: $(body.repository.clone_url) }
    - { name: git-revision, value: $(body.head_commit.id) }

추출된 값은 TriggerTemplate에서 $(tt.params.git-url)로 참조되어 PipelineRun의 파라미터가 된다. 여기서 가장 중요한 건 웹훅 검증이다. 웹훅은 인증되지 않은 인터넷 트래픽을 받는 진입점이므로, Interceptor로 시그니처를 HMAC 검증하고 특정 브랜치·이벤트 타입만 통과시키는 필터를 걸어야 한다. 이를 빼먹으면 누구나 웹훅 URL로 임의의 파이프라인을 실행할 수 있다.

apiVersion: triggers.tekton.dev/v1beta1
kind: EventListener
metadata:
  name: github-listener
spec:
  serviceAccountName: tekton-triggers-sa
  triggers:
    - name: on-push
      interceptors:
        - ref: { name: github }             # 시그니처 검증
          params:
            - name: secretRef
              value: { secretName: github-webhook-secret, secretKey: token }
        - ref: { name: cel }                # main 브랜치만 통과
          params:
            - { name: filter, value: "body.ref == 'refs/heads/main'" }
      bindings:
        - ref: git-push-binding
      template:
        ref: git-push-template

RBAC와 시크릿: 클러스터 네이티브의 대가

Tekton이 클러스터 안에서 도는 대가는 권한 관리를 RBAC로 직접 해야 한다는 것이다. PipelineRun을 실행하는 서비스어카운트는 최소 권한만 가져야 한다. 배포 Task가 cluster-admin을 물고 있으면, 그 파이프라인을 트리거할 수 있는 누구든 클러스터를 장악한다.

레지스트리 인증은 서비스어카운트에 붙인 시크릿으로 처리한다. 자격 증명 시크릿을 secrets에 연결하면 Tekton이 빌드·푸시 스텝에 자동 주입한다.

# 레지스트리 자격 증명 시크릿 생성
kubectl create secret docker-registry registry-creds 
  --docker-server=registry.internal --docker-username=ci-bot 
  --docker-password="$REGISTRY_TOKEN" -n ci

# 파이프라인 전용 서비스어카운트에 연결
kubectl patch serviceaccount tekton-ci-sa -n ci 
  -p '{"secrets":[{"name":"registry-creds"}]}'

실무에서 마주치는 함정

Tekton으로 옮긴 팀이 초반에 걸려 넘어지는 지점은 대부분 “CI 도구가 아니라 쿠버네티스 워크로드”라는 사실을 잊어서 생긴다.

  • 워크스페이스 접근 모드 충돌: ReadWriteOnce PVC를 여러 노드의 병렬 Task가 공유하려 하면 뒤 파드가 Pending에 멈춘다. 같은 워크스페이스를 공유하려면 ReadWriteMany 스토리지가 필요하다.
  • PipelineRun 방치: 완료된 PipelineRun과 파드는 자동으로 지워지지 않는다. 방치하면 etcd에 오브젝트가 수천 개 쌓이므로 만료 정책이나 정리 컨트롤러를 두어야 한다.
  • 이미지 태그 mutable: latest로 스텝 이미지를 두면 어제 성공한 파이프라인이 오늘 실패한다. 이미지는 다이제스트로 핀 고정해야 재현성이 지켜진다.
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
  generateName: ci-run-
spec:
  pipelineRef: { name: build-and-deploy }
  taskRunTemplate:
    podTemplate:
      nodeSelector: { workload-type: ci }   # CI 전용 노드에 격리
      tolerations:
        - { key: dedicated, value: ci, effect: NoSchedule }

마무리

Tekton의 진짜 가치는 “또 하나의 CI 도구”가 아니라, CI를 쿠버네티스의 일급 시민으로 편입시킨다는 데 있다. 파이프라인은 CRD이고 빌드는 파드이므로 클러스터의 RBAC·시크릿·오토스케일링·격리를 그대로 물려받는다. 이미 쿠버네티스를 운영 기반으로 삼은 조직이라면 별도 CI 인프라 없이 클러스터 하나에서 배포와 빌드를 같은 언어로 다룰 수 있다.

다만 통합에는 대가가 있다. 워크스페이스 접근 모드, 완료된 실행의 정리, 웹훅 검증을 전부 쿠버네티스 방식으로 직접 챙겨야 한다. 매니지드 CI가 가려주던 세부가 그대로 드러나는 셈이다. 그래서 Tekton은 “쿠버네티스를 이미 깊이 쓰고, 파이프라인까지 그 모델로 통일하고 싶은 팀”에게 가장 잘 맞는다. 쿠버네티스가 낯선 팀이라면 러너를 붙이는 기존 CI가 당분간은 더 빠를 수 있다. 도구를 이념으로 고르기보다 팀이 어디에 익숙한지를 먼저 묻는 편이 정답에 가깝다.

자주 묻는 질문

Q. 한 Task에 스텝을 여러 개 두는 것과 Task를 여러 개로 쪼개는 것, 기준이 무엇인가요?
A. 상태(파일) 공유 여부가 기준입니다. 같은 볼륨을 순차로 이어 써야 하고 항상 함께 스케줄돼야 한다면 한 Task의 여러 스텝이 단순합니다. 독립 병렬 실행이나 재사용성을 원한다면 별도 Task로 나누는 것이 유연합니다.

Q. 빌드 캐시는 어떻게 재사용하나요? 매 실행마다 임시 PVC를 만들면 캐시가 사라지지 않나요?
A. 맞습니다. volumeClaimTemplate은 실행 격리에는 좋지만 캐시 재사용에는 불리합니다. 실행 간 공유하고 싶은 의존성·빌드 캐시는 별도의 영속 PVC를 캐시 전용 워크스페이스로 붙이거나, 레지스트리 기반 캐시가 실용적입니다. 실행마다 새로 받는 소스와 이어 써야 하는 캐시를 서로 다른 워크스페이스로 분리하는 설계가 핵심입니다.