{"slug": "from-click-ops-to-iac-a-safer-workflow-with-ai", "title": "From Click-Ops to IaC: A Safer Workflow with AI", "summary": "Masterpoint engineer Veronika Gnilitska outlines a workflow that uses AI agents and MCP servers to convert AWS click-ops resources into Terraform/OpenTofu code, emphasizing human review gates and verification scripts to prevent AI hallucinations. The approach was tested on a migration of about 20 AWS Glue jobs, where the agent initially invented details but was corrected by extracting configurations to markdown and running key-by-key comparison scripts.", "body_md": "Published: 8.31.2026\nFrom Click-Ops to IaC: A Safer Workflow with AI\nBy Veronika Gnilitska\nTurn click-ops AWS resources into Terraform/OpenTofu with AI and MCPs, using human review gates, independent verification, and a merge checklist.\nTable of Contents\nThe problem with click-ops, in one migration\nWhy AI and MCPs help, and where they get dangerous\nA safer workflow for turning manual infrastructure into IaC\nMatching agent autonomy to operation risk\nPractical guardrails for MCP-assisted IaC work\nEnforce it in the pipeline, not just the prompt\nSplit the work across subagents\nReusable Agent Skill\nChecklist before merging AI-generated IaC\nCodify click-ops without handing AI the keys\nWe’ve seen the same pattern across client environments: AWS resources created in the console years ago, SSH keys nobody can trace, and security-group allow-lists that still contain former employees’ IP addresses. The resources keep working, so nobody touches them. Meanwhile, the gap between what runs and what is documented widens every quarter.\nThis guide shows how to close that gap without handing production to an agent. AI handles extraction and scaffolding; humans review the evidence and control every consequential action. The examples focus on Terraform and OpenTofu (collectively referred to as “TF”) on AWS.\nThe problem with click-ops, in one migration\n\n#\nOn a recent client engagement, the task looked mechanical: migrate about 20 AWS Glue jobs from one AWS account to another and codify them in TF along the way. Each job had a configuration that existed only in the console. Worker counts, script locations, VPC settings, connections.\nOne of our engineers pointed an AI agent (our choice at Masterpoint is Claude Code), connected to AWS through the \naws-iac-mcp-server MCP server,\n at the source account to pull down each job’s configuration and generate the TF for it. It worked, until it didn’t. Around the fifth or sixth job, the agent started inventing details, deciding the Python script filename should match the Glue job name because that is the “standard” pattern. The legacy jobs did not follow it. The generated code looked plausible and was quietly wrong.\nThe fix was not a better model, but a better process. The agent wrote a script that extracted every job’s configuration into structured markdown, and only after that data was reviewed did it transform the inventory into TF. And after the migration, the agent wrote a verification script that compared every job’s configuration key by key across both accounts and flagged the diffs. That script caught the remaining discrepancies and made the result trustworthy.\nThat is the whole thesis of this post in miniature. LLMs are inconsistent at repeating a boring process 20 times. They are excellent at writing the tooling that makes the process deterministic.\nWhy AI and MCPs help, and where they get dangerous\n\n#\nMCP servers give an agent structured access to your real environment. The \nTerraform MCP\n \nand OpenTofu MCP\n servers give it live provider and module documentation instead of stale training data. \nAWS MCP servers\n, like the AWS-managed MCP server with full API support and up-to-date documentation, let it inspect what actually exists in your account. Early on, we covered the mechanics of these in \nUsing MCPs to Run Terraform\n.\nFor click-ops codification specifically, that combination is genuinely strong. The agent can enumerate resources you forgot existed, cross-reference them against your state files, and draft the HCL to manage them. In our experience, discovery that used to mean days of console screenshots and spreadsheet archaeology collapses into an afternoon of review.\nPurpose-built importers like \nTerraformer\n and \naws2tf\n are fast and deterministic, but they emit flat code that captures every provider default and none of your conventions, so teams often rewrite the output anyway. The AI workflow below is slower and needs guardrails. In exchange, you get code shaped to your module patterns, with a review gate at every step.\nThe danger shows up when the agent stops reading and starts writing. Across our survey responses and client work, the failure modes cluster into four groups.\nState operations.\n An engineer on our team let an agent run TF state migrations in full-agent mode without requiring it to present a plan first. It lost the state. Recovery meant re-importing every resource, complete with the quoting and escaping headaches that \nimport\n commands bring when resource IDs contain quotes or live inside lists.\nNetworking and security changes.\n In a sandbox account, another engineer watched an agent “solve” a network connectivity issue by opening a security group to the public internet. Technically, the connection now worked. This is the scariest failure mode because the agent optimizes for the symptom you gave it, and broad ingress is always a valid-looking fix.\nHallucinated interfaces.\n Agents invent module inputs that do not exist. Ask for custom DB parameters in an RDS child module that does not expose them, and the agent will happily write \ndb_parameters = {...}\n as if by magic. Feeding it the module README and source helps less than you would expect.\nThe non-standard 10%.\n When 90% of your codebase follows a naming convention, the agent will steamroll the 10% that does not. Legacy infrastructure is, almost by definition, that 10%, and precisely the code you are trying to import.\nNone of these four is “the AI wrote bad syntax.” The output \nlooked\n right, but it was wrong. Newer models and live module schemas make every one of them rarer, but they still occur. An agent under pressure still reaches for the plausible pattern that doesn’t fit or the shortcut that incorrectly mutates state. So we build guardrails for the direction of failure instead.\nA safer workflow for turning manual infrastructure into IaC\n\n#\nThe workflow we recommend has five steps. The agent participates in all of them, but it has write access only in the middle, only to your repository, never to your cloud account or your state.\nDiscover with metadata-only access.\n In production, especially where the account may contain sensitive data, connect the MCP through a dedicated SSO profile that can inspect resource metadata and configuration but cannot read workload data. Generic read-only access may still allow object/item/secret reads, so reserve it for lower-risk environments. Give it a seed resource (say, a single ECS service ARN) and have it walk the dependency graph to enumerate everything related — task roles, security groups, target groups, autoscaling policies, CloudWatch alarms, ACM certificates — then extract each resource’s full configuration, secrets redacted, into \nstructured YAML files\n rather than HCL, since that is diffable and reviewable. This dependency expansion from a single entrypoint is where the workflow saves the most time versus hunting down \nclick-ops resources\n by hand.\nReview the extraction.\n Spot-check the extracted data against the console. A YAML inventory is much easier to review than generated TF.\nScaffold TF from the verified data.\n Now let the agent generate modules and root configuration from the extracted files. Provide your org’s conventions and file layout as context. Our post on \nthe standard TF files\n is a useful baseline to hand it.\nPlan the import + human runs it.\n Have the agent draft \nimport\n blocks\n (available in both OpenTofu and \nTerraform 1.5+\n) rather than imperative \nstate\n commands. Import blocks surface in the plan. A human runs \ntofu plan\n and reads it.\nVerify with a script.\n Generate a verification script that compares live configuration against the new code, key by key. Run it before and after cutover. A clean plan tells you TF is internally consistent. The verification script tells you the code matches reality.\nExpect the first plans after import to be noisy. Providers apply defaults and normalize values, so diffs appear on attributes nobody ever set. This \nplan noise\n is ordinary provider behavior that predates AI workflows. Set attributes to match provider defaults, and use \nignore_changes\n sparingly. Do not ask the agent to make the noise go away. It may happily change real configuration to silence a fake diff, so review every edit that gets the plan to zero.\nOne more expectation to set. This workflow is tuned for tens to low hundreds of resources, and beyond that, you should batch by the type of service, resource types, and plan around API rate limits during discovery.\nThe goal is a zero-change plan confirmed by an independent script. Then wire the repo into \nSpacelift\n or \nGitHub Actions\n so every change goes through a plan and a review instead of the console. Restrict human console write access at the same time, or the closet starts refilling with more skeletons the day you finish.\nMatching agent autonomy to operation risk\n\n#\nNot every operation deserves the same leash length. This table is how we scope it, distilled from our engineering survey.\nOperation type\nRecommended agent autonomy\nRead-only discovery and config extraction\nHigh.\n Let it run with read-only credentials\nWriting verification and comparison scripts\nHigh.\n The script itself is reviewable before it runs\nGenerating TF from verified, extracted data\nMedium.\n Human review required before merge\nModule usage and refactoring\nMedium.\n Verify every input against the real module schema\nImport planning\nLow.\n Agent drafts import blocks, human runs the plan\napply\n, \ndestroy\n, \nstate rm\n, \nstate mv\nNone\n without review. Agent must print exact commands and wait\nSecurity and network changes\nNone\n without review. Diff every rule and reject broad CIDRs\nThe bottom rows are where failures are irreversible or silent. A lost state file has no undo. A security group opened to \n0.0.0.0/0\n works perfectly and complains to nobody, and we have seen both agents and deadline-pressured humans reach for it when exact CIDRs are unknown. Encode the rule so neither can.\nPractical guardrails for MCP-assisted IaC work\n\n#\nDefault MCPs and cloud credentials to read-only, granting write access per task and revoking it after. With the \nAWS MCP server\n this is two environment settings, with IAM staying the primary control:\n\n```\n{\n  \"READ_OPERATIONS_ONLY\": \"true\",\n  \"REQUIRE_MUTATION_CONSENT\": \"true\"\n}\n```\n\nPrefer MCPs that show the underlying CLI or API call, and read those commands before approving them. Allow-list commands only after they earn it.\nRequire the agent to print any mutating command, in full, before running it. Put this in your rules file rather than trusting yourself to remember. It is the difference between the lost-state story above and a non-event.\nSecurity-review any MCP server or generated tooling before first use. Not foolproof, but it filters obvious problems.\nPut verification scripts in the definition of done. Generated code without an independent check is a draft, not a deliverable.\nEncode narrow platform constraints in rules files up front. Where the acceptable space is narrow, such as CI/CD workflows or module interfaces, an open-ended prompt invites the agent to fill the gap with its own inventions. We learned this with GitHub Actions, where a rule that said “follow security best practices” got us mutable action tags. A rewrite spelling out the exact format (pin to a full commit SHA, never \n@main\n or \n@latest\n) removed the poor security practice.\nKeep static checks in the loop. \nfmt\n, \nvalidate\n, \ntflint\n, and \nTrivy\n catch a class of problems before any human spends attention.\nEnforce it in the pipeline, not just the prompt\n\n#\nRules files are advisory. An agent can ignore them, and a rushed human can bypass them. The guardrails that matter most belong in the deployment platform, where they are mandatory. E.g., in Spacelift, that is a \nplan policy\n. This one rejects any plan that opens a security group to the world:\n\n```\npackage spacelift\n\ndeny[msg] {\n  rc := input.terraform.resource_changes[_]\n  rc.type == \"aws_vpc_security_group_ingress_rule\"\n  rc.change.after.cidr_ipv4 == \"0.0.0.0/0\"\n  msg := sprintf(\"%s allows ingress from the entire internet\", [rc.address])\n}\n\ndeny[msg] {\n  rc := input.terraform.resource_changes[_]\n  rc.type == \"aws_security_group_rule\"\n  rc.change.after.type == \"ingress\"\n  rc.change.after.cidr_blocks[_] == \"0.0.0.0/0\"\n  msg := sprintf(\"%s allows ingress from the entire internet\", [rc.address])\n}\n```\n\nA \nconftest\n step in GitHub Actions running the same Rego against plan JSON works too. Either way, the agent-opened-the-firewall failure mode dies in the pipeline, no matter who authored the change.\nSplit the work across subagents\n\n#\nClaude Code and other modern harnesses support subagents, scoped sessions with their own context window, tools, and permissions. Three uses map onto this workflow.\nFan repetitive work out, one subagent per service. The Glue job failure surfaced around job five, and long sessions are where models drift from reading toward pattern-matching. A fresh subagent per service with identical instructions keeps every job consistent, because nothing depends on the model remembering job one while writing job twenty.\nStart by separating permissions by role. The discovery agent only needs access to the read-only MCP, while the scaffolding agent can write to the repository but should not have cloud credentials. Neither should be able to run \napply\n or interact with state. Once those boundaries are enforced in configuration, the autonomy model no longer depends on everyone remembering the rules.\nIt also helps to separate generation from verification. Before the code reaches a human reviewer, a clean-context subagent can compare the generated TF against the inventory and the actual module schemas. Because it starts from a fresh context, the verifier provides an independent second pass before human review.\nThese controls do add friction, so they should match the risk involved. They make sense for production state, IAM, and network changes, where a mistake can have a wide blast radius. For a disposable sandbox or a short-lived spike, the same level of process would probably be unnecessary.\nReusable Agent Skill\n\n#\nWe suggest packaging the workflow as a reusable skill so the agent follows the same inventory-first process and safety checks every time.\nKeep your organization’s repository and module conventions in companion rules or skills (depending on your AI framework), rather than in the migration skill itself. If you don’t have any, our guides on \nTerraform root module structure\n and \nroot module sizing\n are a good starting point.\n\n0:00\n\nThe skill in action, expanding an ECS service with its related resources (IAM, network, autoscaling, etc.) into Terraform.\n\n```\n---\nname: codify-clickops-iac\ndescription: Safely codify existing manually-managed AWS infrastructure into Terraform/OpenTofu. Use when importing click-ops resources, migrating existing AWS resources into IaC, or generating TF from live infrastructure.\n---\n\n# Codify Click-Ops Infrastructure\n\nMove existing AWS infrastructure into Terraform/OpenTofu without changing it.\n\nThe target is a **zero-unexplained-change plan backed by independent verification**. Treat live infrastructure as data to extract, not a pattern to infer.\n\n## Assumptions\n\nThis skill assumes:\n\n- AWS resources are accessible through read-only AWS MCP tools or equivalent APIs.\n- A Terraform/OpenTofu MCP server is available for provider, module, and Registry documentation.\n- The target Terraform/OpenTofu repository is available.\n- The agent can read module source code.\n- A human performs all cloud mutations, state operations, and applies.\n\nThis skill assumes the following companion skills or rules are available:\n\n- **Repository structure** — defines the root module layout, required files, naming conventions, provider configuration, backend configuration, version constraints, tagging conventions, and project organization.\n- **Module development** — defines how reusable modules are structured, documented, versioned, and consumed.\n\nThis skill focuses only on migrating existing infrastructure into Terraform/OpenTofu. It relies on the companion skills to determine _how the repository and modules should be organized_.\n\n## Safety boundary\n\nCloud access is read-only.\n\nNever run:\n\n- `terraform apply` / `tofu apply`\n- `terraform import` / `tofu import`\n- `terraform state *` / `tofu state *`\n- `destroy`\n- Any AWS mutation\n\nA human owns all cloud and state mutations.\n\nNever broaden IAM or network access to make a resource work.\n\n## 1. Inventory\n\nStart from the seed identifiers you are given (for example, a single ECS service ARN) and expand outward to the full set of related resources before inventorying anything. Do not assume the scope is limited to the resources explicitly named.\n\nFrom each seed, follow its references and associations to discover the resources it depends on or that depend on it, for example:\n\n- An ECS service pulls in its task definitions, IAM task/execution roles, security groups, target groups and load balancer listeners, autoscaling targets and policies, CloudWatch log groups and alarms, ACM certificates, and service discovery entries.\n- Resolve each discovered resource's own references recursively until the graph stops expanding, then de-duplicate.\n\nRecord how each resource was reached (which seed and which reference) so the scope is auditable, and confirm the expanded set with the human before continuing. Missing a related resource here is the most common way an import leaves infrastructure half-managed.\n\nThen inspect every resource in the expanded scope and write its configuration to `./inventory/`, one YAML file per resource.\n\nCapture all configuration needed to reproduce the resource, including:\n\n- IDs\n- Names\n- ARNs\n- Tags\n- IAM configuration\n- Networking\n- Service-specific settings\n- References to secrets\n\nRecord values exactly as returned by AWS.\n\nRules:\n\n- Do not normalize legacy names or paths.\n- Do not infer missing values.\n- Represent unavailable values as `null`.\n- Never write secret values; record only references such as Secrets Manager ARNs.\n\nDo **not** generate Terraform yet.\n\nBefore continuing, compare the inventory against live AWS and resolve any missing or unexplained values.\n\n## 2. Scaffold\n\nGenerate Terraform/OpenTofu using the verified inventory as the source of truth.\n\nFollow the repository's existing structure and conventions.\n\nBefore using an existing module:\n\n- Read its actual interface from source (`variables.tf`).\n- Only use inputs it exposes.\n- If the required capability is missing, report it instead of inventing an input.\n\nPreserve the existing infrastructure exactly unless explicitly instructed otherwise, including:\n\n- Resource names\n- Filenames\n- Paths\n- IDs\n- IAM permissions\n- Network rules\n- Encryption\n- Logging\n- Public/private exposure\n\nPrefer the smallest implementation that accurately represents the infrastructure.\n\nRun non-mutating validation:\n\n``` bash\ntofu fmt -check\ntofu validate\ntflint\ntrivy config .\n```\n\nFix problems in the code — never by changing live infrastructure.\n\n## 3. Prepare imports\n\nCreate declarative `import` blocks using the real resource IDs from the inventory.\n\nWrite all `import` blocks to a dedicated `imports.tf` file, one block per resource, so imports live in a single reviewable place and are easy to remove after the import is complete. Do not scatter them across resource files.\n\nCheck every import address and ID carefully, especially IDs containing quotes, commas, indexes, or other escaping.\n\nExpect the first `plan` to fail. Each provider has its own required import ID format (for example, the AWS provider expects specific composite ID shapes per resource type), so the initial IDs are often wrong. This is normal: feed the exact error back in, correct the ID format in `imports.tf`, and re-plan until the IDs resolve. Iterating on these errors is part of the process, not a sign the workflow failed.\n\nDo **not** execute Terraform. The human will review the plan and perform the normal `plan`/` apply` workflow.\n\nProvide the exact commands for the human to run.\n\n## 4. Review the plan\n\nThe Terraform/OpenTofu plan answers one question:\n\n> **Will importing these resources introduce unintended infrastructure changes?**\n\nThe expected result is:\n\n- No destroy\n- No replacement\n- No unexplained functional changes\n\nExpected metadata drift (for example, agreed tag updates or provider normalization) may be acceptable. Explain every remaining diff before modifying the code.\n\nDo **not** make the plan green by changing real infrastructure.\n\nContinue only when every remaining change is understood and intentional.\n\n## 5. Verify independently\n\nThe verification script answers a different question:\n\n> **Does the generated Terraform/OpenTofu actually describe the live infrastructure?**\n\nWrite a read-only verification script that compares live AWS configuration against the inventory and generated Terraform, attribute by attribute.\n\nDo not reuse assumptions made while generating the Terraform.\n\nReport mismatches like:\n\n``` text\n<resource>  <attribute>  MISMATCH\n  expected: <value>\n  live:     <value>\n```\n\nExit with a non-zero status when mismatches exist.\n\n## Completion checklist\n\nBefore declaring success, confirm:\n\n- Every module input exists in the real module interface.\n- Names, paths, filenames, and IDs match the inventory.\n- Security posture is no weaker than the original.\n- Imports use declarative `import` blocks.\n- The human-reviewed plan contains no unexplained changes.\n- Inventory and Terraform contain no secret values.\n- The verification script passes.\n- Static validation passes.\n\nGenerated IaC is not complete until both the plan and independent verification succeed.\n```\n\nChecklist before merging AI-generated IaC\n\n#\nRun this on every PR where an agent wrote the code. It is a short review, and every item on it traces back to a failure we have hit.\nEvery module input exists in the module’s actual \nvariables.tf\n, checked against source, not the agent’s word\nResource names, filenames, and paths match the extracted inventory, not a “standard” pattern\nSecurity posture is no looser than what the inventory documents: no broadened network rules or IAM policies, no wildcard principals or actions, no publicly exposed endpoints or storage, no weakened encryption or logging settings\nImports use declarative import blocks, reviewed in the diff, not \nstate\n commands run from a terminal\nplan\n output reviewed by a human, with zero unexplained changes or destroys\nInventory files and generated TF contain no secret values, only references to where secrets live, since \nsecrets land in state in plain text\nVerification script exists, ran against live infrastructure, and passed\nfmt\n, \nvalidate\n, tflint, and a security scanner ran clean\nThe agent’s session used read-only cloud credentials, and any write action was executed by a human\nSomeone who did not prompt the agent reviewed the PR\nCodify click-ops without handing AI the keys\n\n#\nClick-ops infrastructure is not a moral failing. It is what happens when teams move fast, and every company has some. The real mistake is asking an AI agent with admin credentials and a vague prompt to codify it. The result is polished-looking code that reflects the agent’s assumptions rather than your infrastructure.\nThe pattern that works is older than AI: treat untrusted input with narrow interfaces and independent verification. AI accelerates extraction, scaffolding, and validation, but it doesn’t change the fundamentals.\nStart with one resource group, read-only credentials, and the Agent Skill above. And if you want a second set of eyes on the result or you want to do this at scale, our \nIaC audits\n exist for exactly this. 👋\n👋 \nSitting on click-ops infrastructure you want to codify?\n \nGet in touch\n, we're the experts at safely migrating manual infrastructure into IaC and we'd love to chat!", "url": "https://wpnews.pro/news/from-click-ops-to-iac-a-safer-workflow-with-ai", "canonical_source": "https://masterpoint.io/blog/dont-let-ai-break-your-infra/", "published_at": "2026-09-03 11:04:22+00:00", "updated_at": "2026-09-03 11:23:36.062646+00:00", "lang": "en", "topics": ["ai-tools", "ai-agents", "mlops", "developer-tools"], "entities": ["Masterpoint", "Veronika Gnilitska", "AWS", "Terraform", "OpenTofu", "Claude Code", "aws-iac-mcp-server", "Terraformer"], "alternates": {"html": "https://wpnews.pro/news/from-click-ops-to-iac-a-safer-workflow-with-ai", "markdown": "https://wpnews.pro/news/from-click-ops-to-iac-a-safer-workflow-with-ai.md", "text": "https://wpnews.pro/news/from-click-ops-to-iac-a-safer-workflow-with-ai.txt", "jsonld": "https://wpnews.pro/news/from-click-ops-to-iac-a-safer-workflow-with-ai.jsonld"}}