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
Installation
Follow the below link for installation.
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.
Subscribe to my newsletter
Read articles from Anish Bista directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Anish Bista
Anish Bista
Anish Bista is a passionate student deeply involved in cloud-native, DevOps, and open-source software, particularly focusing on Kubernetes ☸. He actively contributes to CNCF projects, including KubeVirt and Kanisterio. Anish is also engaged in community work and is a member of DevOpsCloudJunction. He also serve as a Co-Organizer at CNCF Kathmandu. His interest and Expertise lies on K8s and Golang.