feat(ex04): Tekton pipeline — GitOps image-tag bump loop

- apps/ci/tekton.yaml: installs Tekton v0.65.1 via kustomize remote ref
- apps/ci/pipeline.yaml: deploys pipeline resources via ArgoCD
- manifests/ci/tekton/kustomization.yaml: points to upstream release
- manifests/ci/pipeline/serviceaccount.yaml: pipeline-runner SA
- manifests/ci/pipeline/pipeline.yaml: 4-task Pipeline (clone, validate, bump, push)
- manifests/ci/pipeline/pipelinerun.yaml: bumps podinfo 6.6.2 → 6.7.0
- scripts/set-git-credentials.sh: creates git-credentials Secret (not in git)
- docs/04-tekton-pipeline.md: Exercise 04 participant guide
This commit is contained in:
Paul Harkink 2026-02-28 15:32:39 +01:00
parent 633f3f6e46
commit fb2edcac9e
8 changed files with 519 additions and 0 deletions

22
apps/ci/pipeline.yaml Normal file
View file

@ -0,0 +1,22 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: workshop-pipeline
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "6"
spec:
project: workshop
source:
repoURL: https://github.com/innspire/ops-demo.git
targetRevision: HEAD
path: manifests/ci/pipeline
destination:
server: https://kubernetes.default.svc
namespace: tekton-pipelines
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true

24
apps/ci/tekton.yaml Normal file
View file

@ -0,0 +1,24 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: tekton
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "5"
spec:
project: workshop
source:
repoURL: https://github.com/innspire/ops-demo.git
targetRevision: HEAD
path: manifests/ci/tekton
kustomize: {}
destination:
server: https://kubernetes.default.svc
namespace: tekton-pipelines
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ServerSideApply=true

216
docs/04-tekton-pipeline.md Normal file
View file

