Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

23 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸš€ go-web-app

A production-grade Go web server β€” intentionally minimal in application logic, but maximally realistic in DevOps infrastructure. The real complexity lives in Docker, Kubernetes, Helm, GitHub Actions CI/CD, and ArgoCD GitOps.

CI/CD Pipeline Go Version Docker Kubernetes License: MIT


πŸ“‹ Table of Contents


🎯 Project Overview

This project is a Go HTTP web server that serves four static HTML pages (home, courses, about, contact) on port 8080. It exposes Kubernetes-native health check endpoints and supports zero-downtime graceful shutdown.

What makes it interesting is NOT the application β€” it's the surrounding infrastructure:

Layer Technology Purpose
Application Go 1.22.5 Lightweight HTTP server, no frameworks
Containerization Docker (Distroless) ~20 MB production image
Orchestration Kubernetes Self-healing, auto-scaling
Packaging Helm Templated, versioned deployments
Continuous Integration GitHub Actions Test β†’ Build β†’ Push β†’ Update
Continuous Delivery ArgoCD GitOps-driven cluster sync

πŸ—οΈ Architecture Overview

graph TB
    subgraph DEV["πŸ‘¨β€πŸ’» Developer Workflow"]
        DEV1[Write Code] --> DEV2[git push main]
    end

    subgraph CI["βš™οΈ GitHub Actions β€” CI"]
        CI1[Job 1: Test and Lint<br/>go vet + go test -race] --> CI2[Job 2: Build and Push<br/>Docker Image to DockerHub]
        CI2 --> CI3[Job 3: Update Manifest<br/>Bump image tag in values.yaml]
    end

    subgraph GITOPS["πŸ“¦ GitOps Source of Truth"]
        REPO[(Git Repository<br/>helm/go-web-app/values.yaml<br/>image.tag: sha-abc1234)]
    end

    subgraph CD["πŸ”„ ArgoCD β€” CD"]
        ARGO1[ArgoCD polls repo<br/>every 3 min] --> ARGO2{Drift<br/>detected?}
        ARGO2 -- Yes --> ARGO3[Render Helm Chart]
        ARGO3 --> ARGO4[Apply to Cluster]
        ARGO2 -- No --> ARGO1
    end

    subgraph K8S["☸️ Kubernetes Cluster β€” webapps namespace"]
        ING[NGINX Ingress<br/>go-web-app.local] --> SVC[ClusterIP Service<br/>port 80]
        SVC --> POD1[Pod 1<br/>:8080]
        SVC --> POD2[Pod 2<br/>:8080]
    end

    USER[🌐 Internet User] -->|DNS| ING
    DEV2 --> CI1
    CI3 --> REPO
    REPO --> ARGO1
    ARGO4 --> K8S

    style DEV fill:#1e3a5f,color:#fff
    style CI fill:#2d4a2d,color:#fff
    style GITOPS fill:#4a2d1e,color:#fff
    style CD fill:#3d1e4a,color:#fff
    style K8S fill:#1e3a4a,color:#fff
Loading

🐹 Go Language Concepts Used

1. HTTP Server & ServeMux

mux := http.NewServeMux()   // Local multiplexer, avoids global state pollution
mux.HandleFunc("/home", homePage)
mux.HandleFunc("/healthz", healthzHandler)

Why a local mux? Using http.DefaultServeMux (the global one) is dangerous in larger projects β€” any imported package can silently register routes on it. A local ServeMux gives you full control.

2. Goroutines & Channels

sequenceDiagram
    participant main
    participant goroutine as Server Goroutine
    participant OS

    main->>goroutine: go srv.ListenAndServe()
    main->>OS: signal.Notify(quit, SIGTERM, SIGINT)
    Note over main: blocks on select{}

    OS-->>main: SIGTERM from Kubernetes
    main->>goroutine: srv.Shutdown(ctx)
    goroutine-->>main: in-flight requests drained
    main->>main: os.Exit(0)
