DEV Community

TuviDev
TuviDev

Posted on

How I built a DSL for infrastructure in 10 days with AI

Lessons from building infra-lang — an IaC DSL with 5 compilation targets, an LSP, and live K8s E2E tests. What worked, what surprised me, and what I'd do differently.

The idea

Every team I've worked with maintains the same application in at least three formats: Kubernetes manifests for production, a docker-compose.yml for local dev, and a GitHub Actions workflow for CI. Every change touches all of them. They drift. And bugs in the K8s YAML only surface when kubectl apply rejects them — not when you write them.

I wanted one file that compiles to all of them. So I built Infra Lang.

What it looks like

service api {
    image: "myapp/api:v1.0.0"
    replicas: 3
    port 8080
    health http("/health")
    resources {
        requests { cpu: 200m, memory: 256Mi }
        limits   { cpu: 1000m, memory: 512Mi }
    }
}
Enter fullscreen mode Exit fullscreen mode

That block compiles to a Kubernetes Deployment + Service, a Docker Compose service, a Helm chart, Terraform HCL, or a GitHub Actions workflow. One source of truth, five targets.

Why a DSL, not YAML templates
Helm, Kustomize, and string interpolation in CI all push the same problem down the road: you still write YAML, just with placeholders. Validation still happens late.

A real DSL gives you three things:

A parser that catches errors immediately. Infra Lang uses a hand-written LALR(1) grammar with {} blocks. A typo is caught at parse time with a location and a helpful message — not after you deploy.

Compile-time linting. 10 security rules catch hardcoded secrets, mutable image tags, and privileged containers. 13 reliability rules catch thundering-herd replica counts, databases without backups, and single-replica Kafka. Error-severity findings block compilation entirely.

One mental model. You think in services, databases, queues, and pipelines — not in individual YAML documents for each platform.

The AI-assisted process
I want to be transparent: this project was built with extensive AI assistance — about 40 sessions over 10 days using Claude as a coding partner.

Here's how the process actually worked:

I owned every architecture decision. The DSL syntax, which backends to support, what the LSP should do, how to structure tests. AI doesn't make those calls.

AI wrote implementation and tests. I reviewed, ran, and iterated. Every session had a specific scope: "add semantic tokens to LSP", "fix Compose secret mounting", not "build me a DSL".

I ran everything locally. Docker Desktop, kind for Kubernetes E2E, real helm lint, real docker compose up. AI can't do that.

27 real bugs were found through the process. Secret base64 encoding that passed unit tests but failed kubectl apply. Service port naming that Kubernetes rejected. Windows URI path conversion that crashed the LSP. These are things you only find by actually running the code against real targets.

Is there risk in AI-assisted development? Yes — I don't know 100% of the codebase intimately. But the code runs, tests pass on 3 operating systems and 3 Python versions, and the output is validated against real Kubernetes clusters.

What surprised me
Coverage lies
93% line coverage sounded impressive. Then I ran mutation testing — automatically introducing bugs into the code and checking whether tests catch them.

Results: some modules had 34% mutation score despite high line coverage. The tests executed the code but didn't verify the output. A test that says assert result is not None gives you coverage but catches nothing.

After two sessions of targeted fixes, critical modules reached 82-100% mutation score. The lesson: line coverage tells you what code ran, not whether your tests actually work.

Cross-platform is harder than you think
My first CI run on Windows failed because .read_text() without encoding="utf-8" uses the system default (cp1252 on Windows). Every file read in the entire codebase needed explicit UTF-8.

Docker daemon detection needed special handling too — Windows CI runners have the Docker CLI installed but no running daemon. docker version succeeds but docker compose up fails. The fix: check docker info exit code, not just whether the binary exists.

The LSP was the most rewarding part
Building a language server taught me more about developer experience than anything else in this project. Each feature had its own challenge:

Cross-file rename needed word-boundary-aware regex — renaming db shouldn't touch main-db
Semantic tokens required a line-based tokenizer that doesn't crash on malformed input
Signature help needed brace-balance counting to detect which block the cursor is inside
Workspace indexing needed to scan files on disk without blocking the main LSP thread
The result: completion, hover, diagnostics, go-to-definition, find-references, rename, semantic tokens, signature help, document highlight, and folding — all working across every .infra file in the project.

Live E2E tests catch what unit tests miss
Three of the most serious bugs were invisible to unit tests:

Kubernetes Secrets with invalid base64 — unit tests checked structure, kubectl apply rejected the values
Multi-port Services without port names — valid YAML, invalid Kubernetes API
Compose secrets declared but never mounted to services — file existed, container couldn't access it
Now there's an opt-in test suite that actually spins up a kind cluster, runs kubectl apply, starts docker compose up, and runs helm lint --strict. These tests found real bugs that 1800+ unit tests missed.

What I'd do differently
Start with PyPI from day 1. I spent the first week telling people to pip install git+https://github.com/.... The friction was enormous. Once I published to PyPI, installation became pip install infra-lang — 5 seconds instead of a paragraph of instructions.

Write blog posts before launching. SEO takes weeks to build. Dev.to articles, technical deep dives, comparison posts — all of these should exist before you post on HN, not after.

Build community before features. I built 5 backends, a full LSP, Helm chart generation, and live E2E tests. Then I posted on Hacker News and got 13 upvotes. Features don't create adoption. Reach does.

The numbers
1877 tests, 93% line coverage, mutation testing on all critical modules
5 backends: Kubernetes, Helm, Docker Compose, Terraform, GitHub Actions
LSP with 10+ features including completion, hover, rename, semantic tokens, and signature help
CI on Linux, macOS, Windows across Python 3.11, 3.12, 3.13
Live E2E: real kubectl apply on kind, real docker compose up, real helm lint
Try it
Bash

pip install infra-lang
infra --help
Or with the VS Code language server:

Bash

pip install 'infra-lang[lsp]'
Links:

If you maintain infrastructure in multiple formats, I'd love your feedback — especially on language design and which compilation targets matter most.

Top comments (1)

Collapse
 
reidmarlow profile image
Reid Marlow

The compile-before-apply part is the real win, not the single source. Bugs showing up at kubectl apply instead of at write time is exactly the drift problem, and a compiler moves that rejection earlier. The boundary I'd probe is hand-edits: once someone tweaks a generated manifest directly, you're back to three drifting files with no signal. Did you make the generated output read-only, or do you plan to?