Infrastructure as Code for Solo Developers: Terraform, Pulumi, and When a Shell Script Is Enough
You have one VPS, a domain, and a side project that just started getting real traffic. Something breaks at 2 AM, and you SSH in, fix it manually, then realize you have no idea what you changed last time. Three months later, you need to spin up a staging environment and discover that your production server is a snowflake — a unique artifact of dozens of undocumented tweaks that exist nowhere except on that single machine.
This is the moment infrastructure as code solo developers encounter for the first time: not as a theoretical best practice borrowed from platform engineering teams, but as a genuine personal need. The question is not whether to adopt IaC, but how much of it you actually need.
Why IaC Matters When You Are the Entire Team
The standard argument for infrastructure as code centers on collaboration — version control, code review, shared understanding across a team. Strip all of that away for a solo developer and the case still holds, for a different reason entirely: your memory is unreliable.
A declarative description of your infrastructure serves as living documentation. It records not just what exists, but what you intended. When you come back to a project after six months of working on something else, a Terraform file tells you exactly what DNS records you configured, what firewall rules are in place, and what cloud resources are running. Your bash history does not.
There is also the disaster recovery angle. Solo developers rarely think about this until they need it. If your VPS provider has an outage, or if you accidentally destroy a droplet, how long does it take you to rebuild everything? With proper IaC, the answer is minutes. Without it, the answer is “however long it takes me to remember what I did.”
But the benefits come with real costs: learning curves, additional tooling, state management overhead, and the temptation to over-engineer. The trick is finding the right tool at the right abstraction level for your actual situation.
Terraform: The Industry Default
Terraform remains the most widely adopted IaC tool, and for good reason. Its declarative model using HashiCorp Configuration Language (HCL) maps cleanly to how most people think about infrastructure: describe what you want, and let the tool figure out how to get there.
A minimal Terraform configuration for a solo developer might look like this:
resource "digitalocean_droplet" "web" {
image = "ubuntu-24-04-x64"
name = "web-production"
region = "nyc3"
size = "s-1vcpu-1gb"
ssh_keys = [digitalocean_ssh_key.main.fingerprint]
}
resource "cloudflare_record" "web" {
zone_id = var.cloudflare_zone_id
name = "app"
content = digitalocean_droplet.web.ipv4_address
type = "A"
proxied = true
}
This is readable even if you have never touched Terraform before. A droplet gets created, a DNS record points to it. Run terraform apply, and both resources exist. Run terraform destroy, and they are gone.
The strengths for solo use: HCL is simple enough to learn in a weekend. The provider ecosystem is massive — virtually every cloud service has a Terraform provider. The plan/apply workflow gives you a preview of changes before they happen, which is invaluable when you are the only person catching mistakes.
The friction points: HCL is not a programming language. The moment you need conditional logic, loops over dynamic data, or string manipulation beyond basic interpolation, you start fighting the syntax. Terraform’s type system is minimal, and complex configurations often feel like you are working around the language rather than with it.
Pulumi: Write Real Code Instead
Pulumi takes a fundamentally different approach: instead of a domain-specific language, you write infrastructure definitions in TypeScript, Python, Go, or C#. For developers who already think in code, this can feel dramatically more natural.
import * as digitalocean from "@pulumi/digitalocean";
import * as cloudflare from "@pulumi/cloudflare";
const droplet = new digitalocean.Droplet("web", {
image: "ubuntu-24-04-x64",
name: "web-production",
region: "nyc3",
size: "s-1vcpu-1gb",
});
new cloudflare.Record("web", {
zoneId: config.require("cloudflareZoneId"),
name: "app",
content: droplet.ipv4Address,
type: "A",
proxied: true,
});
The result is functionally identical to the Terraform example. But because this is TypeScript, you get full IDE support, type checking, the ability to write functions and abstractions, and access to the entire npm ecosystem. Need to fetch something from an API during deployment? Import axios. Need complex string formatting? Just write JavaScript.
For a solo developer, Pulumi’s advantage is cognitive consolidation. If you already write TypeScript for your application, writing your infrastructure in the same language means one less context switch. You can share types between your app and your infra code. You can write unit tests for your infrastructure using the same testing framework you already know.
The downside is complexity. Pulumi requires a runtime, has its own state management service (though you can self-host the state backend), and its documentation, while improving, is less mature than Terraform’s. When something goes wrong, debugging involves both your code and Pulumi’s internal engine, which can be opaque.
CDKTF: The Middle Ground
Cloud Development Kit for Terraform (CDKTF) attempts to combine Terraform’s provider ecosystem with a general-purpose programming language. You write TypeScript (or Python, Java, C#, Go), and CDKTF synthesizes it into Terraform JSON that is then applied through the standard Terraform engine.
This is appealing in theory: you get Terraform’s battle-tested providers and state management with the expressiveness of a real programming language. In practice, CDKTF adds a compilation layer that can introduce subtle mismatches between what you wrote and what Terraform executes. Error messages sometimes refer to the generated JSON rather than your source code, making debugging frustrating.
For solo developers, CDKTF is worth considering if you are already invested in the Terraform ecosystem but find HCL limiting. Otherwise, the added abstraction layer rarely justifies itself for small-scale infrastructure.
Tool Comparison
| Criteria | Terraform (HCL) | Pulumi | CDKTF | Shell Scripts | Ansible |
|---|---|---|---|---|---|
| Language | HCL (DSL) | TS, Python, Go, C# | TS, Python, Go, C#, Java | Bash / Zsh | YAML + Jinja2 |
| Learning curve | Low-Medium | Medium | Medium-High | Low | Medium |
| State management | Built-in (local/remote) | Pulumi Cloud or self-hosted | Terraform backend | None | None (agentless) |
| Provider ecosystem | Excellent | Good (growing) | Excellent (uses TF providers) | N/A (raw API calls) | Good (modules) |
| Idempotency | Yes | Yes | Yes | Manual effort | Yes |
| Best for solo devs when… | Managing 3+ cloud resources | You want one language for everything | HCL feels too limiting | 1-2 simple resources | Server configuration post-provisioning |
| Cost (solo use) | Free | Free tier available | Free | Free | Free |
| Drift detection | Yes (terraform plan) |
Yes (pulumi preview) |
Yes (via Terraform) | No | Partial |
When a Shell Script Is Genuinely Enough
Here is an opinion that will draw objections from the IaC purist crowd: sometimes a shell script is the right answer.
If your entire infrastructure is a single VPS that you provision once and configure occasionally, the overhead of Terraform or Pulumi may genuinely exceed the benefit. A well-written shell script that calls your cloud provider’s CLI can provision a server, set up DNS, and configure a firewall in 50 lines. It is not idempotent, it does not track state, and it will not handle drift detection — but if you run it once a year when you rebuild your server, those shortcomings do not matter.
The shell script becomes insufficient when any of these conditions appear:
- You manage resources across multiple providers (VPS + DNS + CDN + object storage)
- You need to reproduce your infrastructure regularly (staging environments, client projects)
- Your infrastructure has dependencies that must be created in order
- You want to preview changes before applying them
- You have been bitten by a manual change that caused an outage
The honest threshold for most solo developers: once you cross roughly three interconnected cloud resources, the investment in a proper IaC tool starts paying for itself.
State Management Without a Team
State is the single most misunderstood concept for developers new to IaC. Terraform’s state file is a JSON document that maps your configuration to real-world resources. Lose it, and Terraform no longer knows what it manages. Corrupt it, and you are in for a painful recovery.
For solo developers, state management choices look different than for teams:
Local state with version control: The simplest approach. Keep your terraform.tfstate file in a private Git repository. This is technically discouraged because state files can contain secrets, but for a solo developer with a private repo and no sensitive credentials in state, it works. Encrypt the file with git-crypt or sops if you are handling anything sensitive.
Remote state with S3 or equivalent: Store your state in an object storage bucket with versioning enabled. This is more robust — you get automatic backups through bucket versioning, and you eliminate the risk of losing state if your local machine dies. For solo use, a Backblaze B2 or Cloudflare R2 bucket costs nearly nothing and gives you proper state locking.
Pulumi Cloud free tier: If you use Pulumi, their hosted service manages state for you. The free tier covers individual developers comfortably. This removes state management from your concerns entirely, at the cost of depending on a third-party service.
Whichever approach you choose, the rule is simple: state must be backed up, and it must never be shared in a public repository.
Ansible for Configuration vs. Terraform for Provisioning
A common source of confusion is the boundary between provisioning and configuration. Terraform creates infrastructure: servers, networks, DNS records, load balancers. Ansible configures what runs on that infrastructure: installing packages, deploying applications, managing systemd services, editing configuration files.
These tools are complementary, not competing. A practical solo developer workflow looks like this:
- Terraform provisions a VPS, sets up DNS records, configures firewall rules at the provider level, and creates any object storage buckets or managed databases you need.
- Ansible connects to the provisioned server via SSH and installs Docker, configures Nginx or Caddy, sets up SSL certificates, deploys your application, and manages ongoing configuration changes.
You can use Terraform’s remote-exec provisioner to run commands on a server after creation, but this is intentionally limited. HashiCorp explicitly recommends using a dedicated configuration management tool for anything beyond basic bootstrapping.
For the solo developer who finds both tools excessive, Ansible alone can handle a surprising amount. It can provision cloud resources through its own module ecosystem (though less elegantly than Terraform), and its agentless, SSH-based approach means there is nothing to install on your servers. If you are going to learn only one tool, Ansible’s broader applicability makes it a strong choice.
The Cost of Over-Engineering
The infrastructure-as-code ecosystem has a gravitational pull toward complexity. Read enough blog posts and you will convince yourself that your two-server side project needs Terraform workspaces, remote state with DynamoDB locking, a CI/CD pipeline for infrastructure changes, automated drift detection, and a custom provider for your homegrown API.
This is over-engineering, and it carries real costs for solo developers:
- Time spent on tooling instead of product. Every hour configuring your IaC pipeline is an hour not spent on the application your users actually care about.
- Maintenance burden. Terraform providers update, APIs change, and your carefully crafted configurations develop deprecation warnings that nag you every time you run a plan.
- Abstraction debt. Overly abstracted infrastructure code becomes harder to understand when you return to it after months away. A flat, repetitive Terraform file is easier to reason about than a deeply nested module hierarchy.
The antidote is pragmatism. Start with the minimum viable IaC and expand only when a real problem forces you to.
A Practical Starter Pattern: VPS + DNS + SSL
For solo developers ready to adopt infrastructure as code, here is a concrete starting point that covers the most common setup:
Project structure:
infra/
main.tf # VPS + DNS resources
variables.tf # Provider tokens, domain names, regions
outputs.tf # IP addresses, hostnames
terraform.tfvars # Actual values (gitignored)
ansible/
playbook.yml # Server configuration
inventory.ini # Generated from Terraform output
What Terraform manages:
- One VPS instance with your preferred OS image
- DNS A record pointing your domain to the VPS
- DNS records for email (MX, SPF, DKIM if applicable)
- Cloud firewall allowing ports 22, 80, 443 only
- SSH key registration with the cloud provider
What Ansible manages:
- System updates and unattended upgrades
- Caddy or Nginx with automatic SSL via Let’s Encrypt
- Docker and Docker Compose installation
- Application deployment via Docker Compose
- Basic hardening: fail2ban, SSH key-only auth, firewall rules
This pattern handles 80% of solo developer infrastructure needs. It provisions in under five minutes, it is reproducible, and the total configuration is small enough to keep in your head. Expand from here only when your project demands it.
Making the Decision
The infrastructure as code solo developers question ultimately comes down to a cost-benefit analysis that is personal to your situation. Here is a decision framework:
Use shell scripts if you have a single server you rarely rebuild, and you are comfortable with the risk of undocumented manual changes.
Use Terraform if you manage multiple cloud resources, want change previews, and prefer a focused tool with excellent documentation.
Use Pulumi if you want infrastructure defined in the same language as your application and are comfortable with a smaller community.
Use Ansible alone if your infrastructure is simple but your server configuration is complex, or if you want a single tool for both provisioning and configuration.
Use Terraform + Ansible together if you want clean separation between provisioning and configuration and plan to manage infrastructure that may grow.
The worst choice is no choice at all — continuing to SSH into servers and make changes by hand while telling yourself you will document it later. You will not document it later. Pick the simplest tool that covers your current needs, commit your configurations to a private repository, and move on to building the thing that actually matters.
Michael Sun builds and writes about infrastructure for independent developers. His current stack uses Terraform for provisioning, Ansible for configuration, and the occasional shell script for everything in between.