Build a CI/CD Pipeline with Tekton & ArgoCD – Kubernetes Native

TL;DR – Quick Takeaways

  • Tekton + ArgoCD give you a fully declarative, Kubernetes‑native CI/CD stack that scales with your workloads.
  • Separate concerns: Tekton runs the build‑test‑containerize steps, while ArgoCD continuously syncs manifests from Git to clusters.
  • A healthy pipeline must handle failures — Tekton can abort and roll back, ArgoCD can auto‑revert to the last good commit.
  • Secure secrets with external vaults and Kubernetes CSI; never commit credentials.
  • Expect extra CPU/Memory overhead, but the payoff is observable speed, auditability, and GitOps‑driven safety.

Before you start, you need:

  • A Kubernetes 1.27+ cluster (managed or self‑hosted) with kubectl v1.27.
  • Helm 3.11 installed locally.
  • Tekton Pipelines v0.46.0 and Tekton Triggers v0.22.0 CRDs applied.
  • ArgoCD v2.7.0 installed in the same cluster (or a dedicated argocd namespace).
  • A Git repository (GitHub, GitLab, or Bitbucket) that holds your source code and Kustomize/Helm manifests.
  • Access to a secrets manager (HashiCorp Vault 1.13+, AWS Secrets Manager, or Azure Key Vault).

Introduction: The Rise of Cloud‑Native CI/CD

A senior developer at a fintech startup once saw a production rollback take 45 minutes because a Jenkins job hung on a flaky Docker layer. The incident cost the team a lost SLA window and a bruised reputation. When they switched to a Kubernetes‑native stack, the same pipeline completed in under ten seconds, and every change was replayable from Git.

The shift from monolithic CI servers to cloud‑native tools isn’t just hype. CNCF’s 2023 survey shows 71 % of Kubernetes users have embraced GitOps, with ArgoCD topping the list at 53 %. Organizations crave pipelines that live inside the cluster they manage, enabling the same scalability, observability, and security policies across both development and production.

Why Tekton + ArgoCD?

Tekton treats each CI step as a lightweight Kubernetes Pod, letting you reuse cluster resources and apply pod‑level policies. ArgoCD, on the other hand, continuously reconciles the desired state stored in Git with the live state on the cluster—exactly the GitOps promise. Coupling the two gives you a declarative, version‑controlled end‑to‑end workflow without lock‑in to a single vendor.

💡 Pro Tip: Keep your Tekton and ArgoCD manifests under separate directories (ci/ and cd/) in the same repo. This separation clarifies ownership and reduces merge conflicts.


Architectural Foundations: How Tekton and ArgoCD Work Together

Below is a high‑level view of the data flow from a developer’s push to a running service.

flowchart LR
    subgraph Dev[Developer]
        A[Git Push] --> B[Webhook]
    end
    subgraph CI[Tekton]
        B --> C[Trigger Event]
        C --> D[Build Task]
        D --> E[Test Task]
        E --> F[Containerize Task]
        F --> G[Push Image to Registry]
        G --> H[Update Kustomize base (image tag)]
    end
    subgraph CD[ArgoCD]
        H --> I[Git Commit (manifests)]
        I --> J[ArgoCD Application]
        J --> K[Sync to Staging]
        K --> L[Canary Rollout with Argo Rollouts]
        L --> M[Promote to Production]
    end
    style Dev fill:#f9f,stroke:#333,stroke-width:2px
    style CI fill:#bbf,stroke:#333,stroke-width:2px
    style CD fill:#bfb,stroke:#333,stroke-width:2px

Tekton components (Tasks, Pipelines, Triggers)

  • Task – a reusable step definition (e.g., build, test). Each runs in its own container image.
  • Pipeline – stitches tasks together, passing the output of one as input to the next.
  • Trigger – listens for external events (GitHub webhook, schedule) and starts a PipelineRun.

ArgoCD’s GitOps principles

ArgoCD watches a Git repository for changes to Kubernetes manifests. When a diff appears, it applies the new resources, records the operation, and optionally rolls back if health checks fail.

