deploy app via GitHub pipeline

Summary

Fairly simple, almost complete copy & paste from ClaudeAI but some things required tweaking: specifically how image versions were identified (generated pipeline had issues with using branch name which would disappear on PR) as well as lowercasing image name on the right place.

This assumes that the docker image is built and pushed into ghcr.io repository and it is named ramblings (see sample build file here: https://github.com/dkambur/Ramblings/blob/main/.github/workflows/build.yaml - stages prior to deploy)

Preparation steps - GitHub source code repo

Secure main branch

This is questionable as it is my own repo with public visibility. Done so just in case.

ClaudeAI instructions For securing

Originally from ClaudeAI - slightly modified

Require approval for fork PRs:

Go to Settings → Actions → General Under Fork pull request workflows, select Require approval for first-time contributors

Was done already.

Protect your main branch:

Settings → Branches → Add rule for main Enable Require pull request reviews before merging

Different name: Require a pull request before merging 1 Approver Require review from Code Owners

Limit who can approve workflow runs:

Only trusted collaborators should be able to approve Actions. Set accordingly.

Preparation steps - kubernetes

Create Service Account

Taken from ClaudeAI

# Create a namespace for your app (optional but recommended)
kubectl create namespace ramblings

# Create a service account for GitHub Actions
kubectl create serviceaccount github-deployer -n ramblings

# Create a secret for the service account token
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Secret
metadata:
name: github-deployer-token
namespace: ramblings
annotations:
kubernetes.io/service-account.name: github-deployer
type: kubernetes.io/service-account-token
EOF

Create deployer role and authorise to deploy

Taken from ClaudeAI

Copy the text below into rbac-deployment.yaml:

---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: deployer-role
namespace: ramblings
rules:
# Deployments
- apiGroups: ["apps"]
  resources: ["deployments"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]

# ReplicaSets (needed for deployments)
- apiGroups: ["apps"]
  resources: ["replicasets"]
  verbs: ["get", "list", "watch"]

# Pods (for viewing status)
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list", "watch"]

# Services
- apiGroups: [""]
  resources: ["services"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]

# ConfigMaps and Secrets (if your app needs them)
- apiGroups: [""]
  resources: ["configmaps", "secrets"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]

---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: deployer-rolebinding
namespace: ramblings
subjects:
- kind: ServiceAccount
  name: github-deployer
  namespace: ramblings
  roleRef:
  kind: Role
  name: deployer-role
  apiGroup: rbac.authorization.k8s.io

Apply

kubectl apply -f rbac-deployment.yaml

Get token for GitHub

# Get the token
TOKEN=$(kubectl get secret github-deployer-token -n ramblings -o jsonpath='{.data.token}' | base64 -d)

# Get the cluster CA certificate
CA_CERT=$(kubectl get secret github-deployer-token -n ramblings -o jsonpath='{.data.ca\.crt}')

# Get your cluster server URL
CLUSTER_URL=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')

# Create a kubeconfig for GitHub Actions
cat <<EOF > github-kubeconfig.yaml
apiVersion: v1
kind: Config
clusters:
- name: github-cluster
  cluster:
    certificate-authority-data: ${CA_CERT}
    server: ${CLUSTER_URL}
contexts:
- name: github-context
  context:
    cluster: github-cluster
    namespace: ramblings
    user: github-deployer
current-context: github-context
users:
- name: github-deployer
  user:
    token: ${TOKEN}
EOF

cat github-kubeconfig.yaml

Note: Generate a separate certificate for this but for development environment, this is ok.

Store the token in GitHub

Go to your repo: /settings/secrets/actions Click New repository secret

Add this secret: Name: KUBE_CONFIG Value: The entire contents of github-kubeconfig.yaml

Add deployment stage to your pipeline

  deploy:
    needs: build-and-push
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up kubectl
        uses: azure/setup-kubectl@v4
        with:
          version: 'latest'

      - name: Configure kubectl
        run: |
          mkdir -p $HOME/.kube
          echo "${{ secrets.KUBE_CONFIG }}" > $HOME/.kube/config
          chmod 600 $HOME/.kube/config          

      - name: Verify kubectl access
        run: |
          kubectl version --client
          kubectl get pods -n ramblings || echo "No pods yet"          

      - name: Deploy to Kubernetes
        run: |
          export IMAGE_TAG IMAGE_NAME
          envsubst < kubernetes/k8s-deployment.yaml | kubectl apply -f -          

      - name: Wait for deployment rollout
        run: |
          kubectl rollout status deployment/ramblings-app -n ramblings --timeout=5m          

      - name: Get deployment status
        run: |
          echo "Deployment status:"
          kubectl get deployment ramblings-app -n ramblings
          echo ""
          echo "Pods:"
          kubectl get pods -n ramblings -l app=ramblings-app          

And here’s corresponding k8s-deployment.yaml file:

---
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ramblings-app
  namespace: ramblings
  labels:
    app: ramblings-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ramblings-app
  template:
    metadata:
      labels:
        app: ramblings-app
    spec:
      containers:
        - name: ramblings-app
          image: ghcr.io/$IMAGE_NAME:$IMAGE_TAG
          ports:
            - containerPort: 8080
              name: http
          resources:
            requests:
              memory: "512Mi"
              cpu: "250m"
            limits:
              memory: "512Mi"
          livenessProbe:
            httpGet:
              path: /deck-api/health
              port: 8080
            initialDelaySeconds: 30
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /deck-api/health
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 5

Replace ramblings with your application name.

See deployment pipeline for how IMAGE_NAME and IMAGE_TAG are generated. It should be done differently.

Run the pipeline and enjoy