You can generate Terraform from existing AWS resources by inspecting the resources through AWS APIs, mapping their settings to provider-supported resource blocks, and reviewing the resulting HCL. ops0 Discovery helps inventory resources and produce IaC for review. Generating code and adopting resources into Terraform state are separate steps: neither output alone makes the existing infrastructure safely managed.
Terraform also provides native import workflows. The CLI import command adopts a resource into state without writing its configuration. Import blocks can be paired with configuration generation to create starter HCL for supported resources. For either route, review resource ownership, provider behavior, and the proposed plan before applying.
Why This Problem Exists
Most AWS infrastructure wasn't created with Terraform. Someone spun up an EC2 instance from the console during a demo. A developer created an S3 bucket for a quick test. The networking team built out VPCs manually because "we'll codify it later." Later never came.
Now you have resources without a matching IaC definition. Cloud activity logs may still record changes, but the repository does not explain how to reproduce or manage those resources. Inventory, change history, and declarative configuration answer different questions.
This is brownfield infrastructure: an existing environment you are bringing under a new management workflow. Start with a bounded set of resources and identify anything already owned by another Terraform state, CloudFormation stack, controller, or deployment tool.
The Manual Approach and Why It Breaks Down
Terraform has a built-in import command. You can run terraform import aws_instance.example i-1234567890abcdef0 and it pulls the resource into your state file. Then you write the matching HCL by hand.
The problems start immediately. You need to know the resource type and its ID. You need to write HCL that matches the current config exactly, or terraform plan will show drift on your first run. Dependencies between resources have to be figured out manually. If your security group references a VPC, you need both imported and wired up correctly.
For a single resource, matching the live settings to a resource block is manageable. At larger scale, the difficult part is consistent ownership, addressing, dependencies, and review. Automation helps collect the facts, but provider support and the quality of the generated configuration still determine the work needed.
Terraform 1.5 introduced import blocks and experimental configuration generation with terraform plan -generate-config-out=generated.tf. The generated HCL is a starting point to edit, not a guarantee of a correct module design. HashiCorp documents the generation workflow and conflicting-argument limitations.
Worked Example: Generate HCL for One Existing S3 Bucket
Illustrative import workflow, not an ops0 success report. Use a bucket you own in a sandbox account. Replace the example name with its actual import ID, configure the correct AWS provider credentials and region, and select a secured state backend before adoption. The bucket must not already be managed in a different state or stack.
Add an import.tf file with an import block. Leave the matching resource block absent for this configuration-generation example.
import {
to = aws_s3_bucket.existing
id = "replace-with-your-existing-bucket-name"
}
terraform init
terraform plan -generate-config-out=generated.tf
terraform fmt
terraform validate
terraform plan -out=adoption.tfplan
terraform show adoption.tfplan
The output path must be new. Review the generated resource, fix conflicting arguments, and rerun the plan. The intended adoption plan imports the selected bucket with no unintended create, update, replace, or destroy actions. If changes appear, stop and decide whether the code should match the live settings or whether a separate infrastructure change is needed.
After review and approval, terraform apply adoption.tfplan records the planned import. It can also perform any changes contained in that saved plan, which is why review comes first. Run a new normal plan afterward and verify the baseline. Do not treat an import action as evidence that every related bucket control was adopted.
Import, State, and Coverage Limitations
An S3 bucket can have separately modeled controls such as versioning, policy, public-access blocking, and encryption configuration. Check the AWS provider documentation for each resource and import format. Importing the bucket itself is not the same as adopting those related resources.
Use a single state owner per remote object, protect state and plan files as potentially sensitive data, and keep credentials out of the repository. Unsupported resources, provider defaults, external controllers, and resources owned by CloudFormation all require an explicit decision. HashiCorp explains CLI import ownership and sensitive data in state.
Terraform Import Blocks vs the terraform import Command
Terraform 1.5 added the import block, which declares an import in configuration instead of running it as a one-off command. You write an import block with the resource address (to) and the provider's identifier (id), then run a normal plan. The import shows up in the plan output and is applied with the rest of the change, so it goes through the same pull request and approval as any other edit.
The older terraform import command writes one object into state immediately, with no plan to review and no record in your configuration. It still works, but import blocks are the safer default for teams that review infrastructure changes. Terraform 1.7 added for_each on import blocks for adopting many similar resources at once, and OpenTofu supports import blocks as well.
For resources that have no configuration yet, terraform plan -generate-config-out=generated.tf writes starter HCL for each import block. Treat that file as a draft: it copies provider defaults and computed values you will usually want to trim before review.
ops0 follows the same reviewed pattern. Discovery selects the resources, then generates the configuration and an import order based on their dependencies, so the team reviews code and plan before anything is adopted into state.
How Automated Discovery Works
Automated discovery collects resource configuration from the APIs and helps map relationships into IaC. Coverage depends on the account permissions, regions, supported resource types, and provider schema. Check the scan results for skipped resources and errors before treating the inventory as complete.
ops0's Discovery process works like this:
- 1Connect your AWS account (read-only IAM role)
- 2Discovery scans the configured accounts, regions, and supported resource types
- 3Review the discovered configuration, tags, networking, and access settings for the selected resources
- 4Review discovered dependencies and import order. Verify important relationships against provider configuration; missing permissions or unsupported links can leave the graph incomplete.
- 5Generate Terraform HCL for the selected resources, then validate dependency references and provider-supported arguments before preparing state import.
Treat the output as reviewable infrastructure code. Confirm references, provider aliases, account and region settings, import identifiers, and any sensitive values. A generated block can be syntactically valid and still propose an unwanted change when compared with the live resource.
What Gets Captured
For each selected AWS resource, inspect the settings needed by its Terraform provider schema: names, tags, region, networking, encryption, access controls, and references to related resources. API discovery can expose useful context without making every value a writable Terraform argument.
Computed attributes, provider defaults, separately managed policy attachments, and sensitive values need individual treatment. An omitted setting may create a plan difference; copying every API field into HCL can also produce invalid or conflicting arguments.
Handling the Edge Cases
Real AWS accounts are messy. Discovery has to handle resources that depend on each other in circular ways, resources that were partially configured, resources created by other tools (CloudFormation, CDK, Pulumi) that might conflict, and resources that exist in the console but aren't visible through standard API calls.
ops0 handles this with checkpoint-based scanning. If a scan fails halfway through because of a transient API error or rate limiting, it picks up from the last checkpoint instead of starting over. This matters in large accounts where a full rescan takes minutes.
From Discovery to Managed Infrastructure
Generating the Terraform is step one. The real value comes from what happens next.
Once your resources are codified, you can version control them. You can run terraform plan before making changes. You can set up compliance checks that catch violations before deployment. You can detect drift when someone makes a manual change.
ops0 connects Discovery directly to its IaC engine. The generated Terraform feeds into deployment pipelines with compliance gates. The Resource Graph tracks drift between your code and reality. Continuous monitoring shows the deployed infrastructure status and anomalies.
Move from inventory to reviewed code, then to state adoption and a verified plan. The effort depends on provider coverage, resource count, dependencies, and how much of the environment is already managed. There is no universal scan time or adoption-time guarantee.
When to Use This Approach
Automated discovery makes sense when you have more than a handful of unmanaged resources, when you need to bring an existing environment under Terraform management quickly, when you're doing a cloud audit and need a complete inventory, or when you're migrating between IaC tools and need a clean starting point.
It does not replace understanding your infrastructure. Review the code, choose the scope, secure the state backend, and establish one owner for each resource. Expand only after a small import gives you a plan with no unintended additions, changes, or deletions.
For the product workflow, start with ops0 Discovery, review the generated code through Infrastructure as Code, and use drift prevention to check what changes after adoption.
Example Review Step
1. Run discovery against the AWS account.
2. Generate Terraform for unmanaged resources.
3. Review names, tags, IAM, networking, and dependencies.
4. Sync the generated files to Git.
5. Import state only after the generated code matches live AWS resources.
Can existing AWS resources be converted to Terraform?
Yes. A discovery workflow can inspect existing AWS resources and generate Terraform that teams review before adopting those resources into IaC.
What should teams review before adopting generated Terraform?
Teams should review resource mappings, names, tags, state import strategy, dependencies, IAM behavior, and whether generated code matches the live account.
How does Git sync help brownfield IaC adoption?
Git sync makes generated Terraform reviewable through branches, pull requests, commits, and audit history before production ownership changes.
- Generating configuration for import: HashiCorp. Import blocks, starter HCL, review, and conflicting resource arguments.
- terraform import command: HashiCorp. CLI import updates state and requires a single resource owner.
- terraform plan command: HashiCorp. Saved plans and inspection of proposed infrastructure actions.
- Manage sensitive data: HashiCorp. Sensitive values in state and plan artifacts.
- aws_s3_bucket resource: HashiCorp AWS Provider. Bucket import identifiers and separately managed S3 controls.