How to create CD with GitHub Actions and Kubernetes in your Golang application

- 9 mins

Golang and kubernetes

An example to create pipeline to deploy a simple GO application into k3s cluster with Docker image from scratch.

Previous requirements:

Simple http application with GO

A simple illustrative example with http server on port 80 with basic handle respond on a main url.

If you already have any application you can go to the next step.

package main

import (
    "fmt"
    "net/http"
    "os"
)

func handler(w http.ResponseWriter, r *http.Request) {
    var name, _ = os.Hostname()
    fmt.Fprintf(w, "<h1>This request was processed by host: %s</h1>\n", name)
}

func main() {
    fmt.Fprintf(os.Stdout, "Web Server started. Listening on 0.0.0.0:80\n")
    http.HandleFunc("/", handler)
    http.ListenAndServe(":80", nil)
}

You can run the application to ensure works correctly:

go run main.com

If the application its works then we can prepare docker image to “Dokcerize” the application.

Build docker image from scrath

Firstly, we create a file named infrastucture/docker/Dockerfile to contain all config. We use the alpine image as a builder to compile de Go binary and then use image from scratch to copy binary with SSL certificates and define de binary as entrypoint.

This way we get a minimal docker image that contain your binary application than only around 5 MB!

# Use latest Go alpine image as builder
FROM golang:alpine AS build

# Define app as working directory
WORKDIR /app

# Copy all project into docker image
COPY . .

# Compile the Go Application
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /app/bin/demo main.go

# Use minimum image with binary
FROM scratch AS prod

# Copy SSL Certificates from build stage
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

#Copy the application binary from build stage
COPY --from=build /app/bin/demo /app/bin/demo

# Define your binary as entrypoint
ENTRYPOINT ["/app/bin/demo"]

And we can make the build of the image:

docker build -t demo-app:1 . -f ./infrastucture/docker/Dockerfile

You can found more options on the oficial documentation: https://docs.docker.com/engine/reference/commandline/build/#options

Connect your K3s cluster with Github

For Github Actions can access to your k3s cluster you need import de kubeconfig into secrets actions.

In k3s you can found the kubeconfig on /etc/rancher/k3s/k3s.yaml. You kubeconfig will like somehow like this:

apiVersion: v1
clusters:
- cluster:
      certificate-authority-data: LS0...
      server: https://your-server.com:6443
  name: default
contexts:
- context:
    cluster: default
    user: default
  name: default
current-context: default
kind: Config
preferences: {}
users:
- name: default
  user:
    client-certificate-data: LS0...
    client-key-data: LS0...

Then you go to your project from Github and add the secret named KUBE_CONFIG with the k3s.yaml content.

Project ⮕ Settings ⮕ Secret and Variables ⮕ Actions

GitHub repository secrets

Github action for Continuous Delivery (CD)

Finally we come to the long awaited section. We will see how to create a flow with two steps.

Create a file into root of main project: .github/workflow/cd.yaml where we create a workflow triggered on a push/merge on your principal branch, in this case main branch.

on:
    push:
        branches: [ "main" ]

First step

In the first step build the docker image and publish on the container registry, in this case use the ghcr (GitHub Container Registry).

You can found more information about github packages: https://docs.github.com/en/packages/learn-github-packages

build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
        
      - name: Log in to the Container registry
        uses: docker/login-action@v2
        with:
          registry: ghcr.io
          username: $
          password: $
        
      - name: Build and push
        uses: docker/build-push-action@v3
        with:
          context: .
          platforms: linux/amd64
          file: ./infrastructure/docker/Dockerfile
          push: true
          tags: ghcr.io/$/demo-app:$

Second step

Publish your docker image from registry to k8s cluster.

First of all need a create a token on github with read:package scope and create secret in json format encoded as base64 that you publish on your k8s secrets.

Let’s Base64 encode it first:

echo -n "username:your_token" | base64
#example of output: dXNlcm5hbWU6eW91cl90b2tlbg==

Change username for your GitHub username and your_token with the previous generated token

Create a json and encode to Base64 again:

echo -n  '{"auths":{"ghcr.io":{"auth":"dXNlcm5hbWU6eW91cl90b2tlbg=="}}}' | base64
#example of output: eyJhdXRocyI6eyJnaGNyLmlvIjp7ImF1dGgiOiJkWE5sY201aGJXVTZlVzkxY2w5MGIydGxiZz09In19fQ==

Create a manifest to deploy your application on kuberntes deployment, service, ingress

#infrastructure/kubernetes/demo-app-deployment.yml

apiVersion: apps/v1
kind: Deployment
metadata:
    name: demo-app
spec:
    selector:
        matchLabels:
            app: demo-app
    replicas: 2
    template:
        metadata:
            labels:
            app: demo-app
    spec:
        containers:
            - name: demo-app
              image: ghcr.io/path-to-image/demo-app
              env:
                - name: GIN_MODE
                  value: "release"
              ports:
                - containerPort: 80
              livenessProbe:
                httpGet:
                    path: /
                    port: 80
                initialDelaySeconds: 5
                periodSeconds: 3
              readinessProbe:
                httpGet:
                    path: /
                    port: 80
                initialDelaySeconds: 5
                periodSeconds: 3
#infrastructure/kubernetes/demo-app-service.yml

apiVersion: v1
kind: Service
metadata:
    name: demo-app
spec:
    ports:
        - port: 80
          protocol: TCP
          targetPort: 80
    type: NodePort
    selector:
        app: demo-app
#infrastructure/kubernetes/demo-app-ingress.yml

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
    annotations:
    name: demo-app
spec:
    ingressClassName: nginx
    rules:
        - host: domain.com
          http:
            paths:
                  - path: /
                    pathType: Prefix
                    backend:
                      service:
                          name: demo-app
                          port:
                            number: 80

Finally, once we have the manifests and secrets, we can create the last step of the workflow.

deploy:
  needs: [build]
  runs-on: ubuntu-latest
  steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Log in to the Container registry
        uses: docker/login-action@v2
        with:
          registry: ghcr.io
          username: $
          password: $
    
      - name: Kubernetes set context
        uses: Azure/k8s-set-context@v3
        with:
          method: kubeconfig
          kubeconfig: $
    
      - name: Deploy
        uses: Azure/[email protected]
        with:
          action: deploy
          strategy: basic
          imagepullsecrets: |
            dockerconfigjson-github-com
          manifests: |
            ./infrastructure/kubernetes/demo-app-deployment.yml
            ./infrastructure/kubernetes/demo-app-service.yml
            ./infrastructure/kubernetes/demo-app-ingress.yml
          images: ghcr.io/$/demo-app:$

And just commit in your project the complete CD workflow config .github/workflow/cd.yaml.

name: CD

on:
  push:
    branches: [ "main" ]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Log in to the Container registry
        uses: docker/login-action@v2
        with:
          registry: ghcr.io
          username: $
          password: $

      - name: Build and push
        uses: docker/build-push-action@v3
        with:
          context: .
          platforms: linux/amd64
          file: ./infrastructure/docker/Dockerfile
          push: true
          target: prod
          tags: ghcr.io/$/demo-app:$

  deploy:
    needs: [build]
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Log in to the Container registry
        uses: docker/login-action@v2
        with:
          registry: ghcr.io
          username: $
          password: $

      - name: Kubernetes set context
        uses: Azure/k8s-set-context@v3
        with:
          method: kubeconfig
          kubeconfig: $

      - name: Deploy
        uses: Azure/[email protected]
        with:
          action: deploy
          strategy: basic
          imagepullsecrets: |
            dockerconfigjson-github-com
          manifests: |
            ./infrastructure/kubernetes/demo-app-deployment.yml
            ./infrastructure/kubernetes/demo-app-service.yml
            ./infrastructure/kubernetes/demo-app-ingress.yml
          images: ghcr.io/$/demo-app:$

Then you can go to Actions section in your repository and show all workflows and you can see the summary.

CD summary workflow

And that’s all just have fun and deploy :-)

You can read the article on Medium

Albert Colom

Albert Colom

Backend developer