Loading
serverErrors := make(chan error, 1)
go func() {
    serverErrors <- srv.ListenAndServe()  // Non-blocking start
}()

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

select {
case err := <-serverErrors:   // Port already in use, etc.
    log.Fatalf("server error: %v", err)
case sig := <-quit:           // OS signal received
    srv.Shutdown(ctx)         // Graceful drain
}

Key Go concepts at play:

  • Goroutines β€” lightweight threads (2 KB stack vs. 1 MB for OS threads)
  • Channels β€” typed message-passing between goroutines (chan error, chan os.Signal)
  • select statement β€” waits on multiple channel operations simultaneously (like a switch for channels)
  • Buffered channel (make(chan error, 1)) β€” prevents goroutine leak if no one reads the error immediately

3. Context & Timeouts

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

srv.Shutdown(ctx)  // Gracefully drain requests within 30 seconds

context.WithTimeout creates a context that automatically cancels after 30 seconds. This is Go's idiomatic way to propagate deadlines through the call stack. defer cancel() prevents a context leak.

4. HTTP Handler Pattern

// All handlers follow the same signature: http.HandlerFunc
func homePage(w http.ResponseWriter, r *http.Request) {
    http.ServeFile(w, r, "static/home.html")
}

Go's http.HandlerFunc is a function type that implements the http.Handler interface. This is Go's take on the strategy pattern β€” any function with the right signature is a handler.

5. Server Timeouts (DoS Prevention)

srv := &http.Server{
    Addr:         "0.0.0.0:8080",
    ReadTimeout:  10 * time.Second,   // Max time to read request
    WriteTimeout: 10 * time.Second,   // Max time to write response
    IdleTimeout:  60 * time.Second,   // Keep-alive connection idle timeout
}

Without timeouts, a slow client can hold connections open forever, eventually exhausting the server's file descriptors (the "Slowloris" attack).

6. Table-Driven Tests

cases := []struct {
    name       string
    handler    http.HandlerFunc
    wantStatus int
}{
    {"home page", homePage, 200},
    {"healthz probe", healthzHandler, 200},
    {"readyz probe", readyzHandler, 200},
}

for _, tc := range cases {
    t.Run(tc.name, func(t *testing.T) {
        rr := httptest.NewRecorder()
        req, _ := http.NewRequest("GET", tc.path, nil)
        tc.handler.ServeHTTP(rr, req)
        // assertions...
    })
}

Table-driven tests are idiomatic Go. Adding a new case is a single struct literal β€” no new test function needed. httptest.NewRecorder() lets handlers run without a real TCP connection.

7. Graceful Shutdown Flow

flowchart TD
    A[Kubernetes sends SIGTERM] --> B[OS signal received on quit channel]
    B --> C["context.WithTimeout(30s) created"]
    C --> D[srv.Shutdown ctx called]
    D --> E{Active requests?}
    E -- Yes --> F[Wait for requests to complete]
    F --> E
    E -- No --> G[Server stops accepting new connections]
    G --> H[Process exits cleanly]
    C -->|30s elapsed| I[Force shutdown]
    I --> H
Loading

πŸ–₯️ Application Design

Route Table

Method Path Handler Description
GET /home homePage Serves static/home.html
GET /courses coursePage Serves static/courses.html
GET /about aboutPage Serves static/about.html
GET /contact contactPage Serves static/contact.html
GET /healthz healthzHandler Liveness probe β†’ responds ok
GET /readyz readyzHandler Readiness probe β†’ responds ready

Health Check Design

stateDiagram-v2
    [*] --> Starting: Pod created
    Starting --> Ready: /readyz returns 200
    Ready --> Running: Traffic routed to pod
    Running --> Unhealthy: /healthz fails 3x
    Unhealthy --> [*]: Kubernetes restarts container
    Running --> Draining: SIGTERM received
    Draining --> [*]: Graceful shutdown complete
