Building Go Applications with KO: Simplifying Kubernetes Deployments

Building Go Applications with KO: Simplifying Kubernetes Deployments

Introduction

In the world of containerized applications, efficiency and security are paramount. Enter KO, a lightweight yet powerful tool designed specifically for building container images of Go applications. KO simplifies the image building process by executing go build directly on your local machine, eliminating the need for Docker during the build phase. This makes it perfect for streamlined CI/CD pipelines where simplicity and speed are crucial.

KO shines particularly bright in scenarios where your image predominantly consists of a single Go application with minimal dependencies on the underlying OS. It supports multi-platform builds effortlessly and generates Software Bill of Materials (SBOMs) by default, ensuring transparency and compliance. Moreover, its built-in YAML templating capabilities make it an invaluable asset for deploying applications on Kubernetes.

Blog follows following Resources

YouTube

Github

Installation

Follow the below link for installation.

Install KO

if you are in ubuntu, make sure to add Add Go binary path to your PATH, if your are installing it from go .

echo 'export PATH=$PATH:$(go env GOPATH)/bin' >> ~/.profile
source ~/.profile

Sample Application

static/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Go Server Example</title>
    <style>
        body {
            font-family: Arial, sans-serif;
        }
        .logo {
            display: block;
            margin: 20px auto;
            width: 100px;
        }
        form {
            text-align: center;
            margin: 20px 0;
        }
        ul {
            list-style: none;
            padding: 0;
            text-align: center;
        }
        li {
            margin: 10px 0;
        }
    </style>
</head>
<body>
    <img src="https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png" alt="Logo" class="logo">
    <form id="addForm">
        <input type="text" id="nameInput" name="name" placeholder="Enter name" required>
        <button type="submit">Add Name</button>
    </form>
    <ul id="nameList"></ul>
    <script>
        async function fetchNames() {
            const response = await fetch('/names');
            if (response.ok) {
                const names = await response.json();
                console.log('Fetched names:', names);
                if (!names) {
                    console.error('Fetched names is null');
                    return;
                }
                updateNameList(names);
            } else {
                console.error('Failed to fetch names:', response.statusText);
            }
        }

        function updateNameList(names) {
            if (!Array.isArray(names)) {
                console.error('Names is not an array:', names);
                return;
            }
            const nameList = document.getElementById('nameList');
            nameList.innerHTML = '';
            names.forEach(name => {
                const li = document.createElement('li');
                li.textContent = name;
                const deleteForm = document.createElement('form');
                deleteForm.action = '/delete';
                deleteForm.method = 'post';
                deleteForm.style.display = 'inline';
                deleteForm.innerHTML = `
                    <input type="hidden" name="name" value="${name}">
                    <button type="submit">Delete</button>
                `;
                deleteForm.addEventListener('submit', async (event) => {
                    event.preventDefault();
                    const formData = new FormData(deleteForm);
                    const response = await fetch('/delete', {
                        method: 'POST',
                        body: formData
                    });
                    if (response.ok) {
                        const updatedNames = await response.json();
                        console.log('Updated names after delete:', updatedNames);
                        updateNameList(updatedNames);
                    } else {
                        console.error('Failed to delete name:', response.statusText);
                    }
                });
                li.appendChild(deleteForm);
                nameList.appendChild(li);
            });
        }

        document.getElementById('addForm').addEventListener('submit', async (event) => {
            event.preventDefault();
            const nameInput = document.getElementById('nameInput');
            const name = nameInput.value.trim();
            if (name === '') {
                console.error('Name input is empty');
                return;
            }
            const formData = new FormData();
            formData.append('name', name);
            console.log('FormData being sent:', formData);
            for (let [key, value] of formData.entries()) { 
                console.log(key, value);
            }
            const response = await fetch('/add', {
                method: 'POST',
                body: formData
            });
            if (response.ok) {
                const names = await response.json();
                console.log('Updated names after add:', names);
                if (!names) {
                    console.error('Updated names after add is null');
                    return;
                }
                updateNameList(names);
                event.target.reset();
            } else {
                console.error('Failed to add name:', response.statusText);
            }
        });

        // Fetch initial names on page load
        fetchNames();
    </script>
</body>
</html>

main.go

package main

import (
    "embed"
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "path"
    "sync"
)

var (
    names      = []string{}
    namesMutex sync.Mutex
)

//go:embed static/*
var staticFiles embed.FS

func main() {
    http.HandleFunc("/", handleIndex)
    http.HandleFunc("/ping", pingHandler)
    http.HandleFunc("/add", handleAdd)
    http.HandleFunc("/delete", handleDelete)
    http.HandleFunc("/names", handleNames)
    //http.Handle("/static/", http.FileServer(http.Dir(os.Getenv("KO_DATA_PATH"))))
    http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticFiles))))

    log.Println("Starting server on :8000")
    if err := http.ListenAndServe(":8000", nil); err != nil {
        log.Fatalf("Could not start server: %s\n", err)
    }
}

func pingHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Pong!")
}

func handleIndex(w http.ResponseWriter, r *http.Request) {
    log.Println("Serving index.html")
    indexPath := path.Join("static", "index.html")
    content, err := staticFiles.ReadFile(indexPath)
    if err != nil {
        log.Printf("Error reading index.html: %v", err)
        http.Error(w, "Page not found", http.StatusNotFound)
        return
    }
    w.Write(content)
}