⚠️ Warning: ArgoCD does not manage image builds. Let Tekton handle that; ArgoCD only syncs manifests that reference the built image.

The combined CI/CD flow

  1. Developer pushes code → webhook fires.
  2. Tekton Trigger creates a PipelineRun.
  3. Tasks compile, test, containerize, and push the image.
  4. The final task updates the Kustomize overlay with the new image tag and commits back to Git.
  5. ArgoCD detects the commit, syncs the manifest to the staging namespace.
  6. If health checks pass, a promotion step (Argo Rollouts) moves the new version to production.

The separation lets teams debug CI failures without touching CD, and vice‑versa.


Prerequisites and Environment Setup

Kubernetes cluster requirements

A multi‑node 1.27 cluster with at least 3 CPU and 4 Gi free per node works for a modest POC. Make sure the API server has the admissionregistration.k8s.io/v1 API enabled; Tekton registers several mutating/validating webhooks.

# Verify version
kubectl version --short
# Example output:
# Client Version: v1.27.0
# Server Version: v1.27.3

Installing Tekton Pipelines and Triggers

# Install Tekton Pipelines
kubectl apply -f https://storage.googleapis.com/tekton-releases/pipeline/previous/v0.46.0/release.yaml

# Verify pods are ready
kubectl rollout status deployment tekton-pipelines-controller -n tekton-pipelines

# Install Tekton Triggers
kubectl apply -f https://storage.googleapis.com/tekton-releases/triggers/previous/v0.22.0/release.yaml

💡 Pro Tip: Pin the manifest URLs to a specific version (as shown) to avoid breaking changes during CI runs.

Deploying and configuring ArgoCD

# Install ArgoCD in its own namespace
kubectl create namespace argocd
helm repo add argo https://argoproj.github.io/argo-helm
helm install argocd argo/argo-cd -n argocd --version 5.51.0

Expose the UI via port‑forward for initial configuration:

kubectl port-forward svc/argocd-server -n argocd 8080:443

Log in with the autogenerated admin password:

kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d

Once logged in, create a Project that scopes the CI/CD repos:

# argocd/project.yaml
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: fintech-platform
  namespace: argocd
spec:
  description: Project for fintech microservices
  sourceRepos:
    - https://github.com/nileshblog.tech/fintech-platform.git
  destinations:
    - namespace: staging
      server: '*'
    - namespace: production
      server: '*'
  clusterResourceWhitelist:
    - group: '*'
      kind: '*'
kubectl apply -f argocd/project.yaml

Step‑by‑Step Implementation: Building the Pipeline

1. Creating Tekton Tasks (Build, Test, Containerize)

Below are three core tasks. Each includes error handling and uses specific container images that ship with the required tools.

Build Task (task-build.yaml)

apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
  name: build-go
  labels:
    version: "0.1"
spec:
  params:
    - name: git-url
      description: Repository URL
    - name: revision
      description: Git revision (branch/tag/sha)
  workspaces:
    - name: source
  steps:
    - name: clone
      image: alpine/git:2.42.0
      script: |
        #!/bin/sh
        set -euo pipefail
        git clone $(params.git-url) $(workspaces.source.path)/src
        cd $(workspaces.source.path)/src
        git checkout $(params.revision)
    - name: compile
      image: golang:1.21-alpine3.18
      workingDir: $(workspaces.source.path)/src
      script: |
        #!/bin/sh
        set -euo pipefail
        go mod tidy || { echo "go mod failed"; exit 1; }
        go build -o /workspace/output/app .
    - name: output
      image: busybox:1.36.1
      script: |
        #!/bin/sh
        cp /workspace/output/app $(workspaces.source.path)/src/app

Test Task (task-test.yaml)

apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
  name: test-go
spec:
  workspaces:
    - name: source
  steps:
    - name: unit-test
      image: golang:1.21-alpine3.18
      workingDir: $(workspaces.source.path)/src
      script: |
        #!/bin/sh
        set -euo pipefail
        go test ./... -coverprofile=cover.out || { echo "Tests failed"; exit 1; }

