pickuma.
Infrastructure

Infrastructure as Code for Solo Founders

You do not need a Terraform monorepo, a dedicated infrastructure engineer, or a complex CI pipeline to get the benefits of infrastructure as code. A single main.tf file, a state backend, and a GitHub Actions workflow that runs on push is enough.

7 min read

Infrastructure as code is sold as an enterprise practice: Terraform modules, remote state locking in S3, Atlantis for automated plan-and-apply, a dedicated platform team that reviews every terraform plan output in a pull request. That version exists because it solves problems that emerge when 40 engineers touch the same cloud account. You are not that team, and you do not need that version.

For a solo founder running a few services on a cloud provider, IaC solves exactly two problems: it replaces click-ops with version-controlled configuration, and it makes your infrastructure reproducible when you need to recreate it — after an incident, for a staging environment, or when you switch cloud providers. Everything else — the module registries, the policy-as-code frameworks, the multi-account architectures — is overhead you can postpone until you have a second engineer.

Why you need IaC even when you are the only developer

The argument against IaC for solo founders is persuasive: you set up the cloud resources once, they run for months without changes, and writing Terraform for a setup that takes 20 minutes in the console feels like overhead. The argument is correct until it breaks.

Three things happen to solo founders that IaC prevents:

You cannot remember how you configured the firewall rule. Six months after launch, you need to open a new port for a microservice. The security group was configured in the AWS console during a late-night session. You do not remember which rule allows which CIDR block, and you are afraid to touch it because the site is up and the rule is working. A Terraform file with aws_security_group_rule documents every open port, protocol, and source range. You can read it, understand it, and modify it without fear.

Your cloud provider has an outage that destroys your VPS. The instance is unrecoverable. The provider’s support says to launch a new instance. You need to reproduce: the OS image, the installed packages, the Nginx configuration, the SSL certificates, the DNS records, the database connection strings. If you built the original instance by hand, you are rebuilding from memory. If you built it with Terraform or Ansible, you run terraform apply and the infrastructure comes back in minutes.

You want to create a staging environment that matches production. Without IaC, you open the console, peer at the production setup, and manually recreate it in the staging account — missing the custom kernel parameter, the non-default Postgres extension, and the IAM role that took an hour to debug. The staging environment is not a copy of production. It is a rough approximation that will not catch the production bug you built it to find.

Picking your tool

Four tools dominate the IaC landscape in 2026, and the choice for a solo founder is simpler than comparison matrices suggest.

Terraform is the default. It supports every major cloud provider, has the largest community, and the terraform plan output tells you exactly what will change before you apply it. The downsides: HashiCorp’s license change to BSL in 2023 means commercial use has strings attached, and the HCL configuration language has a learning curve that peaks at “how do I conditionally include a block based on environment.”

OpenTofu is a fork of Terraform from before the BSL change, maintained under the Linux Foundation. It is drop-in compatible with existing Terraform configurations — same HCL syntax, same provider ecosystem, same plan-and-apply workflow. If the Terraform license concerns you and you do not need Terraform Cloud features, OpenTofu removes the licensing variable entirely.

Pulumi lets you write infrastructure in TypeScript, Python, or Go instead of HCL. For a solo founder who spends all day in TypeScript, writing new aws.s3.Bucket("assets") is more natural than writing an HCL resource block. The tradeoff: Pulumi’s state management is more opinionated (it defaults to Pulumi Cloud for state storage, though you can configure S3), and the community is smaller, which means fewer third-party module examples to copy from.

Ansible is not IaC in the Terraform sense — it is configuration management. You write YAML playbooks that describe the state of a server (packages installed, services running, files present) and Ansible applies them over SSH. For a solo founder running everything on a single VPS, Ansible plus a shell script is often simpler than Terraform plus a cloud provider abstraction. The workflow: provision a VM manually once, write an Ansible playbook that installs everything you need, and from then on, new VMs are provisioned by running the playbook against a fresh instance.

What a minimal Terraform setup looks like

A solo-founder Terraform project does not need modules, workspaces, or remote backends with state locking. It needs a main.tf file, a state file stored somewhere durable, and a terraform apply command you can run from your laptop.

Here is a real example that provisions a single application on Hetzner Cloud: one VPS, a firewall, a DNS record, and an SSH key.

terraform {
  required_providers {
    hcloud   = { source = "hetznercloud/hcloud" }
    cloudflare = { source = "cloudflare/cloudflare" }
  }
  backend "s3" {
    bucket = "my-terraform-state"
    key    = "production/terraform.tfstate"
    region = "us-east-1"
  }
}

