Go in Kubernetes: patterns en valkuilen

Leer hoe je Go applicaties draait in Kubernetes. Van health checks en graceful shutdown tot resource limits en veelvoorkomende valkuilen.

29 augustus 20267 min leestijdDoor We Develop Communication

Go en Kubernetes zijn een gouden combinatie. Kleine statische binaries, snelle startup en lage memory footprint maken Go ideaal voor containers die op een cluster draaien. Maar zonder de juiste patterns loop je snel tegen valkuilen aan: OOMKills, trage rolling updates, dropped connections of pods die blijven hangen in CrashLoopBackOff.

In deze gids lopen we de belangrijkste patterns door voor Go in Kubernetes en de valkuilen die je beter kunt vermijden. Van graceful shutdown en probes tot resource limits, configuration en observability, alles wat je nodig hebt om een Go service stabiel in productie te draaien.

Waarom Go en Kubernetes zo goed samenwerken

Go compileert naar een enkel statisch binary zonder runtime dependencies. Dat betekent dat je Docker image minimaal kan zijn: vaak onder de 20 MB met een scratch of distroless base. Kleine images pullen sneller, starten sneller en verlagen je attack surface.

Daarnaast start een Go proces binnen milliseconden op. Kubernetes kan daardoor snel schalen en rollouts gaan vlot. Combineer dat met Go's lage geheugenvoetafdruk en je krijgt een service die efficiënt omgaat met de resources die Kubernetes uitdeelt.

Als je nog niet weet hoe je Go naar containers brengt, lees dan eerst onze gids over production deployment van Go apps. De basis van een goede Dockerfile is het fundament onder elke Kubernetes deployment.

Pattern 1: minimalistische container images

Bouw je image in twee stappen. In de builder stage compileer je het binary, in de runtime stage kopieer je alleen dat binary naar een lege base image.

FROM golang:1.23 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /out/app ./cmd/api

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /out/app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]

Let op de -ldflags="-s -w" om debug info te strippen en CGO_ENABLED=0 voor een volledig statisch binary. Een nonroot user is een security best practice die in Kubernetes vaak vereist is via Pod Security Standards.

Pattern 2: graceful shutdown op SIGTERM

Wanneer Kubernetes je pod termineert, stuurt het eerst SIGTERM. Je hebt dan terminationGracePeriodSeconds (default 30) om netjes af te sluiten voordat SIGKILL volgt. Je Go app moet dit signaal oppakken en actieve requests laten afronden.

func main() {
    srv := &http.Server{Addr: ":8080", Handler: router()}

    go func() {
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatal(err)
        }
    }()

    stop := make(chan os.Signal, 1)
    signal.Notify(stop, syscall.SIGTERM, syscall.SIGINT)
    <-stop

    ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second)
    defer cancel()
    if err := srv.Shutdown(ctx); err != nil {
        log.Printf("shutdown error: %v", err)
    }
}

De valkuil hier: Kubernetes haalt je pod pas uit de Service endpoints nadat het SIGTERM heeft gestuurd. Er is dus een korte window waarin nieuwe requests nog bij je afsluitende pod aankomen. Een preStop hook met een korte sleep (5 seconden) geeft het endpoints-systeem de tijd om bij te werken.

lifecycle:
  preStop:
    exec:
      command: ["/bin/sleep", "5"]

Meer details over graceful shutdown en signal handling vind je in onze handleiding over HTTP servers bouwen in Go.

Pattern 3: liveness, readiness en startup probes

Probes zijn het belangrijkste contract tussen je Go app en Kubernetes. Ze bepalen wanneer verkeer mag binnenkomen en wanneer je pod herstart wordt.

Readiness probe

Readiness bepaalt of je pod klaar is voor verkeer. Laat deze falen als kritieke dependencies (database, cache) niet beschikbaar zijn.

