Almost every growing engineering organization has the same story. You shipped fast to find product-market fit. Cloud resources were created through the AWS, GCP, or Azure console because that was the fastest path. Then more were added. Then more. And now you have a production environment that nobody fully understands, no version control, no audit trail, and no safe way to reproduce it.
This is what we call click-ops, and it is one of the most common reasons growing teams hit a wall.
The good news: you do not need to rebuild your platform from scratch to get out of click-ops. You need a structured migration to Infrastructure-as-Code (IaC), executed in a way that does not put production at risk.
Why move off click-ops at all
Before discussing the migration, it is worth being precise about why this matters:
The migration approach that actually works
Trying to import every existing resource into Terraform on day one is a recipe for outage. Here is the approach we use with Cloudvorn clients.
1. Start with discovery, not coding
Before writing a single line of Terraform, inventory what exists. Use 'aws-resources-export', 'gcloud asset export', or similar to dump current-state resources. Group them by criticality and rate of change. This is the single most undervalued step.
2. Choose your tool deliberately
Terraform is the default for good reason — it has the largest provider ecosystem and the deepest community. OpenTofu is the open-source fork worth considering if you want to avoid HashiCorp licensing concerns. Pulumi is excellent if your team would prefer real programming languages over HCL. AWS CDK is reasonable if you are AWS-only and want TypeScript or Python.
Pick one. Do not mix.
3. Codify new resources first
The lowest-risk path is to start writing IaC for *new* resources from day one. Anything created from now on lives in version control. This stops the bleeding even before you migrate existing resources.
4. Migrate one bounded surface at a time
Pick something with low blast radius and low rate of change — IAM policies, S3 buckets, security groups. Use 'terraform import' to bring them under management. Verify the plan output is empty (i.e., your code matches reality). Commit. Move to the next surface.
The rule: never migrate something you cannot afford to break. Save your highest-risk resources for last, after you have built migration muscle memory.
5. Set up remote state with locking before you go further
State files contain sensitive data and cannot be lost or corrupted. Use S3 + DynamoDB locking, GCS, or Terraform Cloud. Never check state files into git.
6. Build CI/CD for plan and apply
Manual 'terraform apply' from someone’s laptop is barely better than the console. Set up a pipeline that runs 'plan' on every pull request, requires approval, and runs 'apply' only after merge. This is where IaC starts paying compounding dividends.
7. Document as you go
Every module gets a README. Every architecture decision gets an ADR. Every non-obvious choice gets a comment. Future-you will thank present-you.
Common pitfalls to avoid
How long does this take
For a typical SME with a moderately complex AWS environment, a structured IaC migration runs 2–6 weeks depending on scope. The first 2 weeks usually cover discovery, tooling setup, and the first low-risk surfaces. The remaining time is spent migrating progressively more critical resources with confidence built up from earlier wins.
If you want a fixed-price, structured approach to this migration, that is exactly what our [DevOps & Platform Engineering Foundation](/services#devops-foundation) engagement is built for. We come in with a proven methodology, do the migration alongside your team, and hand off documentation your engineers can maintain long after the engagement ends.
The takeaway
Click-ops is fixable. The trick is not heroics — it is sequencing. Start with discovery, codify new resources first, migrate bounded surfaces one at a time, and build CI/CD around plan/apply before you go deep. Done right, you end up with infrastructure your whole team can read, review, and operate — without a single minute of production downtime.