The Philosophical Developer — Chapter 14: Code Your Infrastructure

2026-07-09 · 5 min read

Code Your Infrastructure

TDD-Driven Infrastructure as Code

Last chapter we stood up the local cloud: Floci as our AWS emulator, a local registry, k3s in k3d. It works. But “works” is a state, not a guarantee.

Infrastructure as code turns states into contracts.

All code for this series lives in the local-cloud repo on GitHub.

Why OpenTofu

OpenTofu forked from Terraform when HashiCorp switched to BUSL. The code is the same. The philosophy diverged — OpenTofu stayed open source, community-governed, and committed to backward compatibility. For a project about sovereignty and local control, that alignment matters.

We’re using OpenTofu 1.12.1 with native test support — .tftest.hcl files that let us apply TDD to infrastructure the same way we do to application code.

The Problem With Infrastructure Code

Traditional IaC has a gap: you write configuration, then apply, then pray. The feedback loop is slow and destructive. Drift detection is after-the-fact. There’s no test suite for your infrastructure until something breaks in production.

OpenTofu’s native testing changes that. You can:

  • Write assertions before the resources exist
  • Use plan mode for unit tests (idempotent, no side effects)
  • Reserve apply for integration tests against a real target
  • Run the full suite locally against Floci before touching anything real

This is TDD applied to infrastructure. RED/GREEN/REFACTOR, but the red is a missing VPC and the green is a working cluster.

Module Design

Five modules, five concerns. Each tested independently before composition:

infrastructure/opentofu/
├── modules/
│   ├── storage/        # Versioned S3 bucket
│   ├── network/        # VPC + subnet
│   ├── iam/            # ECS task role + policy
│   ├── dynamodb/       # State table
│   └── ecs/            # Fargate cluster + service
└── environments/
    └── local/          # Composition + integration test

Storage Module

variable "bucket_name" {
  type    = string
  default = "local-cloud-artifacts"
}

resource "aws_s3_bucket" "this" {
  bucket = var.bucket_name
  tags   = { managed_by = "opentofu" }
}

resource "aws_s3_bucket_versioning" "this" {
  bucket = aws_s3_bucket.this.id
  versioning_configuration { status = "Enabled" }
}

Two tests: default bucket name and custom bucket name. Both pass.

Network Module

variable "vpc_cidr" {
  type    = string
  default = "10.0.0.0/16"
}

resource "aws_vpc" "this" {
  cidr_block           = var.vpc_cidr
  enable_dns_support   = true
  enable_dns_hostnames = true
}

resource "aws_subnet" "this" {
  vpc_id     = aws_vpc.this.id
  cidr_block = cidrsubnet(var.vpc_cidr, 8, 1)
  map_public_ip_on_launch = true
}

VPC with DNS enabled, public subnet with auto-assign. One test, one assertion per output.

IAM Module

resource "aws_iam_role" "this" {
  name = var.role_name
  assume_role_policy = jsonencode({
    Version   = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { Service = "ecs-tasks.amazonaws.com" }
      Action    = "sts:AssumeRole"
    }]
  })
}

Role scoped to ECS task execution with S3 read/write and CloudWatch logs. Policy attached at the module level — consumers just get a role ARN.

DynamoDB Module

The state table. Why? Because eventually this becomes the OpenTofu remote backend. For now it proves we can provision non-compute resources through the same pipeline.

resource "aws_dynamodb_table" "this" {
  name         = var.table_name
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "id"
  attribute    = [{ name = "id", type = "S" }]
}

ECS Module

resource "aws_ecs_task_definition" "this" {
  family                   = var.service_name
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = 256
  memory                   = 512
}

resource "aws_ecs_service" "this" {
  cluster         = aws_ecs_cluster.this.id
  task_definition = aws_ecs_task_definition.this.arn
  launch_type     = "FARGATE"
}

Fargate requires awsvpc networking and explicit subnet configuration. The module accepts a subnet ID from the network module — the dependency chain is explicit.

Composition

The local environment wires everything together:

module "network" { source = "../../modules/network" }
module "storage" { source = "../../modules/storage" }
module "iam"     { source = "../../modules/iam" }
module "dynamodb" { source = "../../modules/dynamodb" }
module "ecs"     { source = "../../modules/ecs"; subnet_id = module.network.subnet_id }

Five modules, one integration test. The test runs plan and asserts that every output is non-empty. If the graph is broken, the plan fails. If a dependency is missing, the assertion fails.

Test Results

=== modules/storage ===   2 passed
=== modules/network ===   1 passed
=== modules/iam ===       1 passed
=== modules/dynamodb ===  1 passed
=== modules/ecs ===       1 passed
=== environments/local== 1 passed
=== All tests passed ===  7 runs

Seven test runs. Zero failures. Against a real Floci instance, not a mock.

Plan Mode vs Apply

Unit tests use command = plan. This validates structure, types, and provider connectivity without provisioning. Floci resources persist across runs — apply in unit tests would cause EntityAlreadyExists on the second invocation.

This mirrors how we think about tests in application code: unit tests should be idempotent and side-effect free. Integration tests are where you actually deploy.

What This Buys Us

  1. Reproducibletofu plan shows exactly what changes. No drift.
  2. Traceable — Every resource is code. Every change is a commit.
  3. Testable — Assertions run before provisioning. Failures are cheap.
  4. Safe — Local-first execution. Floci catches errors before they hit real infrastructure.

The four pillars, applied to infrastructure.

Next

State management. Remote backends. Workspace isolation. The gap between “works locally” and “works in CI” is where real infrastructure engineering lives.

Chapter 15.


Also on LinkedIn.