Choosing a Better Tool
Scenario: Every single Terraform apply for a fleet of servers SSHs into each one to install the latest application packages - which means every infrastructure change is now slow, fragile, and depends on a live package repository being reachable at exactly the wrong moment.
This whole module has been building toward one central production lesson: Terraform is very good at deciding whether an object exists and how it's wired to other objects, and it is not the right tool for configuring what runs inside that object once it exists. For immutable server contents, bake a machine image ahead of time (with the application already installed) rather than configuring a fresh instance from scratch on every deploy. For first-boot bootstrap, use cloud-init/user-data, which the cloud platform itself runs at boot with no SSH round-trip required. For ongoing convergence of an already-running host's configuration, use a dedicated configuration-management tool like Ansible - built specifically for that job, with real idempotency guarantees Terraform's provisioners don't offer. For deploying and running applications themselves, use Kubernetes or an equivalent deployment platform. For anything an API genuinely represents as an object, use the matching provider resource. And for notifications or publishing build artifacts, that's what your CI pipeline is already good at.
The unifying idea: Terraform's job across a system boundary like this is to create the infrastructure and hand off a small, well-defined set of identifiers and endpoints - an IP address, a DNS name, a connection string - to whichever system is responsible for the next step. It should not try to absorb every operational task itself just because it technically can run a shell command.
Analogy: A city planner lays out roads, zoning, and parcel boundaries. Restaurant owners run the actual kitchens that go on those parcels. Handing the planner's office the additional job of also running every restaurant's kitchen wouldn't make the city better organized - it would just make one office responsible for things it has no real ability to do well, and no way to specialize in.
A worked configuration
# Terraform creates infrastructure and publishes a narrow handoff.
output "inventory" {
value = { for k, v in fakecloud_server.node : k => v.address }
}
A genuinely useful production test for "should Terraform be doing this operation, or should something else": ask which system can actually detect drift in this operation over time, retry it safely if it fails partway, report clear status on whether it succeeded, and roll it back if needed. Wherever another tool already has real, mature lifecycle capabilities for a given job - configuration management, deployment platforms, CI - hand that job to it, and let Terraform stay focused on the one thing it does exceptionally well: reconciling infrastructure against a declared desired state.
Common mistakes in production
- Using Terraform provisioners to configure an entire server's software stack from scratch on every apply, instead of baking a machine image ahead of time or using a dedicated configuration-management tool.
- Letting Terraform's role creep into deployment and notification responsibilities that a CI pipeline or deployment platform is already better equipped to own and observe.
Goal: Treat the plan as a change contract: understand every create, update, replacement, and destroy before approving it. Reinforce this lesson in the tf-first-apply Terraform lab. Open/labs/terraformand choose slugtf-first-apply; the lab runs real Terraform against the offline FakeCloud provider.