@ -0,0 +1,216 @@
# Exercise 04 — Tekton Pipeline (GitOps Loop)
**Time**: ~45 min
**Goal**: Build an automated pipeline that bumps the podinfo image tag in Git and watches ArgoCD roll out the new version — the full GitOps CI/CD loop.
---
## What you'll learn
- Tekton concepts: Task, Pipeline, PipelineRun, Workspace
- How a pipeline commits to Git to trigger a GitOps deployment (no container registry needed)
- The full loop: pipeline push → ArgoCD detects → rolling update → new version in browser
---
## The loop visualised
```
You trigger PipelineRun
Task 1: clone repo
Task 2: validate manifests (kubectl dry-run)
Task 3: bump image tag → deployment.yaml: 6.6.2 → 6.7.0
Task 4: git commit + push
ArgoCD polls repo (or click Refresh)
ArgoCD syncs podinfo Deployment
Rolling update → podinfo v6.7.0 in your browser
```
---
## Prerequisites
Exercises 0103 complete. podinfo is reachable at **http://podinfo.192.168.56.200.nip.io** and shows version **6.6.2**.
You need:
- A GitHub account with write access to the `ops-demo` repo
- A GitHub Personal Access Token (PAT) with **repo** scope
---
## Steps
### 1. Verify Tekton is installed
The `apps/ci/tekton.yaml` and `apps/ci/pipeline.yaml` ArgoCD Applications are
already in the repo. ArgoCD is installing Tekton via a kustomize remote reference.
Wait for the install to complete (~35 min after the app appears in ArgoCD):
```bash
kubectl get pods -n tekton-pipelines
# NAME READY STATUS RESTARTS
# tekton-pipelines-controller-xxx 1/1 Running 0
# tekton-pipelines-webhook-xxx 1/1 Running 0
```
Also check that the pipeline resources are synced:
```bash
kubectl get pipeline -n tekton-pipelines
# NAME AGE
# gitops-image-bump Xm
```
---
### 2. Set up Git credentials
The pipeline needs to push a commit to GitHub. Create a Personal Access Token:
1. Go to **GitHub → Settings → Developer settings → Personal access tokens → Fine-grained tokens** (or classic with `repo` scope)
2. Give it write access to the `ops-demo` repository
Then create the Kubernetes Secret (this command replaces the placeholder Secret if it already exists):
```bash
./scripts/set-git-credentials.sh <your-github-username> <your-pat>
```
Verify:
```bash
kubectl get secret git-credentials -n tekton-pipelines
# NAME TYPE DATA AGE
# git-credentials Opaque 2 5s
```
---
### 3. Trigger the pipeline
Apply the PipelineRun (this is the only `kubectl apply` you'll run in this exercise):
```bash
kubectl apply -f manifests/ci/pipeline/pipelinerun.yaml
```
Watch it run:
```bash
kubectl get pipelinerun -n tekton-pipelines -w
```
Or follow the logs with tkn (Tekton CLI, optional):
```bash
# If tkn is installed:
tkn pipelinerun logs -f -n tekton-pipelines bump-podinfo-to-670
```
Or follow individual TaskRun pods:
```bash
kubectl get pods -n tekton-pipelines -w
# Once a pod appears, you can:
kubectl logs -n tekton-pipelines <pod-name> -c step-bump --follow
```
The PipelineRun should complete in ~23 minutes.
---
### 4. Verify the commit
```bash
# Inside the VM — check the latest commit on the remote
git fetch origin
git log origin/main --oneline -3
# You should see something like:
# a1b2c3d chore(pipeline): bump podinfo to 6.7.0
# ...
```
Or check GitHub directly in your browser.
---
### 5. Watch ArgoCD sync
In the ArgoCD UI, click **Refresh** on the **podinfo** application.
ArgoCD will detect that `manifests/apps/podinfo/deployment.yaml` changed and
start a rolling update.
```bash
kubectl rollout status deployment/podinfo -n podinfo
# deployment "podinfo" successfully rolled out
```
---
### 6. Confirm in the browser
Open **http://podinfo.192.168.56.200.nip.io** — you should now see **version 6.7.0**.
```bash
curl http://podinfo.192.168.56.200.nip.io | jq .version
# "6.7.0"
```
The full loop is complete.
---
## Expected outcome
```
PipelineRun STATUS: Succeeded
deployment.yaml image tag: 6.7.0
podinfo UI version: 6.7.0
```
---
## Re-running the pipeline
The `PipelineRun` name must be unique. To run again:
```bash
# Option A: delete and re-apply with same name
kubectl delete pipelinerun bump-podinfo-to-670 -n tekton-pipelines
kubectl apply -f manifests/ci/pipeline/pipelinerun.yaml
# Option B: create a new run with a different name
kubectl create -f manifests/ci/pipeline/pipelinerun.yaml
```
---
## Troubleshooting
| Symptom | Fix |
|---------|-----|
| PipelineRun stuck in "Running" forever | `kubectl describe pipelinerun -n tekton-pipelines bump-podinfo-to-670` |
| `git-credentials` Secret not found | Run `./scripts/set-git-credentials.sh` first |
| Push fails: 403 Forbidden | PAT has insufficient scope — needs `repo` write access |
| Push fails: remote already has this commit | Image tag already at 6.7.0; the pipeline is idempotent (nothing to push) |
| ArgoCD not syncing after push | Click **Refresh** in the UI; default poll interval is 3 min |
| Validate task fails | Check `kubectl apply --dry-run=client -f manifests/apps/podinfo/` manually |
---
## What's next
Exercise 05 is a quick wrap-up: you'll look at the full picture of what you built
and optionally trigger another upgrade cycle to cement the GitOps loop.
If you have time, try the **Bonus Exercise 06**: deploy Prometheus + Grafana and
see cluster and podinfo metrics in a live dashboard.

View file

@ -0,0 +1,173 @@
apiVersion: tekton.dev/v1
kind: Pipeline
metadata:
name: gitops-image-bump
namespace: tekton-pipelines
spec:
description: |
Validates manifests, bumps the podinfo image tag in deployment.yaml,
and pushes the commit back to the ops-demo repo.
ArgoCD then detects the change and rolls out the new image.
params:
- name: repo-url
type: string
description: URL of the ops-demo git repository
default: https://github.com/innspire/ops-demo.git
- name: new-tag
type: string
description: New podinfo image tag to set (e.g. 6.7.0)
default: "6.7.0"
- name: git-user-name
type: string
description: Git author name for the bump commit
default: "Workshop Pipeline"
- name: git-user-email
type: string
description: Git author email for the bump commit
default: "pipeline@workshop.local"
workspaces:
- name: source
description: Workspace for cloning the repo
- name: git-credentials
description: Secret with GitHub username + PAT (basic-auth)
tasks:
# ── Task 1: Clone the repo ─────────────────────────────────────────────
- name: clone
taskSpec:
workspaces:
- name: source
- name: git-credentials
params:
- name: repo-url
- name: git-user-name
- name: git-user-email
steps:
- name: clone
image: alpine/git:latest
workingDir: /workspace/source
env:
- name: GIT_USERNAME
valueFrom:
secretKeyRef:
name: git-credentials
key: username
- name: GIT_PASSWORD
valueFrom:
secretKeyRef:
name: git-credentials
key: password
script: |
#!/bin/sh
set -eu
# Inject credentials into the clone URL
REPO=$(echo "$(params.repo-url)" | sed "s|https://|https://${GIT_USERNAME}:${GIT_PASSWORD}@|")
git clone "${REPO}" .
git config user.name "$(params.git-user-name)"
git config user.email "$(params.git-user-email)"
echo "Cloned $(git log --oneline -1)"
workspaces:
- name: source
workspace: source
- name: git-credentials
workspace: git-credentials
params:
- name: repo-url
value: $(params.repo-url)
- name: git-user-name
value: $(params.git-user-name)
- name: git-user-email
value: $(params.git-user-email)
# ── Task 2: Validate manifests (dry-run) ──────────────────────────────
- name: validate
runAfter: [clone]
taskSpec:
workspaces:
- name: source
steps:
- name: dry-run
image: bitnami/kubectl:latest
workingDir: /workspace/source
script: |
#!/bin/sh
set -eu
echo "Running kubectl dry-run on manifests/apps/podinfo/"
kubectl apply --dry-run=client -f manifests/apps/podinfo/
echo "Validation passed."
workspaces:
- name: source
workspace: source
# ── Task 3: Bump image tag ─────────────────────────────────────────────
- name: bump-image-tag
runAfter: [validate]
taskSpec:
workspaces:
- name: source
params:
- name: new-tag
steps:
- name: bump
image: mikefarah/yq:4.44.3
workingDir: /workspace/source
script: |
#!/bin/sh
set -eu
FILE="manifests/apps/podinfo/deployment.yaml"
CURRENT=$(yq '.spec.template.spec.containers[0].image' "${FILE}")
echo "Current image: ${CURRENT}"
yq -i '.spec.template.spec.containers[0].image = "ghcr.io/stefanprodan/podinfo:$(params.new-tag)"' "${FILE}"
UPDATED=$(yq '.spec.template.spec.containers[0].image' "${FILE}")
echo "Updated image: ${UPDATED}"
workspaces:
- name: source
workspace: source
params:
- name: new-tag
value: $(params.new-tag)
# ── Task 4: Commit and push ────────────────────────────────────────────
- name: git-commit-push
runAfter: [bump-image-tag]
taskSpec:
workspaces:
- name: source
- name: git-credentials
params:
- name: new-tag
steps:
- name: push
image: alpine/git:latest
workingDir: /workspace/source
env:
- name: GIT_USERNAME
valueFrom:
secretKeyRef:
name: git-credentials
key: username
- name: GIT_PASSWORD
valueFrom:
secretKeyRef:
name: git-credentials
key: password
script: |
#!/bin/sh
set -eu
git add manifests/apps/podinfo/deployment.yaml
git commit -m "chore(pipeline): bump podinfo to $(params.new-tag)"
# Inject credentials for push
REMOTE_URL=$(git remote get-url origin | sed "s|https://|https://${GIT_USERNAME}:${GIT_PASSWORD}@|")
git push "${REMOTE_URL}" HEAD:main
echo "Pushed commit: $(git log --oneline -1)"
workspaces:
- name: source
workspace: source
- name: git-credentials
workspace: git-credentials
params:
- name: new-tag
value: $(params.new-tag)

View file

@ -0,0 +1,32 @@
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
# Change the name (e.g. bump-to-670-run2) each time you trigger the pipeline,
# or delete the old PipelineRun first — names must be unique.
name: bump-podinfo-to-670
namespace: tekton-pipelines
spec:
pipelineRef:
name: gitops-image-bump
taskRunTemplate:
serviceAccountName: pipeline-runner
params:
- name: repo-url
value: https://github.com/innspire/ops-demo.git
- name: new-tag
value: "6.7.0"
- name: git-user-name
value: "Workshop Pipeline"
- name: git-user-email
value: "pipeline@workshop.local"
workspaces:
- name: source
volumeClaimTemplate:
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 1Gi
- name: git-credentials
secret:
secretName: git-credentials

View file

@ -0,0 +1,8 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: pipeline-runner
namespace: tekton-pipelines
# The git-credentials Secret is NOT in this repo (it contains a real GitHub token).
# Create it before running the pipeline:
# ./scripts/set-git-credentials.sh <github-user> <github-pat>

View file

@ -0,0 +1,6 @@
# Installs Tekton Pipelines v0.65.1 via kustomize remote reference.
# ArgoCD applies this with its built-in kustomize support.
# Images are pre-pulled by the Vagrantfile, so this only needs network to
# fetch the manifest once (not the images).
resources:
- https://storage.googleapis.com/tekton-releases/pipeline/previous/v0.65.1/release.yaml

38
scripts/set-git-credentials.sh Executable file
View file

@ -0,0 +1,38 @@
#!/usr/bin/env bash
# set-git-credentials.sh — Create the git-credentials Secret for the Tekton pipeline.
#
# Usage:
# ./scripts/set-git-credentials.sh <github-username> <github-pat>
#
# The PAT needs: repo (read + write) scope.
# The Secret is NOT stored in git — it lives only in the cluster.
#
# Run this once before triggering the PipelineRun.
set -euo pipefail
if [[ $# -ne 2 ]]; then
echo "Usage: $0 <github-username> <github-personal-access-token>"
exit 1
fi
GITHUB_USER="$1"
GITHUB_PAT="$2"
NAMESPACE="tekton-pipelines"
echo "→ Creating git-credentials Secret in namespace ${NAMESPACE}"
kubectl create secret generic git-credentials \
--namespace "${NAMESPACE}" \
--from-literal=username="${GITHUB_USER}" \
--from-literal=password="${GITHUB_PAT}" \
--dry-run=client -o yaml | kubectl apply -f -
echo "✓ Secret created. The pipeline is ready to run."
echo ""
echo " Trigger the pipeline:"
echo " kubectl apply -f manifests/ci/pipeline/pipelinerun.yaml"
echo ""
echo " Watch progress:"
echo " kubectl get pipelinerun -n tekton-pipelines -w"
echo " # or use: tkn pipelinerun logs -f -n tekton-pipelines bump-podinfo-to-670"