CollabOps

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:

TypeKeyDescription
SecretREGISTRY_URLInternal registry host (e.g., harbor.internal, nexus.internal:8083)
SecretREGISTRY_USERNAMERegistry account username (push + cluster pull). Harbor robot / Nexus user, etc.
SecretREGISTRY_PASSWORDRegistry account password
SecretKUBE_CONFIGBase64-encoded kubeconfig for the target cluster
SecretSLACK_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: good

Execution Flow

build (build image -> push to internal registry)
  └── deploy (kubeconfig -> imagePullSecret -> apply -> rollout, main push only)
        └── notify (Slack notification, always)

Table of Contents