mux.HandleFunc("/readyz", func(w http.ResponseWriter, r *http.Request) {
    if err := db.PingContext(r.Context()); err != nil {
        http.Error(w, "db not ready", http.StatusServiceUnavailable)
        return
    }
    w.WriteHeader(http.StatusOK)
})

Liveness probe

Liveness moet juist simpel zijn. Als deze faalt, wordt je pod herstart. Controleer hier niet je database, dan komt je pod in een restart loop bij een tijdelijke DB hiccup.

mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
})

Startup probe

Voor apps met een lange opstarttijd (warm caches, migraties) gebruik je een startup probe. Zo voorkom je dat liveness te vroeg ingrijpt.

livenessProbe:
  httpGet: { path: /healthz, port: 8080 }
  periodSeconds: 10
readinessProbe:
  httpGet: { path: /readyz, port: 8080 }
  periodSeconds: 5
startupProbe:
  httpGet: { path: /healthz, port: 8080 }
  failureThreshold: 30
  periodSeconds: 2

Pattern 4: resource limits en GOMAXPROCS

Dit is de grootste valkuil voor Go in Kubernetes. Go's runtime leest standaard het aantal CPU cores van de node, niet van je container. Op een node met 64 cores en een CPU limit van 500m maakt Go 64 OS threads aan, volledig onnodig en vaak schadelijk voor performance.

De oplossing is uber-go/automaxprocs:

import _ "go.uber.org/automaxprocs"

Één import en GOMAXPROCS wordt automatisch afgestemd op je cgroup CPU limit. Voor memory geldt iets vergelijkbaars: sinds Go 1.19 bestaat GOMEMLIMIT. Zet die op ongeveer 90% van je container memory limit om OOMKills door de garbage collector te voorkomen.

env:
  - name: GOMEMLIMIT
    value: "450MiB"
resources:
  requests:
    cpu: "200m"
    memory: "256Mi"
  limits:
    memory: "512Mi"

Merk op dat we geen strakke CPU limit zetten. CPU throttling kan de Go scheduler en garbage collector hinderen tijdens pieken. Meer context over hoe de GC werkt vind je in memory management en de garbage collector in Go.

Pattern 5: configuratie via environment en ConfigMaps

Volg de twelve-factor principes: lees configuratie uit environment variables. Kubernetes mount ConfigMaps en Secrets als env vars of files, en je Go code hoeft niet te weten welke van de twee het is.

type Config struct {
    Port     string `env:"PORT" envDefault:"8080"`
    DBURL    string `env:"DATABASE_URL,required"`
    LogLevel string `env:"LOG_LEVEL" envDefault:"info"`
}

Secrets mount je bij voorkeur als files, niet als env vars. Env vars lekken makkelijk in logs, crash dumps en child processes. Een file op /var/run/secrets/app/db-password is veiliger en kan hot-reloaded worden.

Pattern 6: structured logging naar stdout

Log naar stdout in JSON formaat. Kubernetes-logagents (Fluent Bit, Vector, Loki) parsen dat automatisch en je krijgt zoekbare velden. Met log/slog uit de standaard library is dit triviaal:

logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
    Level: slog.LevelInfo,
}))
slog.SetDefault(logger)

Voeg vaste velden toe zoals service, version en pod (via Downward API) zodat je per release kunt filteren. Uitgebreide voorbeelden staan in onze gids over logging en observability in Go.

Veelvoorkomende valkuilen

Valkuil 1: dropped connections bij rolling updates

Tijdens een rollout worden pods geleidelijk vervangen. Zonder preStop hook en graceful shutdown sluit je pod actieve TCP connecties hardhandig af. Gebruikers zien 502's. Combineer altijd terminationGracePeriodSeconds, een preStop sleep en server.Shutdown.

Valkuil 2: goroutine leaks onder load

In een container met een memory limit lopen goroutine leaks snel uit de hand. Een lekkende goroutine per request betekent OOMKill binnen uren. Gebruik altijd context.Context om goroutines te kunnen cancellen en monitor je goroutine count via pprof. Check onze gids over het context package voor de juiste patterns.