Loading

Liveness vs. Readiness β€” Why Two Probes?

Probe Endpoint Failure Action Checks
Liveness /healthz Container restarted Is the process alive?
Readiness /readyz Pod removed from load balancer Is the app ready for traffic?

🐳 Docker & Containerization

Multi-Stage Build Strategy

graph LR
    subgraph Stage1["Stage 1: builder (golang:1.22.5-alpine3.20)"]
        S1A[COPY go.mod] --> S1B[go mod download]
        S1B --> S1C[COPY source code]
        S1C --> S1D["go build -ldflags='-w -s' -o main"]
        S1D --> S1E[Binary: /app/main]
    end

    subgraph Stage2["Stage 2: final (distroless/static-debian12:nonroot)"]
        S2A[COPY binary from builder] --> S2B[COPY static/ from builder]
        S2B --> S2C[USER nonroot:nonroot]
        S2C --> S2D[EXPOSE 8080]
        S2D --> S2E["ENTRYPOINT [./main]"]
    end

    S1E -->|Only binary + static files copied| Stage2

    style Stage1 fill:#1a3a5c,color:#fff
    style Stage2 fill:#1a3c1a,color:#fff
Loading

Image Size Comparison

Image Size Shell Package Manager Attack Surface
golang:1.22.5 ~800 MB bash apt High
golang:1.22.5-alpine ~250 MB sh apk Medium
gcr.io/distroless/static ~20 MB None None Minimal

Build Flags Explained

CGO_ENABLED=0    # Disable C interop β†’ fully static binary (required for distroless)
GOOS=linux       # Cross-compile for Linux (works from macOS/Windows dev machines)
GOARCH=amd64     # Target x86-64 CPUs (most cloud VMs)
-ldflags="-w -s" # Strip debug symbols β†’ reduces binary size by ~30%
-o main          # Output binary name

Layer Caching Optimization

# CORRECT: go.mod copied first (changes rarely β†’ cached)
COPY go.mod ./
RUN go mod download      # cached unless go.mod changes

COPY . .                 # source code (changes often β†’ cache miss here only)
RUN go build ...

Copying go.mod before the rest of the source code means go mod download is only re-executed when dependencies change, saving 30–60 seconds per build.


☸️ Kubernetes Architecture

Resource Hierarchy

graph TD
    NS[Namespace: webapps] --> DEP[Deployment: go-web-app]
    NS --> SVC[Service: go-web-app<br/>ClusterIP :80]
    NS --> ING[Ingress: go-web-app<br/>go-web-app.local]

    DEP --> RS[ReplicaSet]
    RS --> POD1[Pod 1<br/>container: :8080]
    RS --> POD2[Pod 2<br/>container: :8080]

    ING -->|routes to| SVC
    SVC -->|load balances| POD1
    SVC -->|load balances| POD2

    POD1 -->|liveness| LV1[GET /healthz]
    POD1 -->|readiness| RD1[GET /readyz]
    POD2 -->|liveness| LV2[GET /healthz]
    POD2 -->|readiness| RD2[GET /readyz]

    style NS fill:#1e3a5f,color:#fff
    style DEP fill:#2d4a2d,color:#fff
    style SVC fill:#4a3d1e,color:#fff
    style ING fill:#3d1e4a,color:#fff
Loading

Rolling Update Strategy

sequenceDiagram
    participant K8s as Kubernetes Scheduler
    participant OLD1 as Old Pod 1
    participant OLD2 as Old Pod 2
    participant NEW1 as New Pod 1
    participant NEW2 as New Pod 2

    Note over K8s: New image tag detected
    K8s->>NEW1: Create new pod (3 pods running)
    K8s->>NEW1: Poll /readyz...
    NEW1-->>K8s: 200 Ready
    K8s->>OLD1: Send SIGTERM
    OLD1-->>K8s: Drain in-flight requests (30s)
    K8s->>OLD1: Pod terminated
    Note over K8s: Back to 2 pods
    K8s->>NEW2: Create new pod
    K8s->>NEW2: Poll /readyz...
    NEW2-->>K8s: 200 Ready
    K8s->>OLD2: Send SIGTERM
    OLD2-->>K8s: Drain and terminate
    Note over K8s: Rolling update complete
