This repository contains a simple Go REST API backed by PostgreSQL.
The main point of the project is the DevOps delivery process around it:
- CI on Pull Requests / pushes → lint + SAST + build + tests + migrations + coverage
- CD on push to
main→ build & push Docker image to GHCR + deploy to Kubernetes (k3d)
The topics below are aligned with the course list (SDLC, collaborate, source control, branching strategies, building pipelines, CI, CD, security, Docker, Kubernetes, public cloud, IaC, database changes):
- Collaborate: team workflow via Pull Requests, reviews, and CI status checks before merge
- Source control: GitHub repository as a single source of truth
- Branching strategies: feature branches + merge into
mainthrough PRs - Building Pipelines: GitHub Actions workflows (
ci.yml,cd.yml) define the delivery process as code - Continuous Integration: automated lint + SAST + build + tests + migrations + coverage on PRs (and pushes)
- Continuous Delivery: on push to
mainthe pipeline publishes a new container image and deploys it to Kubernetes - Security: SAST scanning with gosec + required-secrets validation as a pipeline gate
- Docker: containerization via
Dockerfile+ consistent runtime environment - Kubernetes: manifests for Namespace, Deployment, Services, and PostgreSQL StatefulSet; rolling updates on new releases
- Public Cloud: uses GitHub-hosted services (GitHub Actions + GHCR) for CI/CD and artifact hosting
- Infrastructure as Code: Kubernetes YAML manifests + GitHub Actions YAML (reproducible infrastructure and pipelines)
- Database changes: schema migrations executed automatically (init container) and DB deployed as part of the stack (StatefulSet + PVC)
CI runs on:
- Pull Requests to
main - Pushes to
main
Pipeline steps:
- Validate required GitHub secrets exist
- Setup Go & download dependencies
- Lint (
golangci-lint) - SAST (
gosec) - Build (
make build) - Run migrations + tests against Postgres (
make migrate,make test-cover) - Upload coverage artifact (
coverage-all.out)
If CI fails → the change should not be merged.
CD runs on pushes to main and includes:
- Build & push Docker image to GHCR (multi-arch)
- tags:
:latestand:<commit_sha>
- tags:
- Deploy to Kubernetes via a self-hosted runner (labels:
self-hosted,k3d)- pull image from GHCR
- import image into k3d
- apply Kubernetes manifests
- rollout restart / rollout status checks
flowchart LR
A[Feature branch] --> B[Pull Request]
B --> C[CI: secret check]
C --> D[CI: lint]
D --> E[CI: gosec SAST]
E --> F[CI: build + migrations + tests]
F -->|green| G[Merge to main]
F -->|red| X[Fix issues]
G --> H[CD: buildx multi-arch image]
H --> I[CD: push to GHCR :sha + :latest]
I --> J[CD: deploy to k3d via self-hosted runner]
J --> K[Kubernetes rollout update]
All Kubernetes resources for the application are stored in the deploy/k8s/ folder.
This keeps the infrastructure configuration versioned together with the application code (Infrastructure as Code).
Creates a dedicated namespace for the whole stack:
- Namespace:
go-rest
This isolates the project resources from other workloads in the cluster. :contentReference[oaicite:0]{index=0}
Creates a headless Service for PostgreSQL:
clusterIP: None(headless service)- Exposes port
5432 - Selects pods with label
app: postgres
A headless service is commonly used with StatefulSets so the DB can be reached reliably through stable DNS (e.g. postgres.go-rest.svc.cluster.local). :contentReference[oaicite:1]{index=1}
Runs PostgreSQL as a StatefulSet with persistent storage:
- Image:
postgres:15-alpine - Replicas:
1 - Uses a PVC template (
pgdata) with 1Gi storage - Loads credentials from the Kubernetes Secret
postgres-secret - Uses
PGDATA=/var/lib/postgresql/data/pgdata - Has readiness and liveness probes using
pg_isready - Uses
fsGroup: 999so Postgres can write to the mounted volume
This ensures the database data survives Pod restarts. :contentReference[oaicite:2]{index=2}
Deploys the Go REST API as a Deployment and ensures DB migrations run automatically before the app starts.
Key points:
- Deployment name:
go-rest-api - Namespace:
go-rest - Replicas:
1 - Two initContainers:
-
copy-migrations- Uses the same app image (
ghcr.io/...:latest) - Copies
/app/migrationsinto a sharedemptyDirvolume mounted at/migrations
- Uses the same app image (
-
migrate- Uses
migrate/migrate:v4.19.1 - Reads the DSN from
/etc/go-rest-api/config.yml(mounted from secret) - Retries migrations up to 30 times before failing
- Uses
Main container:
- Runs
./server -config /etc/go-rest-api/config.yml - Exposes port
8080
Volumes:
app-config: secretgo-rest-api-configmounted to/etc/go-rest-apimigrations:emptyDirshared between init containers and the app
This pattern guarantees the schema is applied before the API begins serving traffic. :contentReference[oaicite:3]{index=3}
Exposes the Go API inside the cluster using a ClusterIP Service:
- Service name:
go-rest-api - Port
80→ targetPort8080 - Selects pods with label
app: go-rest-api
This enables internal cluster communication and is also convenient for local access via kubectl port-forward. :contentReference[oaicite:4]{index=4}