Completed Solo project Azure · Poland Central

Ephemeral PR Preview Environment Platform (AKS-based)

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.

Azure DevOpsAzure DevOps KubernetesAKS HelmHelm DockerDocker Node.jsNode.js Vue.jsVue Bicep cert-manager ingress-nginx

Overview & 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.

Architecture diagram illustrating the system's components and interactions

Example workflow

Prerequisite: A running AKS cluster provisioned via the one-time bootstrap pipeline (Bicep + Azure DevOps). Safe to rerun at any point — idempotent by design.
Bootstrap pipeline screenshot
1

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.

Step 1 screenshot
2

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.

Step 2 screenshot
3

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.

Step 3 screenshot
4

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.

Step 4 screenshot
5

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.

Step 5 screenshot Step 5 screenshot

Technical improvements

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 impact

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.

Cost estimation

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
The 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.

Limitations & key notes

This is a development environment designed for PR preview and demonstration purposes. The following are deliberate simplifications that would differ in a commercial project.

These are intentional tradeoffs for a portfolio project, not oversights. Each one is documented with the reasoning behind it.

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.

Alternatives considered

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.