func handleAdd(w http.ResponseWriter, r *http.Request) {
    log.Println("Handling add request")
    if r.Method == "POST" {
        log.Println("Received POST request to add name")
        log.Printf("Content-Type: %s", r.Header.Get("Content-Type"))
        if err := r.ParseMultipartForm(10 << 20); err != nil { // 10MB limit
            log.Println("Error parsing multipart form:", err)
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
        name := r.FormValue("name")
        log.Printf("Received name: %s", name)
        if name != "" {
            namesMutex.Lock()
            names = append(names, name)
            namesMutex.Unlock()
            log.Println("Added name:", name)
            log.Println("Current names:", names)
        } else {
            log.Println("Name is empty, not adding")
        }
        jsonResponse(w, names)
    } else {
        log.Println("Invalid request method for add")
        http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
    }
}

func handleDelete(w http.ResponseWriter, r *http.Request) {
    log.Println("Handling delete request")
    if r.Method == "POST" {
        log.Println("Received POST request to delete name")
        if err := r.ParseMultipartForm(10 << 20); err != nil { // 10MB limit
            log.Println("Error parsing multipart form:", err)
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
        name := r.FormValue("name")
        log.Println("Received name to delete:", name)
        namesMutex.Lock()
        for i, n := range names {
            if n == name {
                names = append(names[:i], names[i+1:]...)
                break
            }
        }
        namesMutex.Unlock()
        log.Println("Deleted name:", name)
        log.Println("Current names:", names)
        jsonResponse(w, names)
    } else {
        log.Println("Invalid request method for delete")
        http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
    }
}

func handleNames(w http.ResponseWriter, r *http.Request) {
    log.Println("Handling names request")
    jsonResponse(w, names)
    log.Println("Current names:", names)
}

func jsonResponse(w http.ResponseWriter, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    if data == nil {
        log.Println("jsonResponse data is nil")
    } else {
        log.Println("jsonResponse data:", data)
    }
    if err := json.NewEncoder(w).Encode(data); err != nil {
        log.Println("Error encoding JSON:", err)
        http.Error(w, err.Error(), http.StatusInternalServerError)
    }
}

Test in Local

Run go mod init "github.com/anishbista60/Devops-project" to initalize the module.

Now , run go run main.go . Acess it at localhost:8000

Build using KO

KO_DOCKER_REPO=ttl.sh/anish60/go ko build

The output looks like:

At the last, you will have your image.

Test it on Docker

docker run -d -p 8000:8000 ttl.sh/anish60/go/devops-project-6f42ca0ef80eadcda32a05f2dc655933@sha256:eff45747c125a9253567777997333eb3914e34322bd191c41ce7a03cfba7b7b2

Acess it on : localhost:8000:8000

Deploy to Kubernetes

Install nginx ingress controller

kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.9.4/deploy/static/provider/cloud/deploy.yaml

Install cert-manger

kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.15.1/cert-manager.yamlkubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.9.4/deploy/static/provider/cloud/deploy.yaml

deploy.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: go-server-deployment
  labels:
    app: go-server
spec:
  replicas: 2
  selector:
    matchLabels:
      app: go-server
  template:
    metadata:
      labels:
        app: go-server
    spec:
      containers:
        - name: go-server
          image: ttl.sh/anish60/go/devops-project-6f42ca0ef80eadcda32a05f2dc655933@sha256:eff45747c125a9253567777997333eb3914e34322bd191c41ce7a03cfba7b7b2
          ports:
            - containerPort: 8000

service.yaml

apiVersion: v1
kind: Service
metadata:
  name: go-server-service
  labels:
    app: go-server
spec:
  type: ClusterIP
  ports:
    - port: 80
      targetPort: 8000
  selector:
    app: go-server

ingress.yaml

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    kubernetes.io/ingress.class: nginx
  name: demo-app-ingress
spec:
  rules:
  - host: go.anishbista.xyz
    http:
      paths:
      - backend:
          service:
            name: go-server-service
            port:
              number: 80
        path: /
        pathType: Prefix
  tls:
  - hosts:
    - go.anishbista.xyz
    secretName: demo

cert.yaml

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: anishbista88@gmai.com
    privateKeySecretRef:
      name: letsencrypt-prod
    solvers:
    - http01:
        ingress:
          class: nginx
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: demo
spec:
  secretName: demo
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
  commonName: go.anishbista.xyz
  dnsNames:
  - go.anishbista.xyz

Apply all these manifest, update the DNS record for your dns name.

Now, Access the application at go.anishbista.xyz

Test the vulnerabilities of your image

Install Grype

curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin

Test the image.

 grype ttl.sh/anish60/go/devops-project-6f42ca0ef80eadcda32a05f2dc655933@sha256:eff45747c125a9253567777997333eb3914e34322bd191c41ce7a03cfba7b7b2

This above image supply chain based image so ,it doesnot contain any vulnerabilities

Conclusion

KO is a streamlined tool for building Go application container images directly on your local machine, bypassing the need for Docker during the build process. It excels in speed and simplicity, making it ideal for CI/CD pipelines. KO supports multi-platform builds and automatically generates Software Bill of Materials (SBOMs) for compliance. With built-in YAML templating, it seamlessly integrates with Kubernetes for efficient application deployment. KO ensures transparency and security, enhancing the development and deployment of containerized Go applications in cloud-native environments.