Valkuil 3: database connection pools te groot

Met 20 pod replicas en SetMaxOpenConns(100) open je potentieel 2000 connecties naar één database. Schat de pool size vanuit het cluster-perspectief, niet per pod. Zie ook database toegang in Go.

Valkuil 4: geen PodDisruptionBudget

Zonder PDB kan een node drain al je replicas tegelijk weghalen. Zet minimaal minAvailable: 1 voor elke productie deployment:

apiVersion: policy/v1
kind: PodDisruptionBudget
spec:
  minAvailable: 1
  selector:
    matchLabels:
      app: api

Valkuil 5: log spam verhoogt je kosten

In Kubernetes betaal je vaak per GB logs. Een Go service die op DEBUG logt kan snel terabytes per maand genereren. Zet productie op INFO, gebruik sampling voor high-volume events en log geen request bodies standaard.

Observability: metrics en tracing

Exporteer Prometheus metrics op een aparte poort (bijvoorbeeld :9090) zodat je scraping endpoint niet publiek is. De prometheus/client_golang library maakt dit eenvoudig.

Voor distributed tracing is OpenTelemetry de standaard. In een Kubernetes omgeving met meerdere Go services is tracing onmisbaar om latency-problemen te debuggen. Combineer dit met de patterns uit onze microservices in Go handleiding.

Checklist voor productie

Voordat je een Go service live zet in Kubernetes:

  • Minimalistische distroless of scratch image met nonroot user
  • Graceful shutdown via SIGTERM en server.Shutdown
  • preStop hook van 5 seconden
  • Aparte liveness, readiness en eventueel startup probes
  • automaxprocs geïmporteerd en GOMEMLIMIT gezet
  • Resource requests ingesteld, memory limit gezet, geen strakke CPU limit
  • Configuratie via env vars, secrets als files
  • JSON logging naar stdout op INFO niveau
  • PodDisruptionBudget voor minimum availability
  • Prometheus metrics endpoint op een aparte poort
  • HorizontalPodAutoscaler op CPU of custom metrics

Veelgestelde vragen

Waarom past Go zo goed bij Kubernetes?

Go compileert naar een klein statisch binary, start razendsnel op en heeft een lage geheugenvoetafdruk. Dat maakt Go ideaal voor containers die snel moeten schalen en die je in grote aantallen op een cluster draait.

Hoe implementeer ik een graceful shutdown in Kubernetes?

Luister in je Go app op SIGTERM, sluit je HTTP server met server.Shutdown en wacht tot lopende requests klaar zijn. Combineer dit met een preStop hook of een korte sleep om de kans te geven dat endpoints uit de service worden gehaald.

Welke resource limits zijn verstandig voor een Go service?

Zet altijd een memory limit en gebruik GOMEMLIMIT en GOMAXPROCS afgestemd op je cgroup. Een CPU request zonder te strakke limit werkt meestal het beste, zodat de garbage collector niet wordt gethrottled tijdens pieken.

Moet ik liveness en readiness probes apart configureren?

Ja. Readiness bepaalt of je pod verkeer mag ontvangen, liveness of de pod herstart moet worden. Maak readiness afhankelijk van afhankelijkheden zoals de database, en houd liveness bewust simpel om restart loops te voorkomen.

Hoe voorkom ik dat mijn Go pod te veel CPU pakt op een node?

Stel GOMAXPROCS in op basis van je CPU request of limit met bijvoorbeeld de uber-go/automaxprocs library. Zonder deze aanpassing ziet Go alle cores van de node en maakt het te veel OS threads, wat leidt tot context switching overhead.

Veelgestelde vragen

Klaar om digitaal te groeien?

Wij helpen Nederlandse bedrijven met webtechnologie en SEO-strategieën die écht werken. Neem vrijblijvend contact op.