Designing a Multi-Environment Django Platform on Kind with Pulumi, Components, and Policy as Code

Apr 23, 2026 min read

Why I Built This

I wanted a small infrastructure project that was concrete enough to run locally, but structured enough to show how I think about engineering.

For this project, I wanted something slightly realistic: a Django application running on Kubernetes, provisioned with Pulumi, exposed through Traefik, and able to support more than one environment inside the same cluster.

The result is a local platform built on Kind, Pulumi, Kubernetes, and Django. It runs a shared Traefik ingress controller, deploys isolated dev and stage environments into different namespaces, provisions PostgreSQL inside each environment, and exposes the application through host-based routing.

The source code is available on GitHub: rrbarrero/pulumi-django-k8s.

That part is already useful. The more interesting part, though, is how the infrastructure code evolved from a single script into a more deliberate system.

The Real Goal Was Not the Cluster

The technical target was straightforward: deploy Django to a local Kubernetes cluster.

The design target was more interesting:

  • support more than one environment in the same cluster
  • avoid conflicts around shared resources like Traefik
  • keep configuration explicit and validated
  • make the infrastructure code testable
  • encode some non-trivial operational rules as guardrails

That combination pushes the project beyond a basic IaC example. It becomes a small exercise in system design for infrastructure code.

One Cluster, Two Environments, Clear Boundaries

The project uses two Pulumi stacks, dev and stage, against the same Kind cluster.

That sounds simple until shared resources show up.

Both environments need their own namespace, Django deployment, PostgreSQL deployment, service, ingress, and secrets. But they should not both try to install the same cluster-wide Traefik release. To handle that, I made the ownership explicit:

  • the dev stack manages the shared Traefik release
  • the stage stack reuses it with install_traefik: false

This is a small decision, but it reflects a pattern that matters in larger systems too: separate shared infrastructure from environment-specific infrastructure, and make ownership obvious.

The end result is that both environments run side by side in the same cluster while staying isolated by namespace and host:

  • django.local
  • django-stage.local

That gives me a local setup that is still small enough to understand end to end, but rich enough to demonstrate environment isolation and shared infrastructure concerns.

Refactoring the Pulumi Program Into Something Maintainable

The initial version of the Pulumi program lived in a single __main__.py file. That was perfectly fine for the first pass, but once stage, tests, and policy checks entered the picture, it was time to give the codebase more shape.

I refactored the infrastructure code into a few focused modules:

  • settings.py for typed configuration loading and validation
  • common.py for shared constants and helper functions
  • shared_resources.py for cluster-level resources such as Traefik
  • environment_resources.py for namespaced application resources
  • components.py for reusable Pulumi ComponentResource abstractions
  • policies.py for infrastructure guardrails
  • stack.py as a thin orchestration layer

This separation is small, but meaningful. It makes it much easier to reason about which parts of the system are shared, which are environment-specific, and where cross-cutting concerns belong.

Typed Settings Matter More Than They First Appear

One of the most useful changes in the project was introducing a typed settings layer with Pydantic.

Pulumi already gives you configuration primitives like require and require_secret, but a typed settings model adds a cleaner contract for the stack. It lets the program validate assumptions early and fail with clearer errors.

In practice, the settings layer now validates things like:

  • required configuration values must not be empty
  • replicas must be at least 1
  • ingress values must be hostnames rather than full URLs
  • stack configuration is loaded into one explicit model instead of being spread around the file

This is not a huge amount of code, but it improves the design noticeably. It also makes the project read more like a deliberate system than a script with a few environment variables.

Using Pulumi Components Instead of Repeating Resource Blocks

Once the stack grew beyond a handful of resources, I moved the Django application and PostgreSQL setup into Pulumi ComponentResource classes.

That change mattered because it gave the infrastructure code proper units of composition.

The PostgresComponent owns:

  • the persistent volume claim
  • the PostgreSQL deployment
  • the PostgreSQL service

The DjangoAppComponent owns:

  • the service account
  • the RBAC role and binding
  • the Django deployment
  • the Django service

Modeling those pieces as components made the stack orchestration thinner and easier to read. It also better matches the way I think about the system: not as a flat list of YAML-like objects, but as a set of higher-level infrastructure building blocks.

That is one of the reasons I like Pulumi for projects like this. It allows the infrastructure code to evolve toward normal software design techniques when the problem starts to justify them.

Policy as Code as Lightweight Governance

I also wanted the project to demonstrate that IaC can carry operational rules, not just resource declarations.

For that, I added a small Policy as Code layer inside the Pulumi project. The idea was not to build a giant compliance framework. It was to encode a few rules that are genuinely relevant to this system.

The current guardrails include:

  • container images must not use :latest
  • the django-stage environment must run at least two replicas
  • only django-dev may manage the shared Traefik release
  • services must not use LoadBalancer
  • the application ingress must use ingressClassName: traefik
  • the Django RBAC role must remain limited to pods with get, list, and watch

What I like about this is that the project now expresses some intent, not just implementation.

The code does not merely say “create these resources.” It also says “these are the rules under which this infrastructure is acceptable.”

That is a much better story for infrastructure code.

Testing the Infrastructure Code

I did not want the project to rely only on pulumi preview as a validation mechanism.

pulumi preview is useful, but it is best treated as an integration-level check. I also wanted fast feedback on smaller units of behavior. So I added a pytest suite covering:

  • shared helpers
  • stack orchestration logic
  • policy rules
  • the install_traefik branching behavior

There is also a small Makefile flow that runs:

  • make pulumi-test
  • make pulumi-check

The second target performs a pulumi preview and then runs the test suite, which gives a nice lightweight quality gate for the project.

A GitHub Actions workflow runs the Pulumi-related tests in CI as well. That is not complicated infrastructure, but it does reinforce an important point: even a small portfolio project benefits from an explicit feedback loop.

Why This Project Works Well as a Portfolio Piece

What I like about this project is that it shows several layers of engineering judgment in a compact space.

It demonstrates:

  • local Kubernetes deployment with Kind
  • infrastructure definition with Pulumi
  • multi-environment design in a shared cluster
  • explicit ownership of shared versus environment-specific resources
  • typed configuration validation
  • reusable Pulumi components
  • tests and CI around infrastructure code
  • lightweight Policy as Code guardrails

None of those ideas are individually exotic. The value comes from how they fit together.

That is usually what I try to optimize for in portfolio work. I am less interested in stacking technologies for their own sake than in showing how a system becomes cleaner, safer, and easier to evolve as complexity increases.

Closing Thoughts

This started as a small local Kubernetes deployment. It became more interesting once I treated the infrastructure code as software that deserved design decisions, tests, and guardrails.

That shift in mindset is really the main point of the project.

Kind provides the local cluster. Django provides the application. Kubernetes provides the target runtime. But the real value of the project is in the structure around them: clear stack ownership, typed settings, reusable components, policy checks, and a workflow that is easy to validate repeatedly.

That combination is what makes the project useful to me and worth showing in a portfolio. It is not trying to be a production platform. It is trying to be a small, disciplined example of how infrastructure code can scale from “it works” to “it is designed.”

This article was written with the help of artificial intelligence, as you might expect.