Loading

Rolling update config (maxSurge: 1, maxUnavailable: 0):

  • maxSurge: 1 β€” Allow 1 extra pod during updates (temporarily 3 pods instead of 2)
  • maxUnavailable: 0 β€” Never remove an old pod until the new one is healthy
  • Result: True zero-downtime deployments

Security Context (Defense-in-Depth)

# Pod level
securityContext:
  runAsNonRoot: true        # No root processes
  runAsUser: 65532          # Distroless nonroot user

# Container level
securityContext:
  allowPrivilegeEscalation: false  # Cannot sudo/setuid
  readOnlyRootFilesystem: true     # Cannot write to disk
  capabilities:
    drop: [ALL]                    # No Linux capabilities

Ingress Traffic Flow

flowchart LR
    USER["🌐 User"] -->|"DNS: go-web-app.local"| LB["Cloud Load Balancer"]
    LB --> NGINX["NGINX Ingress Controller"]
    NGINX -->|"path: /* β†’ rewrite: /"| SVC["Service: go-web-app<br/>ClusterIP :80"]
    SVC -->|Round Robin| P1["Pod 1 :8080"]
    SVC -->|Round Robin| P2["Pod 2 :8080"]
    P1 --> HTML["static/*.html"]
    P2 --> HTML

    style USER fill:#2d4a6e,color:#fff
    style LB fill:#6e4a2d,color:#fff
    style NGINX fill:#2d6e4a,color:#fff
Loading

Rate limiting: NGINX Ingress limits each client IP to 20 requests/second (nginx.ingress.kubernetes.io/limit-rps: "20"), providing basic DDoS protection.

Resource Management

Resource Request Limit Reasoning
CPU 50m (5% core) 100m (10% core) Static file server; very CPU-light
Memory 64 Mi 128 Mi Go binary (~10MB) + OS overhead

Requests guide the Kubernetes scheduler (which node to place the pod on). Limits prevent one bad pod from starving the entire node.


πŸ“¦ Helm Chart

Helm is a package manager for Kubernetes. Instead of maintaining separate YAML files for dev/staging/prod, Helm lets you write templates once and override values per environment.

Chart Structure

helm/go-web-app/
β”œβ”€β”€ Chart.yaml          # Chart metadata (name, version, description)
β”œβ”€β”€ values.yaml         # Default configuration values
└── template/           # Kubernetes manifest templates
    β”œβ”€β”€ deployment.yaml
    β”œβ”€β”€ service.yaml
    └── ingress.yaml

How Values Flow Through the Chart

graph LR
    V["values.yaml<br/>image.tag: sha-abc1234"] -->|helm template| T[Template Engine]
    OV["Override values<br/>--set image.tag=sha-xyz"] -->|higher priority| T
    T --> D["deployment.yaml<br/>image: sars2006/go-web-app:sha-abc1234"]
    T --> S[service.yaml]
    T --> I[ingress.yaml]
    D --> K[kubectl apply]
    S --> K
    I --> K

    style V fill:#1a3a5c,color:#fff
    style T fill:#3a1a5c,color:#fff
    style K fill:#1a5c3a,color:#fff
Loading

Key Configuration Options

replicaCount: 2              # Number of pods

image:
  repository: sars2006/go-web-app
  tag: sha-2b6580fec7        # Auto-updated by CI pipeline
  pullPolicy: IfNotPresent

service:
  type: ClusterIP            # Internal only; Ingress handles external traffic
  port: 80

ingress:
  enabled: true
  host: go-web-app.local     # Replace with real domain in production
  tls:
    enabled: false           # Enable with cert-manager for HTTPS