Containerize Task (task-containerize.yaml)

apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
  name: containerize
spec:
  params:
    - name: image
      description: Full image name (registry/repo:tag)
  workspaces:
    - name: source
  steps:
    - name: build-push
      image: gcr.io/kaniko-project/executor:latest
      env:
        - name: DOCKER_CONFIG
          value: /tekton/home/.docker
      script: |
        #!/busybox/sh
        set -e
        /kaniko/executor \
          --dockerfile=$(workspaces.source.path)/src/Dockerfile \
          --context=$(workspaces.source.path)/src \
          --destination=$(params.image) \
          --snapshotMode=redo || { echo "Kaniko failed"; exit 1; }

⚠️ Warning: Kaniko requires write access to the target registry. Attach a docker-registry secret to the task’s ServiceAccount.

2. Constructing the Tekton Pipeline and Wiring Triggers

Create a pipeline that stitches the three tasks together and passes the built image tag downstream.

apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
  name: ci-pipeline
spec:
  params:
    - name: git-url
    - name: revision
    - name: image-repo
  workspaces:
    - name: shared-workspace
  tasks:
    - name: build
      taskRef:
        name: build-go
      params:
        - name: git-url
          value: $(params.git-url)
        - name: revision
          value: $(params.revision)
      workspaces:
        - name: source
          workspace: shared-workspace
    - name: test
      runAfter:
        - build
      taskRef:
        name: test-go
      workspaces:
        - name: source
          workspace: shared-workspace
    - name: containerize
      runAfter:
        - test
      taskRef:
        name: containerize
      params:
        - name: image
          value: "$(params.image-repo):$(params.revision)"
      workspaces:
        - name: source
          workspace: shared-workspace

Now wire a TriggerTemplate that creates a PipelineRun whenever a GitHub push occurs.

apiVersion: triggers.tekton.dev/v1beta1
kind: TriggerTemplate
metadata:
  name: ci-trigger-template
spec:
  params:
    - name: git-url
    - name: revision
    - name: image-repo
  resourcetemplates:
    - apiVersion: tekton.dev/v1beta1
      kind: PipelineRun
      metadata:
        generateName: ci-run-
      spec:
        pipelineRef:
          name: ci-pipeline
        params:
          - name: git-url
            value: $(tt.params.git-url)
          - name: revision
            value: $(tt.params.revision)
          - name: image-repo
            value: $(tt.params.image-repo)
        workspaces:
          - name: shared-workspace
            volumeClaimTemplate:
              metadata:
                name: ci-ws
              spec:
                accessModes: ["ReadWriteOnce"]
                resources:
                  requests:
                    storage: 2Gi

Finally, bind the template to a TriggerBinding that extracts data from the GitHub webhook payload.

apiVersion: triggers.tekton.dev/v1beta1
kind: TriggerBinding
metadata:
  name: github-push-binding
spec:
  params:
    - name: git-url
      value: $(body.repository.clone_url)
    - name: revision
      value: $(body.ref) # e.g., refs/heads/main
    - name: image-repo
      value: "registry.example.com/nileshblog.tech/app"

Create an EventListener service to expose the webhook endpoint.

apiVersion: triggers.tekton.dev/v1beta1
kind: EventListener
metadata:
  name: ci-listener
spec:
  serviceAccountName: tekton-triggers-admin
  triggers:
    - name: github-push
      bindings:
        - ref: github-push-binding
      template:
        ref: ci-trigger-template

Expose it via an Ingress or LoadBalancer so GitHub can POST events.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ci-ingress
  annotations:
    kubernetes.io/ingress.class: nginx
spec:
  rules:
    - host: ci.nileshblog.tech
      http:
        paths:
          - path: /hook
            pathType: Prefix
            backend:
              service:
                name: el-ci-listener
                port:
                  number: 8080

