A fully automated Kubernetes CI/CD platform that spins up an isolated, HTTPS-accessible preview environment for every pull request — and tears it down automatically on merge. No manual steps, no shared staging server, no idle costs.
Architecture
The system is split across two GitHub repositories to keep concerns separate. The application repository (DPP-App) holds frontend and backend source code alongside the build pipeline. The infrastructure repository (DPP-Kubernetes) holds the Helm chart, Bicep templates, and all deployment pipelines. Infrastructure and application concerns evolve independently.
The AKS cluster runs in Azure's Poland Central region on a single
Standard_B2s_v2 burstable node. Two cluster-level
services are bootstrapped once and persist for the cluster's
lifetime: ingress-nginx (HTTP/HTTPS reverse proxy,
single Azure public IP) and cert-manager (automatic
Let's Encrypt TLS for every new environment).
Application workloads live in isolated Kubernetes namespaces — one
permanent main namespace reflecting the current state
of the main branch, plus ephemeral
feature-* namespaces created per PR branch and
destroyed on merge.
How it works
Developer pushes a feature branch
A developer pushes any feature branch to the application repository. No manual steps are required from this point forward — the platform takes over entirely.
Build pipeline detects changes & builds only what changed
The pipeline diffs the branch against main and
builds only the service that actually changed — tagging the
Docker image with the branch name. A CSS fix in the frontend
does not trigger a backend build.
Deploy pipeline creates an isolated environment
Triggered automatically by the build pipeline completing,
the deploy pipeline runs
helm upgrade --install against a dedicated
namespace and generates a unique HTTPS URL for the branch.
Live environment is accessible over HTTPS
Within minutes of the push, the environment is publicly
reachable at
https://feature-<branch>.IP.nip.io — with
a real TLS certificate, accessible to anyone on the team
from any device.
Merge & automatic cleanup
Once the PR is merged, the environment is automatically removed — Helm release uninstalled, namespace deleted, all associated resources (pods, services, secrets, ingress rules, persistent volumes) cleaned up by Kubernetes. An hourly cleanup pipeline also reconciles running namespaces against live GitHub branches.
Engineering
Built on top of an existing local Kubernetes setup using
kind, this platform represents a significant step
forward in automation, accessibility, and engineering rigour.
From local to cloud-accessible environments
Running on kind requires manually loading Docker
images, managing port forwarding, and sharing access via
screen sharing. A preview environment on AKS is publicly
accessible over HTTPS with a real TLS certificate — anyone
with the URL can review a feature branch from any device,
anywhere.
GitOps-style PR preview environments
Each branch maps deterministically to a running environment —
feature/my-branch becomes namespace
feature-my-branch at
https://feature-my-branch.IP.nip.io. The cluster
state always reflects the Git state. There is no shared
staging server accumulating unknown changes, and no manual
environment management.
Path-based change detection
The build pipeline diffs the branch against
main and builds only the service that actually
changed. A CSS fix in the frontend does not trigger a backend
build. This reduces build times and Docker Hub push volume
significantly compared to rebuilding everything on every push.
Smart image tag resolution
When only one service changes on a new branch, the other
service's branch-specific image tag doesn't exist yet. Rather
than failing, the deploy pipeline checks Docker Hub and falls
back to main-latest for the unchanged service.
The environment is always deployable from the first push on
any new branch.
Forced rolling restarts on every deploy
A deploymentId annotation on pod templates is set
to the pipeline build ID on every deploy. When Helm sees this
value change it updates the pod spec, triggering a rolling
restart — preventing stale pods when only image content
changes without a tag change.
Pipeline trigger chaining across repositories
The deploy pipeline in the Kubernetes repository triggers automatically on completion of the build pipeline in the application repository using Azure DevOps pipeline resource triggers. The repositories are fully decoupled — neither needs to know the other's internals — but the full build-to-deploy flow is automatic.
Idempotent bootstrap
The bootstrap pipeline checks whether each component is already installed before acting, making it safe to rerun at any point. It serves as both the initial setup script and the recovery script. Recreating the entire cluster from scratch requires a single manual pipeline run with no command-line interaction.
Automated TLS for every environment
cert-manager watches for new ingress resources
and automatically provisions a Let's Encrypt certificate for
each one within minutes of namespace creation. There is no
certificate management in the pipeline — HTTPS works out of
the box for every preview environment.
Cost-aware scheduling as code
The cluster start/stop schedule is version-controlled pipeline YAML, not a manually configured Azure job. It can be reviewed, modified via PR, and rolled back like any other change. The schedule is self-documenting — the cron expressions and their intent are visible to anyone reading the repository.
Namespace isolation as a security boundary
Each environment runs in its own Kubernetes namespace with its own secrets, configmaps, and persistent volumes. A crash or misconfiguration in one environment cannot affect another. Environments are deleted cleanly with a single namespace deletion — all associated resources are removed automatically.
Business value
This platform was built with a clear business goal: reduce risk, reduce cost, and increase the speed at which a team can safely deliver value. Every technical decision is in service of those three outcomes.
"We caught bugs before they reached customers."
Every pull request gets its own live environment automatically. A tester, product owner, or non-technical stakeholder can click a link and see exactly what a change looks like before it goes live — without asking a developer to set anything up. Bugs get caught earlier, when they're cheap to fix.
"Deployments stopped being a scary event."
This pipeline makes deployments automatic, consistent, and identical every single time. A deployment at 9am on Monday follows exactly the same process as one at 11pm on Friday. The human is removed from the equation.
"We stopped paying for things we weren't using."
The scheduled start/stop cuts the infrastructure bill roughly in half — from approximately €37/month to €17/month — by shutting the cluster down outside working hours. Stale preview environments are cleaned up hourly rather than accumulating indefinitely.
"New developers could get productive on day one."
Because the entire environment is defined as code and cluster setup is a single pipeline run, a new developer doesn't spend their first week getting things working. They push a branch and have a running preview environment without needing to understand the infrastructure.
"We could prove exactly what was deployed and when."
Every Docker image is tagged with a build ID. Every deployment is logged in the pipeline with a timestamp and the triggering commit. If something goes wrong, you can trace back exactly what changed, when it was deployed, and by whom — in minutes rather than hours.
"Preview environments cleaned themselves up."
Stale environments don't require a developer to remember to delete them. The platform checks every hour whether a feature branch still exists in GitHub and removes the environment automatically if the branch has been merged or deleted.
Infrastructure
All costs are approximate, based on Azure Poland Central pricing
for Standard_B2s_v2. The scheduled stop/start
pipeline saves approximately €20/month — a
54% reduction — by running the cluster only
during working hours on weekdays.
| Component | Always on | With stop/start |
|---|---|---|
| AKS node (Standard_B2s_v2) | ~€31/mo | ~€11/mo |
| Azure Load Balancer | ~€3/mo | ~€3/mo |
| Managed OS disk (64GB) | ~€3/mo | ~€3/mo |
| cert-manager / ingress-nginx | Free | Free |
| nip.io DNS | Free | Free |
| Docker Hub (public repo) | Free | Free |
| Azure DevOps (private project) | Free | Free |
| Total | ~€37/mo | ~€17/mo |
Standard_B2s_v2 is a burstable VM — it earns CPU
credits when idle and spends them under load. Since the cluster is
idle most of the time, this model is well suited to a dev
environment with intermittent traffic.
Known constraints
This is a development environment designed for PR preview and demonstration purposes. The following are deliberate simplifications that would differ in a commercial project.
Persistent cluster with ephemeral namespaces
The AKS cluster itself is not ephemeral — only the PR namespaces are. A fully ephemeral cluster approach was ruled out due to the ~8 minute provisioning time making PR preview turnaround impractical.
Single node, no redundancy
A single Standard_B2s_v2 node with no
availability zone redundancy. A node failure takes down all
environments simultaneously. A commercial setup would use
multiple nodes across zones.
nip.io for dynamic URLs
Preview URLs are tied to the cluster IP via nip.io. If the cluster is recreated, the IP changes and all URLs change with it. A commercial setup would use a custom domain with wildcard DNS.
Let's Encrypt rate limits
Each environment gets its own TLS certificate — wildcard certificates are not possible for nip.io subdomains as Let's Encrypt wildcard issuance requires DNS-01 challenge with DNS control. Every new namespace consumes from the 50 weekly issuances Let's Encrypt allows, a quota shared globally across all nip.io users.
Shared MongoDB database across namespaces
All preview environments share a single MongoDB Atlas instance. Data created or modified in one namespace is visible to all others. In a commercial setup each environment would have its own isolated database. A deliberate simplification for a portfolio project.
Design decisions
The following alternatives were evaluated before committing to
AKS. AKS was chosen because the application already ran on
Kubernetes locally using kind, and demonstrates a
broader, more commercially transferable skill set.
Azure Container Apps
Scales to zero with no idle cost — no start/stop scheduler needed. Simpler to operate for pure container workloads.
Not chosen — requires rewriting Helm/ingress/PV manifests. AKS is more directly transferable to commercial Kubernetes environments.
k3s on a cheap VM
Lightweight Kubernetes on a €3–6/month VPS. Helm chart would work without modification since k3s is fully Kubernetes-compatible.
Not chosen — requires self-managing the VM, OS updates, and k3s upgrades. AKS as a managed service is the more appropriate choice for a cloud-native portfolio project.
Spot VMs
Up to 90% discount over standard pricing. Could reduce node cost to approximately €2–3/month.
Not chosen — evictions with 30 seconds notice make them unsuitable as the sole node in a single-node cluster. Acceptable risk in multi-node setups only.
Wildcard DNS + custom domain
Would resolve the nip.io limitations (URL stability on cluster recreate, Let's Encrypt rate limits, DNS-01 wildcard certs).
Valid improvement path — the primary reason it wasn't included is cost and domain registration overhead for a portfolio project. Straightforward to migrate to.