βš™οΈ CI/CD Pipeline

Pipeline Flow

flowchart TD
    PUSH[git push to main] --> J1

    subgraph J1["Job 1: Test and Lint"]
        T1[actions/checkout@v4] --> T2["actions/setup-go@v5<br/>Go 1.22.5 + module cache"]
        T2 --> T3["go vet ./...<br/>static analysis"]
        T3 --> T4["go test -race -v -count=1 ./...<br/>unit tests + race detector"]
    end

    J1 -->|tests pass| J2

    subgraph J2["Job 2: Build and Push (push only, not PRs)"]
        B1[docker/setup-buildx-action@v3] --> B2["docker/login-action@v3<br/>DockerHub credentials"]
        B2 --> B3["docker/metadata-action@v5<br/>tags: latest + sha-abc1234"]
        B3 --> B4["docker/build-push-action@v5<br/>push: true + registry cache"]
    end

    J2 -->|image pushed| J3

    subgraph J3["Job 3: Update Manifest (GitOps)"]
        M1[Checkout repo with GITHUB_TOKEN] --> M2["sed -i 's/tag:.*/tag: sha-abc1234/'<br/>helm/go-web-app/values.yaml"]
        M2 --> M3["git commit 'chore: update image tag skip ci'"]
        M3 --> M4[git push]
    end

    J3 --> ARGOCD[ArgoCD detects new commit<br/>and syncs to cluster]

    style J1 fill:#1a3a5c,color:#fff
    style J2 fill:#1a5c3a,color:#fff
    style J3 fill:#5c3a1a,color:#fff
    style ARGOCD fill:#3a1a5c,color:#fff
Loading

Why This Order?

Tests (seconds) β†’ Docker Build (minutes) β†’ Manifest Update (seconds)

Running tests first catches broken code before wasting 2–3 minutes building a Docker image that will never be deployed.

Concurrency Control

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

If two pushes happen in quick succession, the older pipeline run is cancelled. This prevents a race condition where an older build could overwrite a newer image tag in values.yaml.

Image Tagging Strategy

Tag Example Purpose
latest sars2006/go-web-app:latest Human-friendly, mutable alias
sha-<short> sars2006/go-web-app:sha-a1b2c3d Immutable, traceable to exact commit

Always use SHA tags in Kubernetes manifests. The latest tag is mutable β€” it changes silently and makes rollbacks and debugging extremely difficult.

Secrets Configuration

Secret Location Used By
DOCKER_USERNAME GitHub Repo β†’ Settings β†’ Secrets Job 2: DockerHub login
DOCKER_PASSWORD GitHub Repo β†’ Settings β†’ Secrets Job 2: DockerHub login
GITHUB_TOKEN Auto-provided by GitHub Actions Job 3: commit back to repo

πŸ”„ GitOps with ArgoCD

What is GitOps?

GitOps means Git is the single source of truth for your cluster's desired state. Instead of running kubectl apply or helm upgrade manually, you commit a change and ArgoCD applies it automatically.

graph LR
    subgraph Traditional["Traditional Imperative Approach"]
        A1[Developer] -->|"kubectl apply / helm upgrade"| A2[Kubernetes Cluster]
    end

    subgraph GitOps["GitOps Declarative Approach"]
        B1[Developer] -->|git commit| B2[Git Repository]
        B2 -->|ArgoCD watches| B3[ArgoCD]
        B3 -->|kubectl apply| B4[Kubernetes Cluster]
        B4 -.->|drift detected β€” self-heal| B3
    end

    style Traditional fill:#5c1a1a,color:#fff
    style GitOps fill:#1a5c1a,color:#fff
Loading

ArgoCD Sync Loop