💡 Pro Tip: Enable GitHub secret verification (HMAC) on the listener to avoid spoofed events.

3. Defining ArgoCD Applications for Staging and Production

Create two ArgoCD Application CRs. Both point at the same repo, but each targets a different namespace and folder.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: fintech-staging
  namespace: argocd
spec:
  project: fintech-platform
  source:
    repoURL: https://github.com/nileshblog.tech/fintech-platform.git
    targetRevision: HEAD
    path: manifests/staging
  destination:
    server: https://kubernetes.default.svc
    namespace: staging
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: fintech-production
  namespace: argocd
spec:
  project: fintech-platform
  source:
    repoURL: https://github.com/nileshblog.tech/fintech-platform.git
    targetRevision: HEAD
    path: manifests/production
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

When the containerize task pushes a new image, it also updates the kustomization.yaml in the staging overlay:

# manifests/staging/kustomization.yaml
resources:
  - ../../base
images:
  - name: registry.example.com/nileshblog.tech/app
    newTag: "main-$(git rev-parse --short HEAD)"

The Git commit performed by the Tekton task triggers ArgoCD to sync the staging application. After automated health checks (readiness probes, smoke tests), a promotion step runs Argo Rollouts.

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: fintech-api
  namespace: production
spec:
  replicas: 3
  strategy:
    canary:
      steps:
        - setWeight: 25
        - pause: {duration: 30s}
        - setWeight: 50
        - pause: {duration: 30s}
        - setWeight: 100
  selector:
    matchLabels:
      app: fintech-api
  template:
    metadata:
      labels:
        app: fintech-api
    spec:
      containers:
        - name: api
          image: registry.example.com/nileshblog.tech/app:{{.Values.imageTag}}
          ports:
            - containerPort: 8080

ArgoCD watches the Rollout resource and reports progressive rollout status directly in its UI.


Advanced Patterns and Production Considerations

Implementing Canary Deployments with Argo Rollouts

The previous snippet already shows a simple canary. To make it traffic‑aware, install the analytics plugin:

kubectl apply -f https://raw.githubusercontent.com/argoproj/argo-rollouts/v1.6.2/manifests/install.yaml

Annotate the service with rollouts.kubernetes.io/traffic-weight to let the controller adjust load balancer percentages automatically.

Securing the Pipeline (Credentials, Secrets, RBAC)

External Secret Management

Use the External Secrets Operator to sync Vault secrets into Kubernetes Secret objects.

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: docker-registry-cred
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: vault-backend
    kind: SecretStore
  target:
    name: reg-cred
    creationPolicy: Owner
  data:
    - secretKey: .dockerconfigjson
      remoteRef:
        key: secret/data/docker/registry
        property: config

Mount the reg-cred secret into Tekton tasks via serviceAccountName that references it. For ArgoCD, enable the Vault plugin:

apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-cm
  namespace: argocd
data:
  configManagementPlugins: |
    - name: vault-plugin
      init:
        command: ["sh", "-c"]
        args: ["vault kv get -field=data secret/argocd"]

RBAC Isolation

Grant Tekton’s ServiceAccount only the create and get permissions on Pods in the ci namespace. Deny any delete on production resources.

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: tekton-ci-role
  namespace: ci
rules:
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["create", "get", "list"]
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: tekton-ci-binding
  namespace: ci
subjects:
  - kind: ServiceAccount
    name: tekton-triggers-admin
    namespace: tekton-pipelines
roleRef:
  kind: Role
  name: tekton-ci-role
  apiGroup: rbac.authorization.k8s.io

Cost Optimization and Performance Tuning

Running both Tekton and ArgoCD introduces multiple controllers, each with its own pod. To keep cloud spend under control:

  • Pod autoscaling – enable Horizontal Pod Autoscaler (HPA) on Tekton controller (cpu: 500m) and ArgoCD repo‑server (cpu: 300m).
  • Image caching – configure Kaniko’s cache=true flag; this reduces rebuild time for unchanged layers.
  • Garbage collection – schedule a nightly job that deletes completed PipelineRun objects older than 30 days.

