Production-ready Kubernetes infrastructure on DigitalOcean, provisioned with Terraform.
| Service | URL | Description |
|---|---|---|
| Grafana | https://grafana.gast-k8s.me | Metrics & dashboards (Prometheus) |
| Headlamp | https://dashboard.gast-k8s.me | Kubernetes web UI |
Internet
│
▼
DigitalOcean Load Balancer (public IP: 143.244.221.228)
│ Automatically provisioned when nginx requests a LoadBalancer Service
│
▼
nginx Ingress Controller (pod inside k8s)
│ Routes based on hostname:
│ grafana.gast-k8s.me → Grafana Service
│ dashboard.gast-k8s.me → Headlamp Service
│
▼
Your application pods
TLS flow:
nginx receives HTTPS request
→ uses TLS cert stored in a k8s Secret
→ cert was issued by Let's Encrypt via cert-manager
→ cert-manager auto-renews 30 days before expiry
k8s/
├── versions.tf # Terraform version + required providers
├── providers.tf # Provider configuration (credentials) — root only
├── variables.tf # Input variables
├── outputs.tf # Outputs (URLs, IP, cluster name)
├── main.tf # Module wiring — the "table of contents"
└── modules/
├── cluster/ # DigitalOcean Kubernetes cluster + node pool
├── ingress/ # nginx ingress controller + DigitalOcean DNS domain
├── cert-manager/ # cert-manager + Let's Encrypt ClusterIssuer
├── monitoring/ # kube-prometheus-stack (Prometheus + Grafana + Alertmanager)
└── dashboard/ # Headlamp Kubernetes UI
Provider architecture: Provider configuration (credentials, endpoints) lives only in providers.tf at the root. Child modules declare required_providers only to specify the registry source for non-HashiCorp providers — no credentials, no duplication.
- Terraform >= 1.5
- kubectl
- doctl (DigitalOcean CLI)
- A DigitalOcean account with API token
- PostgreSQL running for Terraform state backend:
cd ~/dev/proj1 && docker compose up -d
# 1. Set environment variables
source ../.env # sets TF_VAR_do_token, PG_CONN_STR, TF_VAR_letsencrypt_email
# 2. Initialize
terraform init
# 3. Preview changes
terraform plan
# 4. Apply
terraform apply
# 5. See outputs
terraform outputConfigure kubectl:
doctl kubernetes cluster kubeconfig save <cluster-id> --access-token $TF_VAR_do_token
kubectl get nodesKubernetes (k8s) is a container orchestration platform. It answers the question: "I have Docker containers — how do I run them reliably at scale?"
Key things Kubernetes handles:
- Scheduling — decides which node (server) runs each container
- Self-healing — restarts crashed containers, replaces failed nodes
- Scaling — adds/removes container instances based on load
- Networking — gives each container a stable internal address
- Rolling updates — deploy new versions with zero downtime
Control Plane (managed by DigitalOcean) Worker Nodes (your node pool)
┌──────────────────────────────────┐ ┌──────────────────┐
│ API Server ← kubectl talks here│ │ kubelet │
│ etcd (stores all state) │◄─────►│ container runtime│
│ Scheduler (places pods) │ │ your pods │
│ Controller (maintains state) │ └──────────────────┘
└──────────────────────────────────┘
With DOKS (DigitalOcean Kubernetes Service), the control plane is managed for you — you only pay for and manage the worker nodes.
The smallest deployable unit. A pod runs one or more containers that share network and storage.
apiVersion: v1
kind: Pod
metadata:
name: my-app
spec:
containers:
- name: app
image: nginx:latest
ports:
- containerPort: 80Pods are ephemeral — they die and get replaced. Never rely on a pod's IP or name being stable.
Manages a set of identical pods. Ensures a specified number of replicas are always running. Handles rolling updates.
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 3 # always keep 3 pods running
selector:
matchLabels:
app: my-app
template: # pod template
spec:
containers:
- name: app
image: nginx:1.25A stable network endpoint for a set of pods. Pods come and go, but the Service IP stays constant.
apiVersion: v1
kind: Service
metadata:
name: my-app
spec:
selector:
app: my-app # routes to pods with this label
ports:
- port: 80
targetPort: 8080
type: ClusterIP # internal only (use LoadBalancer for external)Types:
ClusterIP— internal cluster only (default)NodePort— exposes on each node's IP at a static portLoadBalancer— provisions a cloud load balancer (what nginx ingress uses)
HTTP routing rules. Maps hostnames/paths to Services. Requires an Ingress Controller (nginx) to actually process the rules.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
rules:
- host: myapp.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-app
port:
number: 80
tls:
- hosts:
- myapp.example.com
secretName: myapp-tlsVirtual cluster within a cluster. Isolates resources by team/environment/project.
kubectl get pods -n monitoring # pods in the monitoring namespace
kubectl get pods --all-namespaces # pods everywhereStore configuration outside of container images.
# ConfigMap — non-sensitive config
kubectl create configmap app-config --from-literal=ENV=production
# Secret — sensitive data (base64 encoded in etcd)
kubectl create secret generic db-creds \
--from-literal=password=supersecretStorage that outlives a pod. Like a virtual hard drive attached to your pod.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-data
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 10GiA reverse proxy that runs as a pod and processes Ingress resources. When you create an Ingress, nginx automatically updates its config and starts routing traffic. It also handles TLS termination.
Watches for Ingress resources with the cert-manager.io/cluster-issuer annotation. Automatically requests TLS certificates from Let's Encrypt, stores them in Kubernetes Secrets, and renews them before expiry.
- Prometheus — scrapes metrics from all pods every 15 seconds using
/metricsendpoints. Stores time-series data. Query with PromQL. - Grafana — visualises Prometheus data. Ships with pre-built k8s dashboards.
- Alertmanager — sends alerts (Slack, email, PagerDuty) when Prometheus rules trigger.
- kube-state-metrics — translates k8s object state into Prometheus metrics (pod restarts, deployment health, etc.)
- node-exporter — exposes CPU, RAM, disk metrics per node.
Web UI for Kubernetes. Lets you browse pods, view logs, inspect deployments, check events — all from a browser. Requires a service account token for authentication.
# Cluster overview
kubectl get nodes
kubectl get pods --all-namespaces
kubectl top nodes # CPU/RAM per node (requires metrics-server)
kubectl top pods -n monitoring
# Inspect a resource
kubectl describe pod <name> -n <namespace>
kubectl describe ingress -n headlamp
# Logs
kubectl logs <pod-name> -n <namespace>
kubectl logs <pod-name> -n <namespace> -f # follow (like tail -f)
kubectl logs <pod-name> -n <namespace> --previous # crashed pod logs
# Execute into a pod (like docker exec)
kubectl exec -it <pod-name> -n <namespace> -- /bin/sh
# Port forward (access a service locally without ingress)
kubectl port-forward svc/grafana 3000:80 -n monitoring
# Then open http://localhost:3000
# Check TLS certificates
kubectl get certificate -A
kubectl describe certificate grafana-tls -n monitoring
# Watch resources change in real time
kubectl get pods -n monitoring -w
# Get events (useful for debugging)
kubectl get events -n <namespace> --sort-by='.lastTimestamp'Store these in ~/dev/proj1/.env (never commit this file):
export TF_VAR_do_token=<your-digitalocean-token>
export TF_VAR_letsencrypt_email=<your-email>
export PG_CONN_STR=postgres://terraform:password@localhost:5432/terraform_backend?sslmode=disableState is stored in a PostgreSQL database (remote backend). This means:
- Multiple people can collaborate safely (state is not local)
- State is not lost if your laptop dies
- Terraform locks the state during operations to prevent conflicts
# If you need to inspect or manipulate state directly:
terraform state list # all resources in state
terraform state show <resource> # details of one resource
terraform state mv <old> <new> # move/rename a resource
terraform import <resource> <id> # import existing resource into state