Kubernetes Deploy
A pipeline that deploys an image from an internal registry (Harbor/Nexus) to a Kubernetes cluster
After building and pushing an image to an internal container registry (Harbor/Nexus), the Kubernetes cluster pulls that image and deploys it. It targets a generic / on-premise cluster (not managed GKE/EKS) and authenticates via kubeconfig instead of cloud IAM, so it works in airgapped environments too.
Prerequisites
Register the following secrets in advance:
| Type | Key | Description |
|---|---|---|
| Secret | REGISTRY_URL | Internal registry host (e.g., harbor.internal, nexus.internal:8083) |
| Secret | REGISTRY_USERNAME | Registry account username (push + cluster pull). Harbor robot / Nexus user, etc. |
| Secret | REGISTRY_PASSWORD | Registry account password |
| Secret | KUBE_CONFIG | Base64-encoded kubeconfig for the target cluster |
| Secret | SLACK_WEBHOOK | (Optional) Webhook URL for deploy notifications |
For the cluster to pull images from the internal registry, the Deployment pod spec must include imagePullSecrets: [name: regcred]. The pipeline above creates the regcred secret idempotently. In an onprem (airgapped) environment, mirror the alpine:3.19 and alpine/kubectl:1.34.1 base images into the internal registry in advance.
Full Code
name: k8s-deploy
# After building and pushing an image to an internal registry (Harbor/Nexus),
# the Kubernetes cluster pulls that image and deploys it. Targets a generic /
# on-premise cluster (not managed GKE/EKS) — authenticates via kubeconfig(secret)
# instead of cloud IAM, so it also works in airgapped environments.
triggers:
push:
branches: [main] # Runs on push to main
env:
IMAGE_REPO: "my-project/my-app" # Image path inside the internal registry (<project>/<image>)
K8S_NAMESPACE: "production" # Target namespace
DEPLOYMENT_NAME: "my-app" # Deployment to update
CONTAINER_NAME: "app" # Container name inside the Deployment
IMAGE_PULL_SECRET: "regcred" # imagePullSecret name for internal registry pull
jobs:
# 1. Build image + push to internal registry (auth -> build in one job)
build:
phase: build
steps:
- name: checkout
uses: "collabops/checkout@v2"
with:
repo-url: "https://<collabops-host>/<workspace>/<repository>.git"
# Create .docker/config.json with an internal registry account (Harbor robot / Nexus user, etc.) — docker-build-push auto-loads it
- name: registry-login
image: alpine:3.19
env:
REGISTRY_URL: "${{ secrets.REGISTRY_URL }}"
REGISTRY_USERNAME: "${{ secrets.REGISTRY_USERNAME }}"
REGISTRY_PASSWORD: "${{ secrets.REGISTRY_PASSWORD }}"
run: |
set -euo pipefail
cd /workspace/source
auth=$(printf '%s:%s' "$REGISTRY_USERNAME" "$REGISTRY_PASSWORD" | base64 | tr -d '\n')
mkdir -p .docker
printf '{"auths":{"%s":{"auth":"%s"}}}\n' "$REGISTRY_URL" "$auth" > .docker/config.json
grep -qxF '.docker' .dockerignore 2>/dev/null || echo '.docker' >> .dockerignore
- name: docker-build-push
uses: "collabops/docker-build-push@v1"
with:
# Push to the internal registry with an immutable tag (commit sha) + latest
tags: |
${{ secrets.REGISTRY_URL }}/${{ env.IMAGE_REPO }}:${{ collabops.sha }}
${{ secrets.REGISTRY_URL }}/${{ env.IMAGE_REPO }}:latest
context: "/workspace/source"
file: "Dockerfile"
push: "true"
# 2. Deploy to Kubernetes (kubeconfig auth -> imagePullSecret -> apply -> rollout)
deploy:
phase: deploy
needs: [build]
if: "collabops.ref == 'refs/heads/main' && collabops.event_name == 'push'"
steps:
- name: checkout
uses: "collabops/checkout@v2"
with:
repo-url: "https://<collabops-host>/<workspace>/<repository>.git"
- name: rollout
image: alpine/kubectl:1.34.1 # Lightweight image with kubectl. Match your cluster version
env:
KUBE_CONFIG: "${{ secrets.KUBE_CONFIG }}"
REGISTRY_URL: "${{ secrets.REGISTRY_URL }}"
REGISTRY_USERNAME: "${{ secrets.REGISTRY_USERNAME }}"
REGISTRY_PASSWORD: "${{ secrets.REGISTRY_PASSWORD }}"
run: |
set -euo pipefail
cd /workspace/source
# Restore kubeconfig
printf '%s' "$KUBE_CONFIG" | base64 -d > /tmp/kubeconfig
export KUBECONFIG=/tmp/kubeconfig
# Ensure namespace + imagePullSecret idempotently.
# `create secret --dry-run` output contains credentials, so write it to a file
# and apply (avoid leaking into pipeline logs).
kubectl create namespace "${{ env.K8S_NAMESPACE }}" --dry-run=client -o yaml | kubectl apply -f -
kubectl create secret docker-registry "${{ env.IMAGE_PULL_SECRET }}" \
--docker-server="$REGISTRY_URL" --docker-username="$REGISTRY_USERNAME" \
--docker-password="$REGISTRY_PASSWORD" -n "${{ env.K8S_NAMESPACE }}" \
--dry-run=client -o yaml > /tmp/regcred.yaml
kubectl apply -f /tmp/regcred.yaml
rm -f /tmp/regcred.yaml
# Apply manifests -> update image to this commit -> wait for rollout
kubectl apply -f k8s/ -n "${{ env.K8S_NAMESPACE }}"
IMAGE="$REGISTRY_URL/${{ env.IMAGE_REPO }}:${{ collabops.sha }}"
kubectl set image "deployment/${{ env.DEPLOYMENT_NAME }}" "${{ env.CONTAINER_NAME }}=${IMAGE}" -n "${{ env.K8S_NAMESPACE }}"
kubectl rollout status "deployment/${{ env.DEPLOYMENT_NAME }}" -n "${{ env.K8S_NAMESPACE }}" --timeout=300s
# 3. Deploy notification (optional)
notify:
needs: [deploy]
if: "always()"
steps:
- name: slack
uses: "collabops/slack-notify@v1"
with:
webhook-url: ${{ secrets.SLACK_WEBHOOK }}
message: "k8s deploy: ${{ env.DEPLOYMENT_NAME }} (${{ collabops.sha }})"
title: "Deploy to Kubernetes"
color: goodExecution Flow
build (build image -> push to internal registry)
└── deploy (kubeconfig -> imagePullSecret -> apply -> rollout, main push only)
└── notify (Slack notification, always)