💡 Pro Tip: For workloads that rarely change, set replicas: 1 on the ArgoCD application-controller. Scale up only during heavy release cycles.


Trade‑offs, Challenges, and Alternatives

Tekton vs. GitHub Actions / Jenkins X

AspectTekton (K8s‑native)GitHub Actions (SaaS)Jenkins X (Hybrid)
Resource overheadMultiple controllers (~500 mCPU each)Negligible (hosted)Moderate (Jenkins master)
FlexibilityUnlimited custom containersLimited to actions marketplaceGood, but opinionated
Learning curveSteep (CRDs, YAML)LowMedium
Portability100 % cluster‑agnosticTied to GitHubWorks on K8s but needs plugins

My take: If your organization already lives in Kubernetes and you need fine‑grained security policies, the extra CPU cost of Tekton pays off in auditability and isolation.

ArgoCD vs. Flux

  • ArgoCD shines with a rich UI, health checks, and built‑in support for Helm/Kustomize.
  • Flux offers a smaller controller footprint and tighter integration with gitops-toolkit.
  • Both use the same GitOps reconciliation loop; the choice often boils down to operational preference.

Common pitfalls and debugging tips

  • Missing webhook secret – Verify that GitHub’s secret matches the EventListener signature.
  • PVC quota errors – Tekton’s workspaces use PersistentVolumeClaims; ensure the namespace has sufficient quota.
  • Image pull failures – Double‑check that the docker-registry secret resides in the same namespace as the task pod.
  • ArgoCD sync loops – If a manifest references a non‑existent ConfigMap, ArgoCD will repeatedly attempt to apply and mark the app OutOfSync. Use kubectl diff locally to spot the mismatch.

Common Errors & Fixes

SymptomLikely CauseFix
Tekton TaskRun pod fails with “permission denied”ServiceAccount lacks secret read permissionBind secret-reader Role to the task’s ServiceAccount.
ArgoCD Application shows “Health: Degraded”Liveness probe timeout after rolloutIncrease initialDelaySeconds or check container log for startup errors.
Kaniko executor exits with “no such file or directory”Dockerfile path mismatchEnsure --dockerfile points to the correct relative path inside the workspace.
GitHub webhook returns 404Ingress host misconfiguredVerify DNS resolves ci.nileshblog.tech to the LoadBalancer IP.
PipelineRun stalls at “Pending”No matching PVC for workspaceCreate a StorageClass with appropriate provisioner and set volumeClaimTemplate.storageClassName.

Conclusion

When to choose this stack

  • Your workloads already run on Kubernetes and you want full auditability through GitOps.
  • You need independent scaling of CI (builds, tests) and CD (deployment sync).
  • Your organization values declarative pipelines that survive cluster migrations.

The future of K8s‑native CI/CD

The CNCF community continues to invest in Tekton and Argo projects, adding features like pipelines-as-code, policy enforcement, and progressive delivery. Expect tighter integration with Service Meshes and AI‑assisted test selection, making the combination even more powerful for enterprise‑scale delivery.

⚠️ Warning: Introducing multiple controllers inevitably raises the operational surface. Pair this stack with robust monitoring (Prometheus + Grafana) and a clear ownership model to keep complexity in check.


Call to Action

If you found this guide helpful, drop a comment below, share it with your team, or subscribe to the newsletter on nileshblog.tech for more deep‑dive articles on cloud‑native engineering.


Author Bio:
I’m Nilesh Raut, a Software Development Engineer with 2+ years of experience, specializing in Go, JavaScript, Python, Docker, Kubernetes, Git, Jenkins, microservices, and system design (LLD/HLD), backed by a strong foundation in data structures and algorithms. Alongside my engineering journey, I bring 4+ years of hands‑on experience in SEO, where I’ve worked extensively on content strategy, keyword research, technical SEO, and organic growth, helping products and businesses scale efficiently by aligning solid technology with search‑driven performance.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top