Infrastructure Should Be Inferred, Not Written

January 2025 – Ivan Cernja

Talk to developers building cloud applications today. For every feature they ship, there's a corresponding dance with Terraform configs, Kubernetes manifests, and CI/CD pipelines. The promise of Infrastructure as Code was simple: make our systems reproducible, scalable, maintainable.

We traded physical cables for YAML files, but the burden remained the same.

Infrastructure as Code forces a translation layer between your application's needs and the systems that provision them. You write TypeScript that needs a database, then context-switch to HCL to describe that database to Terraform, which translates it to CloudFormation, which finally talks to AWS. Every layer is a translation. Every translation loses intent.

Something changed in the past two years. LLMs got good enough to write real application code, and they exposed a truth we've been avoiding: the best way to express infrastructure requirements is through application code itself.

Upside down abstractions

When you build an API endpoint that needs to store user data, you think: I need to save this user object. It should persist across restarts. I need to query by email. Multiple instances need access.

This is application logic. How developers think.

Traditional IaC forces you to think differently:

resource "aws_db_instance" "users" {
  allocated_storage    = 20
  storage_type        = "gp2"
  engine              = "postgres"
  engine_version      = "15.0"
  instance_class      = "db.t3.micro"
  db_name             = "users"
  username            = var.db_username
  password            = var.db_password
  parameter_group_name = "default.postgres15"
  skip_final_snapshot = true
  
  vpc_security_group_ids = [aws_security_group.db.id]
  db_subnet_group_name   = aws_db_subnet_group.main.name
}

resource "aws_security_group" "db" {
  name   = "db-security-group"
  vpc_id = aws_vpc.main.id

  ingress {
    from_port   = 5432
    to_port     = 5432
    protocol    = "tcp"
    cidr_blocks = [aws_subnet.private.cidr_block]
  }
}

# ... another 50 lines of VPC, subnet, and IAM configuration

The abstraction inverted. Instead of expressing what you need (store users), you're expressing how the cloud provider should configure its primitives. The problem space became the solution space.

Platform engineers think in these primitives. That's their job. But most developers building applications just want to ship features users care about. This is cognitive overhead that doesn't advance that goal.

Code that knows what it needs

Modern LLMs can generate and reason about code like this:

// Express what you need, not how to provision it
const db = new Database("users", {
  migrations: "./migrations",
});

interface User {
  id: number;
  email: string;
  name: string;
}

export const createUser = api(
  { method: "POST", path: "/users" },
  async (user: Omit<User, "id">): Promise<User> => {
    const result = await db.queryRow`
      INSERT INTO users (email, name)
      VALUES (${user.email}, ${user.name})
      RETURNING id, email, name
    `;
    return result!;
  }
);

Just clear intent: "I need a SQL database called 'users'. Here's how I'm using it."

The LLM reads this and understands it needs a PostgreSQL instance, accessible from the API service, with migration support and a defined schema. It infers the endpoint is public. It knows this needs HTTPS in production. The infrastructure knowledge is implicit in the application code.

Application code is a higher-fidelity representation of infrastructure needs than infrastructure code.

Three technical shifts

Five years ago this didn't work. Now it does. Three things converged:

LLMs maintain context across languages and domains

Yeoman and Cookiecutter were template engines. They scaffolded code but couldn't reason about it. LLMs read TypeScript, understand you need durable storage, and know that means a database with backups, replicas, and connection pooling.

They understand the semantic link between await db.query() in your application and a managed PostgreSQL instance in your cloud account. Traditional tooling couldn't do this cross-domain reasoning.

Infrastructure can be generated, not written

Terraform showed us infrastructure could be code. We learned the wrong lesson. We thought we should write that code. Infrastructure should be generated from application code, not hand-written in parallel. When an LLM sees you import a SQL database, it generates the RDS configuration, security groups, and IAM roles. When it sees you publish to a topic, it provisions pub/sub infrastructure.

Type systems are infrastructure specifications

Type systems aren't just for catching bugs. They're semantic markers LLMs use to infer infrastructure needs:

interface EmailConfig {
  smtpHost: string;
  smtpPort: number;
  apiKey: Secret<string>;  // LLM knows: this needs secrets management
}

const sendEmail = api(
  { method: "POST", path: "/webhooks/stripe" },  // LLM knows: this needs to be publicly accessible
  async (req, res) => {
    // Webhook handler
  }
);

const processQueue = new Queue<OrderEvent>("orders", {
  handler: async (event) => {
    // LLM knows: this needs async compute + queue infrastructure
  }
});

Types are infrastructure specifications in disguise. Secret<T> means provision secrets manager. Queue<T> means provision message queue. SQLDatabase means provision relational database. LLMs read these type signatures and generate the corresponding cloud resources.

One file, one concern

With Terraform, adding a cron job means splitting your work across two files:

// application.ts
export async function sendWeeklyDigest() {
  const users = await db.query`SELECT * FROM users WHERE subscribed = true`;
  for (const user of users) {
    await sendEmail(user);
  }
}

Then infrastructure in another:

# infrastructure.tf
resource "aws_cloudwatch_event_rule" "weekly_digest" {
  name                = "weekly-digest"
  description         = "Trigger weekly digest"
  schedule_expression = "cron(0 9 ? * MON *)"
}

resource "aws_cloudwatch_event_target" "lambda" {
  rule      = aws_cloudwatch_event_rule.weekly_digest.name
  target_id = "lambda"
  arn       = aws_lambda_function.digest.arn
}

resource "aws_lambda_permission" "allow_eventbridge" {
  statement_id  = "AllowExecutionFromEventBridge"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.digest.function_name
  principal     = "events.amazonaws.com"
  source_arn    = aws_cloudwatch_event_rule.weekly_digest.arn
}

# ... plus lambda function definition, IAM roles, etc.

With application code that expresses infrastructure needs:

const _ = new CronJob("weekly-digest", {
  title: "Send weekly digest",
  schedule: "0 9 * * MON",
  endpoint: sendWeeklyDigest,
});

export const sendWeeklyDigest = api({}, async () => {
  const users = await db.query`SELECT * FROM users WHERE subscribed = true`;
  for (const user of users) {
    await sendEmail(user);
  }
});

An LLM generating the second example understands the infrastructure (EventBridge, Lambda permissions, IAM roles) is implicit. Modify the schedule or add error handling? One place. Infrastructure updates automatically.

The emerging pattern

Multiple approaches are testing this territory. Wing created a purpose-built language for cloud applications—elegant, but it's a DSL with less training data for LLMs to reference. Pulumi lets you write infrastructure in real programming languages, but you're still describing AWS primitives in TypeScript instead of HCL. SST takes a hybrid approach for AWS. Ampt focuses on JavaScript-first development with automatic provisioning.

The pattern that works best for LLMs: plain TypeScript that naturally expresses infrastructure needs. Not infrastructure code. Not DSLs. Just application code that happens to be infrastructure-aware:

// TypeScript. Also infrastructure.
const db = new Database("users", {
  migrations: "./migrations",
});

export const getUser = api(
  { auth: true, method: "GET", path: "/users/:id" },
  async ({ id }: { id: string }): Promise<User> => {
    return await db.queryRow`SELECT * FROM users WHERE id = ${id}`;
  }
);

Snippet from Encore, a TypeScript backend framework that generates infrastructure from application code (I work there)

When an LLM writes this, it's writing TypeScript. Millions of examples. Zero learning curve. Same for developers—existing tools, existing knowledge, existing muscle memory.

When frameworks parse your code to generate infrastructure, they can generate other things too. A complete application model emerges—every endpoint, database, pub/sub topic, service boundary. Distributed tracing becomes automatic. Type-safe API clients generate themselves. Local development matches production.

Some frameworks take this further. At Encore (where I work), we build a complete application model from your code. When you write new Database("users"), we know which endpoints need database access, what tables exist, what queries are running. This model is queryable—by you, by your team, by LLMs through MCP servers.

The LLM doesn't just generate code. It queries your running application: "What's the p99 latency on the shorten endpoint?" "Show me failed requests in the last hour." It becomes a pair programmer that can read, write, and observe your entire system.

The right abstraction level is the application's perspective, not cloud provider primitives.

What this enables

Right now you can open Cursor, describe "Build me a URL shortener with rate limiting and analytics," and watch it generate the API code, database schema, caching layer, and background jobs. Deploy that code. It provisions the PostgreSQL database, Redis cache, and serverless functions. The LLM understands "rate limiting" means fast read access (cache), "analytics" means time-series data, "URL shortener" means a database with unique constraints and high read throughput.

It reasons about the application, not the infrastructure.

Developers are building production applications this way today. Companies processing millions of requests per day with frameworks that let you write TypeScript that runs locally and deploys to AWS or GCP with git push. LLMs participate. They read your application code, understand what infrastructure it needs, and generate it.

Infrastructure as Code: you write code describing infrastructure. Infrastructure from Code: infrastructure is derived from application code. One is a mapping problem, the other is inference. LLMs are better at inference.

Conclusion

We spent two decades making infrastructure more like code. We should make code more infrastructure-aware instead.

LLMs aren't better at writing Terraform than humans. They're better at writing application code that naturally expresses infrastructure needs. They maintain a single mental model instead of juggling multiple ones. They reason about what you're trying to accomplish rather than how to configure primitives.

The future of infrastructure isn't more YAML, more abstraction layers, more translation steps. It's application code expressive enough that infrastructure can be inferred, generated, and maintained automatically. We finally have machines that can read our application code and understand what it needs to run—not what we tell them through configuration files, but what it actually needs based on what it does.

Your pixels aren't getting better because you're spending time on Terraform configs. The machines can handle that part now.