sequenceDiagram
    participant GIT as Git Repository
    participant ARGO as ArgoCD Controller
    participant K8S as Kubernetes API

    loop Every 3 minutes
        ARGO->>GIT: Poll for new commits
        GIT-->>ARGO: Latest commit SHA
        ARGO->>K8S: Get live state
        K8S-->>ARGO: Current resources
        ARGO->>ARGO: Compare desired vs live state
        alt Drift detected
            ARGO->>ARGO: Render Helm chart
            ARGO->>K8S: Apply manifests
            K8S-->>ARGO: Applied successfully
        else In sync
            ARGO->>ARGO: Nothing to do
        end
    end
Loading

ArgoCD App Configuration

spec:
  source:
    repoURL: 'https://github.com/DevSars24/go-web-app.git'
    path: helm/go-web-app        # Helm chart location in repo
    targetRevision: HEAD         # Always track latest main branch

  destination:
    server: 'https://kubernetes.default.svc'
    namespace: webapps

  syncPolicy:
    automated:
      prune: true       # Delete resources removed from Git
      selfHeal: true    # Revert manual kubectl changes automatically

Key ArgoCD Features Used

Feature Config Effect
Auto-sync automated: {} Deploys on every new Git commit
Prune prune: true Removes orphaned K8s resources
Self-heal selfHeal: true Reverts manual cluster changes
CreateNamespace syncOptions Auto-creates webapps namespace
ServerSideApply syncOptions Better merge handling, no annotation limits
Cascade delete finalizers Cleans up all resources when ArgoCD app is deleted

Rollback Process

# With GitOps, rollback = revert a commit
git revert <bad-commit-sha>
git push origin main
# ArgoCD detects the revert and rolls back the cluster automatically

πŸ—ΊοΈ Full System Design

End-to-End Component View

graph TB
    subgraph INTERNET["Internet"]
        USER["🌐 End User<br/>Browser"]
        DEV["πŸ‘¨β€πŸ’» Developer<br/>git push"]
    end

    subgraph GITHUB["GitHub"]
        REPO["πŸ“ Repository<br/>Source Code + Helm Chart"]
        ACTIONS["βš™οΈ GitHub Actions<br/>CI Pipeline"]
    end

    subgraph REGISTRY["Container Registry"]
        DHUB["🐳 DockerHub<br/>sars2006/go-web-app"]
    end

    subgraph CLUSTER["☸️ Kubernetes Cluster"]
        subgraph ARGOCD_NS["argocd namespace"]
            ARGO["πŸ”„ ArgoCD<br/>GitOps Operator"]
        end

        subgraph INGRESS_NS["ingress-nginx namespace"]
            NGINX["πŸ”€ NGINX Ingress<br/>Controller"]
        end

        subgraph WEBAPPS_NS["webapps namespace"]
            ING_RES["Ingress Resource<br/>go-web-app.local"]
            SVC["ClusterIP Service<br/>:80"]
            POD1["Pod 1<br/>Go App :8080"]
            POD2["Pod 2<br/>Go App :8080"]
        end
    end

    DEV -->|1. git push| REPO
    REPO -->|2. triggers| ACTIONS
    ACTIONS -->|3. docker push| DHUB
    ACTIONS -->|4. update values.yaml| REPO
    REPO -->|5. poll for changes| ARGO
    DHUB -->|6. pull image| POD1
    DHUB -->|6. pull image| POD2
    ARGO -->|7. apply manifests| WEBAPPS_NS
    USER -->|HTTP request| NGINX
    NGINX --> ING_RES
    ING_RES --> SVC
    SVC --> POD1
    SVC --> POD2

    style INTERNET fill:#1a1a2e,color:#fff
    style GITHUB fill:#0d1117,color:#fff
    style REGISTRY fill:#1c2a4a,color:#fff
    style CLUSTER fill:#0f2027,color:#fff
    style ARGOCD_NS fill:#2d1b69,color:#fff
    style INGRESS_NS fill:#1b2d69,color:#fff
    style WEBAPPS_NS fill:#1b6940,color:#fff
