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:
parent
633f3f6e46
commit
fb2edcac9e
8 changed files with 519 additions and 0 deletions
22
apps/ci/pipeline.yaml
Normal file
22
apps/ci/pipeline.yaml
Normal 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
24
apps/ci/tekton.yaml
Normal 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
216
docs/04-tekton-pipeline.md
Normal 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 01–03 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 (~3–5 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 ~2–3 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.
|
||||
173
manifests/ci/pipeline/pipeline.yaml
Normal file
173
manifests/ci/pipeline/pipeline.yaml
Normal 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)
|
||||
32
manifests/ci/pipeline/pipelinerun.yaml
Normal file
32
manifests/ci/pipeline/pipelinerun.yaml
Normal 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
|
||||
8
manifests/ci/pipeline/serviceaccount.yaml
Normal file
8
manifests/ci/pipeline/serviceaccount.yaml
Normal 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>
|
||||
6
manifests/ci/tekton/kustomization.yaml
Normal file
6
manifests/ci/tekton/kustomization.yaml
Normal 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
38
scripts/set-git-credentials.sh
Executable 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"
|
||||
Loading…
Add table
Reference in a new issue