Beginner's CI/CD for Kubernetes with GitHub Actions

The world of CI/CD tools is overwhelming. When starting out as a Kubernetes developer, do you even need to know so much? Isn't there a quick way to setup a pipeline and get going, focus on building your amazing piece of software.

Beginner's CI/CD for Kubernetes with GitHub Actions
Photo by freddie marriage / Unsplash

The world of CI/CD tools is overwhelming. I mean look at the CI/CD section in CNCF landscape. It's huge for a beginner and new tools are added every few months. When starting out as a Kubernetes developer, do you even need to know so much? Isn't there a quick way to setup a pipeline and get going, focus on building your amazing piece of software. We will have a look at such a setup today.

What should you already have?

  1. A GitHub repository
  2. Dockerfile for your application
  3. A Kubernetes cluster with public API connectivity

Setting Up the Kubernetes Resources

We need to create a deployment to run our application pods. Here's a sample blog service:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: goblog
spec:
  selector:
    matchLabels:
      app: goblog
  template:
    metadata:
      labels:
        app: goblog
    spec:
      containers:
      - name: goblog
        image: maytanthegeek/goblog:latest
        imagePullPolicy: Always
        resources:
          limits:
            memory: "128Mi"
            cpu: "50m"
        ports:
        - containerPort: 8443
Goblog Kubernetes Deployment

The main things to notice here are:

  1. imagePullPolicy: Always which means that the cluster will always pull the image from remote registry for creating new pods, even if the image with same tag is available locally.
  2. The image tag latest is a generic one, not a specific one like a version or commit id etc.

The idea here is that we will push the new versions of our software with the same tag on the container registry. For deployment, we will just require a deployment restart.

GitHub Action to Trigger the Deployment

Now we need to create a GitHub Action that builds our image and triggers a restart on Kubernetes.

name: Deploy Goblog

on:
  push:
    branches:
      - main

env:
  REGISTRY_URL: docker.io/maytanthegeek
  SERVICE_NAME: goblog
  SERVICE_TAG: latest

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
  	  contents: read
    steps:
      - .
        .
        .

  deploy:
    needs: [ build ]
    runs-on: ubuntu-latest

    steps:
      - .
        .
        .
GitHub Action skeleton

Let's complete the build section. I will be pushing to Docker hub but you can use any registry with proper authentication. Look for authentication actions in the marketplace for your registry or use the docker/login-action.

  build:
    runs-on: ubuntu-latest
    permissions:
  	  contents: read
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      
      - name: Generate docker image name
        id: image_tag
        run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
      
      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2

      - name: Build and push orchestration service
        uses: docker/build-push-action@v4
        with:
          context: .
          tags: |
            ${{ env.REGISTRY_URL }}/${{ env.SERVICE_NAME }}:${{ steps.image_tag.outputs.sha_short }}
            ${{ env.REGISTRY_URL }}/${{ env.SERVICE_NAME }}:latest
          push: true

We are tagging the image with both the commit ID and the latest tag.

Now for the deploy part, we need to run kubectl commands but first we need to authenticate with our Kubernetes cluster.

  deploy:
    needs: [ build ]
    runs-on: ubuntu-latest

    steps:
      - name: Install Kubectl
        uses: azure/setup-kubectl@v3

      - name: Setup Cluster Authentication
      	run: cat ${{ secrets.KUBECONFIG }} > /tmp/kubeconfig
      
      - name: Deploying ${{ env.SERVICE_NAME }}
        run: kubectl --kubeconfig=/tmp/kubeconfig rollout restart deployment ${{ env.SERVICE_NAME }}

There are many ways to authenticate. In this case the Kubeconfig file with authentication info is pulled from a secret saved in GitHub Actions.

💡
Bonus: If you are using Teleport (great tool), you should use this.

The complete action looks like this

name: Deploy Goblog

on:
  push:
    branches:
      - main

env:
  REGISTRY_URL: docker.io/maytanthegeek
  SERVICE_NAME: goblog
  SERVICE_TAG: latest

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
  	  contents: read
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      
      - name: Generate docker image name
        id: image_tag
        run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
      
      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2

      - name: Build and push orchestration service
        uses: docker/build-push-action@v4
        with:
          context: .
          tags: |
            ${{ env.REGISTRY_URL }}/${{ env.SERVICE_NAME }}:${{ steps.image_tag.outputs.sha_short }}
            ${{ env.REGISTRY_URL }}/${{ env.SERVICE_NAME }}:latest
          push: true

  deploy:
    needs: [ build ]
    runs-on: ubuntu-latest

    steps:
      - name: Install Kubectl
        uses: azure/setup-kubectl@v3

      - name: Setup Cluster Authentication
      	run: cat ${{ secrets.KUBECONFIG }} > /tmp/kubeconfig
      
      - name: Deploying ${{ env.SERVICE_NAME }}
        run: kubectl --kubeconfig=/tmp/kubeconfig rollout restart deployment ${{ env.SERVICE_NAME }}

Parting Words

CI/CD doesn't need to be a complex pipeline. More simple is more reliable. When starting out, a reliable, no brainer CI/CD can take you a long way.

When your requirements grow, you can implement very similar philosophy with GitOps tools like Flux and Argo. They do a very good job of reconciliation. You can easily have versioned deployments with them which we avaided here.

Have more ideas or an implementation of your own? Share it here so that we can learn together.