Loading

End-to-End Request Journey

sequenceDiagram
    participant B as Browser
    participant DNS as DNS Resolver
    participant LB as Cloud Load Balancer
    participant NGINX as NGINX Ingress
    participant SVC as K8s Service
    participant POD as Go App Pod

    B->>DNS: resolve go-web-app.local
    DNS-->>B: Load Balancer IP
    B->>LB: GET /courses HTTP/1.1
    LB->>NGINX: forward request
    NGINX->>NGINX: match Host header + path rewrite
    NGINX->>SVC: forward to ClusterIP:80
    SVC->>POD: load balance to :8080
    POD->>POD: http.ServeFile courses.html
    POD-->>SVC: HTTP 200 + HTML body
    SVC-->>NGINX: response
    NGINX-->>LB: response
    LB-->>B: HTTP 200
Loading

πŸ“ Project Structure

go-web-app/
β”œβ”€β”€ main.go                       # HTTP server, routes, graceful shutdown
β”œβ”€β”€ main_test.go                  # Table-driven tests with httptest
β”œβ”€β”€ go.mod                        # Go module definition (zero external deps)
β”‚
β”œβ”€β”€ Dockerfile                    # Multi-stage build β†’ ~20 MB distroless image
β”‚
β”œβ”€β”€ static/                       # Static HTML pages served by the Go app
β”‚   β”œβ”€β”€ home.html
β”‚   β”œβ”€β”€ courses.html
β”‚   β”œβ”€β”€ about.html
β”‚   └── contact.html
β”‚
β”œβ”€β”€ K8s/manifests/                # Raw Kubernetes manifests
β”‚   β”œβ”€β”€ namespace.yaml            # Creates the webapps namespace
β”‚   β”œβ”€β”€ deployment.yaml           # 2 replicas, rolling update, security contexts
β”‚   β”œβ”€β”€ service.yaml              # ClusterIP service on port 80
β”‚   └── ingress.yaml              # NGINX Ingress with rate limiting
β”‚
β”œβ”€β”€ helm/go-web-app/              # Helm chart (recommended for production)
β”‚   β”œβ”€β”€ Chart.yaml                # Chart metadata
β”‚   β”œβ”€β”€ values.yaml               # Default values (image tag auto-updated by CI)
β”‚   └── template/                 # Parameterized K8s manifests
β”‚
β”œβ”€β”€ argocd-app.yaml               # ArgoCD Application manifest (GitOps CD)
β”‚
└── .github/workflows/
    └── cicd.yml                  # 3-job GitHub Actions pipeline

πŸš€ Quick Start

Run Locally

# Clone the repository
git clone https://github.com/DevSars24/go-web-app.git
cd go-web-app

# Run directly with Go (no Docker needed)
go run main.go

# Visit http://localhost:8080/home

Run with Docker

# Build the image
docker build -t go-web-app:local .

# Run the container
docker run -p 8080:8080 go-web-app:local

# Visit http://localhost:8080/home

Deploy to Kubernetes with Helm

# Install NGINX Ingress Controller (one-time setup)
helm upgrade --install ingress-nginx ingress-nginx \
  --repo https://kubernetes.github.io/ingress-nginx \
  --namespace ingress-nginx --create-namespace

# Deploy the application
helm upgrade --install go-web-app ./helm/go-web-app \
  --namespace webapps --create-namespace

# Add to /etc/hosts for local testing
echo "$(minikube ip) go-web-app.local" | sudo tee -a /etc/hosts

# Visit http://go-web-app.local

Set Up ArgoCD

# Install ArgoCD
kubectl create namespace argocd
kubectl apply -n argocd -f \
  https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

# Register the application
kubectl apply -f argocd-app.yaml

# Access ArgoCD UI
kubectl port-forward svc/argocd-server -n argocd 8080:443
# Open https://localhost:8080

πŸ“‘ API Reference

Application Endpoints

