Free lesson · Forward Deployed GenAI Engineering
Provision isolated K8s demo environments with TTL teardown
You build a DemoEnvironmentProvisioner that creates K8s namespaces with resource quotas, deploys pre-configured services, seeds sample data, and tears down on TTL expiry.
Course: AI Solution Delivery · Chapter 7 · Stakeholder Communication & Demo Engineering
Free to read — no subscription required.
Introduction
Engineers often spend hours manually standing up demo environments before stakeholder calls, only to leave lingering namespaces that consume cluster resources and risk exposing client data across engagements. Automated provisioning solves this by treating each demo as a short-lived, resource-bounded artifact rather than a hand-configured one-off. By the end of this lesson, you'll be able to implement a DemoEnvironmentProvisioner that spins up isolated Kubernetes namespaces on demand, enforces CPU, memory, and storage quotas, and schedules automatic teardown — all driven from a single configuration object.
Key Terminology
DemoEnvironmentConfig— the Pydantic model that serves as the single configuration object for a demo, encodingttl_hours,cpu_limit,memory_limit,storage_limit,services,seed_dataset, andnetwork_isolated; every step insideprovision()reads from this object rather than from scattered imperative arguments.- Namespace isolation — the Kubernetes namespace
demo-{environment_id}created per engagement byDemoEnvironmentProvisioner, providing a logical boundary that scopes all provisioned services, quotas, and network policies to exactly one stakeholder demo. - Resource quota — a
V1ResourceQuotaobject applied to the demo namespace that enforces hard ceilings oncpu_limit,memory_limit,storage_limit, pod count (20), and service count (10), preventing any single demo from consuming shared cluster capacity. - TTL (time-to-live) — the
ttl_hoursfield onDemoEnvironmentConfig(constrained to 1–72 hours via Pydantic'sge/levalidators) that drives_schedule_teardown, converting each demo from a persistent hand-configured environment into a self-expiring artifact. - Network isolation policy — the optional
NetworkingV1Api-managed policy applied whencfg.network_isolatedisTrue, restricting cross-namespace traffic so client data cannot bleed between concurrent demo environments running on the same cluster. - In-cluster config — the
config.load_incluster_config()call inDemoEnvironmentProvisioner.__init__that bootstraps the Kubernetes Python SDK using the pod's own service-account token, removing any dependency on external kubeconfig files when the provisioner runs inside the cluster.
Concepts
Demos as Short-Lived, Resource-Bounded Artifacts
The core mental model shift this lesson teaches is treating a client demo not as a hand-configured server environment but as a declared, time-bounded artifact — something you spin up from a specification and let expire automatically. The manual alternative produces two failure modes: lingering namespaces that waste cluster resources after the call ends, and shared environments where one engagement's seed data sits next to another's. Both problems trace back to the absence of a lifecycle contract.
DemoEnvironmentConfig encodes that contract up front. Before a single Kubernetes API call is made, the provisioner knows exactly how long the environment should live (ttl_hours), how many cluster resources it may consume (cpu_limit, memory_limit, storage_limit), and whether traffic between namespaces should be blocked (network_isolated). Every subsequent step in provision() is just mechanical translation of those fields into Kubernetes objects (see Code Walkthrough).
Namespace-per-Engagement as the Isolation Boundary
A Kubernetes namespace is the natural unit of demo isolation because resource quotas, network policies, and RBAC rules all scope to a namespace. By naming each namespace demo-{environment_id}, the provisioner creates an unambiguous handle: any cleanup job, monitoring alert, or audit query can target exactly the right environment without risking collision with production workloads or adjacent demos.
Isolation alone is not enough — a namespace without quotas can still consume unbounded CPU and memory. The _apply_resource_quota step pairs namespace isolation with a V1ResourceQuota that caps not just compute (cpu_limit, memory_limit, storage_limit) but also object counts (20 pods, 10 services). The combination ensures that a misbehaving demo service, or one left running past its expected call slot, cannot destabilize adjacent workloads.
The Provisioning Lifecycle and How to Verify It
provision() is a sequential pipeline: create namespace → apply quota → optionally apply network policy → deploy services → seed data → schedule teardown. The ordering matters: the quota must be in place before services are deployed so that any service that would exceed the ceiling is rejected at admission rather than silently over-provisioning.
The returned DemoEnvironment dataclass provides a built-in verification surface. Checking that status == "ready" and that expires_at equals datetime.utcnow() + timedelta(hours=cfg.ttl_hours) gives a single assertion that implicitly confirms namespace creation, quota application, and teardown scheduling all completed without error. If either field is wrong, the pipeline short-circuited before those steps finished — making the return value a lightweight acceptance test, not just metadata.
Code Walkthrough
Building on the DemoEnvironmentConfig model and its TTL and resource-quota fields from the Concepts section, the code below wires those settings into a live Kubernetes provisioner.
DemoEnvironmentProvisioner uses the Kubernetes Python SDK's CoreV1Api, AppsV1Api, and NetworkingV1Api to translate a DemoEnvironmentConfig into a running namespace. Each call to provision() creates the namespace with identifying labels, applies a V1ResourceQuota scoped to the config's CPU, memory, and storage ceilings, optionally applies a network isolation policy, deploys the requested services, seeds demo data, and registers a teardown timer. The namespace follows the convention demo-{environment_id}, making it straightforward to locate and clean up after the engagement ends. Note that the parameter is named cfg rather than config to avoid shadowing the kubernetes.config module imported at the top of the file.
Code snippetpython
1from kubernetes import client, config 2from pydantic import BaseModel, Field 3from dataclasses import dataclass 4from typing import List 5from datetime import datetime, timedelta 6 7class DemoEnvironmentConfig(BaseModel): 8 """Configuration for an isolated demo environment.""" 9 environment_id: str 10 engagement_id: str 11 demo_name: str 12 ttl_hours: int = Field(default=4, ge=1, le=72) 13 cpu_limit: str = "4" 14 memory_limit: str = "8Gi" 15 storage_limit: str = "20Gi" 16 services: List[str] 17 seed_dataset: str 18 network_isolated: bool = True 19 20@dataclass 21class DemoEnvironment: 22 """Represents a provisioned demo environment.""" 23 environment_id: str 24 namespace: str 25 status: str 26 services: List[str] 27 created_at: datetime 28 expires_at: datetime 29 access_url: str 30 31class DemoEnvironmentProvisioner: 32 """Provisions isolated Kubernetes namespaces for client demos.""" 33 34 def __init__(self): 35 config.load_incluster_config() 36 self.core_v1 = client.CoreV1Api() 37 self.apps_v1 = client.AppsV1Api() 38 self.networking_v1 = client.NetworkingV1Api() 39 40 async def provision(self, cfg: DemoEnvironmentConfig) -> DemoEnvironment: 41 """Spin up a complete, resource-bounded demo environment.""" 42 namespace = f"demo-{cfg.environment_id}" 43 44 await self._create_namespace(namespace, cfg) 45 await self._apply_resource_quota(namespace, cfg) 46 47 if cfg.network_isolated: 48 await self._apply_network_policy(namespace) 49 50 deployed = await self._deploy_services(namespace, cfg.services) 51 await self._seed_data(namespace, cfg.seed_dataset) 52 53 teardown_at = datetime.utcnow() + timedelta(hours=cfg.ttl_hours) 54 await self._schedule_teardown(namespace, teardown_at) 55 56 return DemoEnvironment( 57 environment_id=cfg.environment_id, 58 namespace=namespace, 59 status="ready", 60 services=deployed, 61 created_at=datetime.utcnow(), 62 expires_at=teardown_at, 63 access_url=f"https://demo-{cfg.environment_id}.internal", 64 )
_apply_resource_quota (called inside provision) constructs a V1ResourceQuota manifest from cfg.cpu_limit, cfg.memory_limit, and cfg.storage_limit, and additionally caps the namespace at 20 pods and 10 services. Together, namespace isolation and quota enforcement ensure that no single stakeholder demo can destabilize adjacent workloads or persist past its TTL window.
Confirm that after calling provisioner.provision(cfg) the returned DemoEnvironment has status == "ready" and an expires_at value equal to datetime.utcnow() + timedelta(hours=cfg.ttl_hours) — if both fields are set correctly, namespace creation, quota application, and teardown scheduling all completed successfully.
Do's and Don'ts
Having walked through the material above, the following Do's and Don'ts distill it into practice.
Do's
- ✓Do name the namespace
demo-{environment_id}— this convention makes it trivial to locate, audit, and forcibly clean up any engagement's namespace without a separate registry; a flat or engagement-only name collides across concurrent demos and defeats automated teardown. - ✓Do name the
provision()parametercfginstead ofconfig— thekubernetes.configmodule is imported at the top of the file, and shadowing it with a localconfigvariable causes silent import resolution failures that only surface at runtime whenconfig.load_incluster_config()is called inside__init__. - ✓Do verify both
status == "ready"andexpires_at == datetime.utcnow() + timedelta(hours=cfg.ttl_hours)on the returnedDemoEnvironment— these two fields are the observable proof that namespace creation,V1ResourceQuotaapplication, and teardown scheduling all completed in sequence; a missing or incorrectexpires_atmeans the TTL timer was never registered and the namespace will persist indefinitely.
Don'ts
- ✗Don't omit the
V1ResourceQuotapod and service caps — setting only CPU, memory, and storage limits on the namespace still allows unbounded pod and service counts; a runaway demo can exhaust cluster scheduling capacity and destabilize workloads in adjacent namespaces even within its quota ceiling. - ✗Don't skip
_apply_network_policywhencfg.network_isolatedisTrue— without theNetworkingV1Apiisolation policy, a demo namespace has unrestricted east-west access to other namespaces, meaning client data seeded via_seed_datais reachable from every other engagement's pods sharing the cluster. - ✗Don't pass
ttl_hoursoutside theField(ge=1, le=72)bounds —DemoEnvironmentConfigenforces a 1–72 hour window via Pydantic validation; bypassing this (e.g., by constructing the dataclass directly or coercing the field) allows zero-TTL environments that teardown immediately before services are reachable, or multi-day namespaces that accumulate client data across engagements.
This lesson is free to read. Its 3 hands-on labs — real code, in a cloud IDE — are part of the Forward Deployed GenAI Engineering subscription.
From · cancel anytime · Already a subscriber? Sign in →
More free lessons in AI Solution Delivery
- Ch 6Generate K8s manifests from customer-parameterized Jinja2 templates
- Ch 6Manage K8s secrets with rotation and init-container injection
- Ch 6Enforce service isolation with K8s NetworkPolicy
- Ch 6Log compliance events as OTEL traces with structured attributes
- Ch 7Provision isolated K8s demo environments with TTL teardownYou are here
- Ch 8Detect scope drift with embedding similarity classification
- Ch 9Deploy with blue-green Helm charts and atomic service switching