resource "hcloud_server" "app" {
  name        = "app-server"
  image       = "ubuntu-24.04"
  server_type = "cx22"
  location    = "nbg1"
  ssh_keys    = [hcloud_ssh_key.default.id]
}

resource "hcloud_firewall" "web" {
  name = "web-firewall"
  rule {
    direction  = "in"
    protocol   = "tcp"
    port       = "443"
    source_ips = ["0.0.0.0/0"]
  }
  rule {
    direction  = "in"
    protocol   = "tcp"
    port       = "22"
    source_ips = [var.my_ip]
  }
}

resource "cloudflare_record" "app" {
  zone_id = var.cloudflare_zone_id
  name    = "app"
  value   = hcloud_server.app.ipv4_address
  type    = "A"
  ttl     = 300
}

resource "hcloud_ssh_key" "default" {
  name       = "default"
  public_key = file("~/.ssh/id_ed25519.pub")
}

variable "my_ip" {
  description = "My current public IP for SSH access"
  type        = string
}

variable "cloudflare_zone_id" {
  type = string
}

That is the entire infrastructure for a single-server web application: compute, network, DNS, and access control. The file is 60 lines. It replaces a 20-minute console session with a single command and a version-controlled artifact.

The state backend is the one part you cannot skip. Terraform state is a JSON file that maps resource names to real-world IDs. Without a remote backend, the state file lives on your laptop, and if your laptop dies, Terraform loses track of which cloud resources it manages — it will try to recreate them instead of updating them. An S3 bucket costs a few cents per month. Use it.

The CI/CD hook that keeps it honest

The point of IaC is not that you can run terraform apply from a terminal. The point is that every infrastructure change goes through the same pipeline: write code, review the plan, apply, verify. For a solo founder, “review the plan” means “read the terraform plan output before you apply it,” and the pipeline is a GitHub Actions workflow.

name: Terraform
on:
  push:
    branches: [main]
    paths: ['terraform/**']

jobs:
  plan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
      - run: terraform init
        working-directory: terraform
      - run: terraform plan -out=tfplan
        working-directory: terraform
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      - run: terraform apply tfplan
        working-directory: terraform

This workflow runs on every push to main that touches the terraform/ directory. You change a resource in main.tf, push, and GitHub Actions plans and applies it. If the plan fails — a syntax error, a missing variable, a resource conflict — the apply never runs, and the failure is visible in the Actions log.

The workflow does not need a manual approval step (you are the only person pushing), and it does not need a separate plan job with an artifact upload (the apply follows the plan in the same job). The solo-founder version prioritizes simplicity over separation of concerns. When you hire a second engineer, add the approval step.

FAQ

Should I use Terraform for my DNS records too?
Yes. DNS records are infrastructure, and managing them in Terraform means they are version-controlled, documented alongside the resources they point to, and reproducible when you migrate providers. The Cloudflare, AWS Route53, and Hetzner DNS providers all support Terraform. The one exception: if your domain registrar and DNS provider are different services, Terraform manages the DNS records but not the domain registration itself, which stays in the registrar's console. That is fine — domain registration changes once every few years, and console management for that one task is acceptable overhead.
How do I handle secrets in Terraform without checking them into git?
Terraform variables with `sensitive = true` are redacted from plan output, but they still appear in the state file as plaintext. The state file is the security boundary: if your state backend is publicly accessible, every secret in every resource is exposed. Use environment variables (`TF_VAR_db_password`) for secrets at apply time, store the state file in a private S3 bucket with server-side encryption, and never commit `.tfvars` files to git. For production-grade secret management — rotating credentials, audit logging — use your cloud provider's secret manager (AWS Secrets Manager, GCP Secret Manager) and reference secrets by ARN rather than by value.
At what point should I stop using a single main.tf and start splitting into modules?
When the file exceeds roughly 300 lines or when you need to deploy the same pattern in multiple environments. The natural split point is environment-specific variables: one `main.tf` that defines the resources, one `variables.tf` for input variables, and one `terraform.tfvars` per environment (staging, production) that supplies different values. Modules come later, when you find yourself copying the same 40-line block across environments and want a single source of truth. Until then, a flat file with explicit resources is easier to reason about and faster to debug.

Related tools

Some links above are affiliate links. We may earn a commission if you sign up. See our disclosure for details.

Related reading

See all Infrastructure articles →

Get the best tools, weekly

One email every Friday. No spam, unsubscribe anytime.