Endpoint Status Code Response Body Content-Type
GET /home 200 OK home.html text/html; charset=utf-8
GET /courses 200 OK courses.html text/html; charset=utf-8
GET /about 200 OK about.html text/html; charset=utf-8
GET /contact 200 OK contact.html text/html; charset=utf-8
GET /healthz 200 OK ok\n text/plain; charset=utf-8
GET /readyz 200 OK ready\n text/plain; charset=utf-8

Health Check Examples

# Liveness probe
curl http://localhost:8080/healthz
# Output: ok

# Readiness probe
curl http://localhost:8080/readyz
# Output: ready

πŸ§ͺ Testing

# Run all tests
go test ./...

# Run with verbose output
go test -v ./...

# Run with race detector (detects concurrent access bugs)
go test -race ./...

# Run a specific test
go test -run TestHealthzBody ./...

# Check test coverage
go test -cover ./...

Test Design

Tests use net/http/httptest β€” no real network connections, no ports opened:

graph LR
    TC[Test Case] -->|http.NewRequest| REQ["*http.Request"]
    REQ --> REC[httptest.NewRecorder]
    REC -->|handler.ServeHTTP| H[Handler Function]
    H --> RESP[Captured Response in Memory]
    RESP --> ASSERT["Assertions:<br/>status code, Content-Type, body"]

    style TC fill:#1a3a5c,color:#fff
    style ASSERT fill:#1a5c1a,color:#fff
Loading

πŸ”’ Security Hardening

Defense-in-Depth Model

graph TB
    subgraph L1["Layer 1: Container Image"]
        IMG["Distroless base image<br/>No shell Β· No package manager Β· No debug tools"]
    end

    subgraph L2["Layer 2: Container Runtime"]
        RT1[Non-root user uid=65532]
        RT2[Read-only root filesystem]
        RT3[No Linux capabilities]
        RT4[No privilege escalation]
    end

    subgraph L3["Layer 3: Network"]
        NET1["NGINX rate limiting (20 req/sec per IP)"]
        NET2[ClusterIP Service β€” no external exposure]
        NET3[TLS-ready Ingress with cert-manager support]
    end

    subgraph L4["Layer 4: Application"]
        APP1[Server timeouts β€” 10s read/write]
        APP2[Graceful shutdown β€” 30s drain window]
        APP3[Zero external dependencies β€” no library CVEs]
    end

    L1 --> L2 --> L3 --> L4

    style L1 fill:#5c1a1a,color:#fff
    style L2 fill:#5c3a1a,color:#fff
    style L3 fill:#1a3a5c,color:#fff
    style L4 fill:#1a5c1a,color:#fff
Loading

Security Checklist

  • Distroless base image β€” minimal attack surface (~20 MB)
  • Non-root container user β€” uid=65532 (nonroot)
  • Read-only filesystem β€” readOnlyRootFilesystem: true
  • No Linux capabilities β€” drop: [ALL]
  • No privilege escalation β€” allowPrivilegeEscalation: false
  • Server timeouts β€” prevents Slowloris-style attacks
  • Rate limiting β€” 20 req/sec per IP via NGINX
  • Immutable image tags β€” SHA-based tags, not latest
  • Secrets in GitHub Secrets β€” never in source code
  • Least-privilege CI β€” only contents: write permission
  • No external Go dependencies β€” stdlib only, zero CVE exposure

🀝 Contributing

  1. Fork the repository
  2. Create a feature branch: git checkout -b feature/my-feature
  3. Commit your changes: git commit -m 'feat: add my feature'
  4. Push to the branch: git push origin feature/my-feature
  5. Open a Pull Request β†’ CI runs tests automatically on every PR

πŸ“„ License

This project is licensed under the MIT License β€” see LICENSE for details.


Built with Go Β· Docker Β· Kubernetes Β· Helm Β· GitHub Actions Β· ArgoCD

The best way to learn DevOps is to build something real.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages