Deploying a Scalable Golang Application on Kubernetes: A Practical Guide
Learn how to deploy and scale a Golang app on Kubernetes using best practices like stateless design, concurrency, health checks, and autoscaling.
Join the DZone community and get the full member experience.
Join For FreeGolang is the finest programming language for constructing applications that can scale well and at high density due to the concurrency and performance inherent in the language itself. Kubernetes is the best standard for container orchestration, which gives a platform for deploying, managing, and scaling applications. Together, they constitute a formidable pair for creating unobtrusive and bulletproof microservices.
This blog will lead readers through the process of deploying a scalable Golang application on Kubernetes, highlighting essential considerations alongside the more practical 'doing it' steps.
Why Golang and Kubernetes?
Before we go into the how-tos, let's just touch briefly on the whys:
- With Golang for performance and concurrency: Go's extremely efficient lightweight goroutines and channels enable this concurrency. The fast compilation time and statically linked binaries make deployments easy.
- Kubernetes for scalability and resilience: A containerized application can be deployed, scaled, and managed with Kubernetes. It also enables self-healing, load balancing, and rolling updates, ensuring that the application remains up and fully functional even under heavy load.
Key Considerations for Scalability in Golang Applications
The following are certain principles you may wish to consider while designing your application and making the best use of Kubernetes for your Go applications:
- Statelessness: The Go application should be designed as stateless, meaning that no session data or persistent state should be stored within the application instances in memory. Instead, all states should be handled by leveraging external services such as databases (Postgres, MongoDB), caching services (Redis, Memcached), or message queues (Kafka, RabbitMQ). This will allow Kubernetes to scale your pods freely without any risk of data loss or data consistency.
- Concurrency: Go channels and goroutines should be employed for concurrent processing. This allows one Go instance to spawn multiple requests and thus can truly take advantage of the CPU resources.
- Graceful shutdowns: The Go application must have graceful shutdown logic. On receiving a terminate command from Kubernetes, the application should finish processing ongoing requests and free its resources before exiting. This way, no need for cleanup has to be made to protect against data corruption or dropped connections. Listening to a `SIGTERM` signal is only one common way to implement this.
- Configuration: The application will store its configuration externally through environment variables or config files. The application thus becomes highly portable and configurable within Kubernetes without needing to build a new Docker image.
- Health checks: Your app should provide HTTP endpoints for liveness and readiness checks. Kubernetes uses these to ascertain whether your application is up and running, ready to handle traffic, and healthy.
Step-by-Step Deployment on Kubernetes
Let's walk through the process with a hypothetical simple Go API.
1. Containerize Your Golang Application
First, you need a `Dockerfile` to package your Go application into a Docker image.
package main
import (
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from Golang App! Pod: %s\n", os.Getenv("HOSTNAME"))
})
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
server := &http.Server{Addr: ":" + port}
// Start HTTP server in a goroutine
go func() {
log.Printf("Server starting on port %s...", port)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Could not listen on %s: %v\n", port, err)
}
}()
// Graceful shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan // Block until a signal is received
log.Println("Shutting down server gracefully...")
shutdownCtx, cancel := time.WithTimeout(server.Context(), 5*time.Second)
defer cancel()
if err := server.Shutdown(shutdownCtx); err != nil {
log.Fatalf("Server shutdown failed: %v", err)
}
log.Println("Server gracefully stopped.")
}
# Use a minimal base image for the build stage
FROM golang:1.22 AS builder
WORKDIR /app
# Copy go mod and sum files to download dependencies
COPY go.mod go.sum ./
RUN go mod download
# Copy the source code
COPY . .
# Build the Go application
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
# Use a scratch image for the final, very small image
FROM alpine:latest
WORKDIR /root/
# Install ca-certificates for HTTPS calls if needed (optional but recommended)
RUN apk --no-cache add ca-certificates
# Copy the compiled binary from the builder stage
COPY --from=builder /app/main .
# Expose the port your application listens on
EXPOSE 8080
# Run the executable
CMD ["./main"]
Build and push the Docker images to the registry:
docker build -t your-docker-repo/golang-app:1.0.0 .
docker push your-docker-repo/golang-app:1.0.0
2. Define Kubernetes Manifests
Now, let's create the Kubernetes resource definitions.
A. deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: golang-app-deployment
labels:
app: golang-app
spec:
replicas: 3 # Start with 3 replicas for high availability
selector:
matchLabels:
app: golang-app
template:
metadata:
labels:
app: golang-app
spec:
containers:
- name: golang-app
image: your-docker-repo/golang-app:1.0.0 # Replace with your image
ports:
- containerPort: 8080
env:
- name: PORT
value: "8080"
resources: # Define resource requests and limits for better scheduling and stability
requests:
memory: "64Mi"
cpu: "100m" # 100 millicores (0.1 CPU core)
limits:
memory: "128Mi"
cpu: "200m" # 200 millicores (0.2 CPU core)
livenessProbe: # Checks if the app is still running
httpGet:
path: / # Or a dedicated /health endpoint
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe: # Checks if the app is ready to serve traffic
httpGet:
path: / # Or a dedicated /ready endpoint
port: 8080
initialDelaySeconds: 10
periodSeconds: 15
timeoutSeconds: 5
B. service.yaml
apiVersion: v1
kind: Service
metadata:
name: golang-app-service
labels:
app: golang-app
spec:
selector:
app: golang-app
ports:
- protocol: TCP
port: 80 # The port the service exposes
targetPort: 8080 # The port your application listens on inside the container
type: ClusterIP # Exposes the Service on a cluster-internal IP. Default for many apps.
C. Horizontal Pod Autoscaler (hpa.yaml)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: golang-app-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: golang-app-deployment
minReplicas: 3 # Minimum number of pods
maxReplicas: 10 # Maximum number of pods
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # Target CPU utilization (percentage)
# You can also scale based on memory or custom metrics
# - type: Resource
# resource:
# name: memory
# target:
# type: Utilization
# averageUtilization: 80
3. Deploy to Kubernetes
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
kubectl apply -f hpa.yaml
Some of the Best tools to monitor your Kubernetes resources are:
Conclusion
Scale up a Golang application in Kubernetes to reap its user-friendliness in building modern, resilient, and performance-oriented systems. Ensure that you adopt best practices for Go application design, such as making the application stateless, concurrent, and ensuring graceful shutdowns. You will allow Kubernetes features such as deployments, services, and horizontal pod autoscaling to balance the workload on your application accordingly. Come on, adopt this combination to perfect your microservices architecture!
Opinions expressed by DZone contributors are their own.
Comments