AI-narrated article. Prose is preserved with pronunciation adjustments; code and image cues refer to the written article.
Import Existing AWS Resources: Adopt S3 Without Changing Its Contract
This is the AI-narrated article. The prose follows the written post; pronunciation is adjusted, and code and images are announced separately.
Adopt an existing S3 bucket and its configuration facets through explicit ownership checks, read-only discovery and a reviewed import-only Terraform plan.
TL;DR: Importing an AWS resource establishes a Terraform state binding; safe adoption also requires explicit ownership and configuration review. This article supplies complete S3 bucket, versioning, public-access and encryption declarations with four import blocks and read-only discovery commands. Exact Terraform and provider pins keep the example bounded. The initial gate allows intended imports while rejecting infrastructure creates, updates and deletes. Existing tags and settings must be reconciled before an approved apply. Encryption mismatches, duplicate ownership and cleanup mistakes receive concrete diagnosis paths. The example was statically reviewed without AWS execution or invented results.
An existing S3 bucket can have years of consumers, policy decisions and operational history behind it. Importing its name into Terraform is easy. Adopting its configuration without changing that history requires a reviewed import-only plan and an agreed owner for each managed setting.
Writing the configuration you wish the bucket had, importing it, then applying the differences combines ownership transfer with an infrastructure change. Separate those decisions so reviewers can see exactly what Terraform will begin controlling.
This example adopts one existing general-purpose bucket and three configuration facets. It was statically reviewed against primary documentation; no AWS discovery, Terraform plan or import was executed.
Establish ownership before choosing an import address
Confirm the AWS account, bucket name, region and owning team. Search existing Terraform states and repositories, CloudFormation stacks and other controllers before calling the resource unmanaged. A missing declaration in your current repository does not prove there is no existing owner.
Terraform expects one resource address per managed remote object. Importing an object into a second state does not negotiate an ownership transfer: both configurations can subsequently try to control it. A coordinated handover must stop the previous controller's reconciliation and preserve its evidence before the new owner applies. Import ownership warning.
The bucket, versioning configuration, bucket-level public-access block and encryption configuration have distinct Terraform resource types. Importing the bucket does not automatically create declarations or state bindings for every configuration facet, nor does it import stored objects. Import workflow, Pinned bucket resource.
Image. Import-only adoption gate with stop and reconcile branches
Import-only means state adoption with zero planned infrastructure creates, updates or deletes; it does not mean an empty plan before the import.
Discover the current contract with read-only calls
Use Terraform 1.16.0, the AWS provider pinned to 6.0.0, AWS CLI v2 and Bash. Record the actual CLI version. Start in an access-controlled directory with an approved AWS profile. Keep discovery files, plans and state outside public repositories. Configure the team's approved backend and workspace before adoption; these example files otherwise use local state.
The credentials must belong to the bucket-owning account. The existing bucket must already have Enabled versioning, AES256 default encryption and all four bucket-level public-access flags true. MFA delete must be disabled or absent. Directory buckets, MFA-delete changes and KMS migration are excluded. If discovery differs, adapt the configuration to preserve existing settings.
Save and run discovery.sh after setting AWS_PROFILE, BUCKET and ACCOUNT_ID:
Code example. Refer to the written article for the complete code.
Invoke bash discovery.sh. Compare the caller account with the expected owner. The bucket calls use --expected-bucket-owner. Review location, versioning, encryption, public-access flags and tags separately. Location uses null for us-east-1 and legacy EU for eu-west-1; normalize it before configuring the provider. Bucket location, Versioning inspection.
The script stops on errors. If tagging returns NoSuchTagSet, confirm the bucket has no tags before using an empty map. AccessDenied is missing evidence, not an absent configuration. Bucket-level public-access flags do not describe the entire account-level or policy access contract. Public-access inspection, Tag inspection.
Declare only the settings you have reconciled
Save main.tf:
Code example. Refer to the written article for the complete code.
Copy the observed tag set into terraform.tfvars. This illustrative file is a shape to replace, not discovered data:
Code example. Refer to the written article for the complete code.
No provider default tags are added during adoption. Adding an ownership tag is still a change. The allowed-account guard checks the credential account; it does not replace bucket-owner discovery.
The pinned provider documents bucket-name imports for same-account versioning and encryption, and for public-access blocking. Declarations must match observed settings. The actual plan remains authoritative for defaults and omitted attributes; this template cannot guarantee a no-change result. Versioning resource, Encryption resource, Public-access resource.
prevent_destroy rejects destruction while its rule remains declared. It does not prevent updates, and removing the resource configuration removes that protection. Likewise, force_destroy = false does not make every bucket operation harmless. Lifecycle semantics.
Review a complete import-only plan
Save imports.tf:
Code example. Refer to the written article for the complete code.
Then initialize and inspect the prospective adoption:
Code example. Refer to the written article for the complete code.
Review the lock file and selected provider version. The gate is four intended imports, zero additions, zero changes and zero destructions. That is a review criterion, not an observed result. Inspect individual addresses and identifiers as well as the summary; a clean summary does not establish the correct business owner.
If a plan changes tags, encryption, versioning or another field, stop and reconcile the declaration with discovery. Regenerate the saved plan after editing. Do not apply an earlier file or use ignore_changes = all to conceal unexplained differences.
An import-only plan can return exit code 2 with -detailed-exitcode, because adoption changes Terraform's tracking. Do not equate every exit code 2 with an infrastructure update; inspect planned actions. Saved plan files can contain sensitive data. Plan command semantics.
Only after explicit authorization for the reviewed ownership transfer, execute:
Code example. Refer to the written article for the complete code.
Applying a saved plan does not prompt for confirmation. Review and authorization belong before the command. Verify all four bindings and review the following ordinary plan for unintended changes. These commands were not executed here. Saved-plan apply.
Recover from the common encryption mismatch
A realistic failed adoption uses AES256 in the template while discovery reports a customer-managed KMS key. Terraform can propose changing encryption alongside import. The import may be technically valid while the plan violates the no-change boundary.
Reconcile the encryption declaration with the observed algorithm, key and bucket-key setting, verify the key's owning contract, and produce a fresh plan. Any later encryption migration requires separate impact analysis and approval. The pinned encryption resource documents that deleting its Terraform resource resets default encryption, so removing configuration is not safe cleanup.
This is a diagnosis scenario, not an observed cloud failure. Preserve discovery evidence and the rejected plan in practice. Generated configuration can assist unfamiliar imports, but HashiCorp describes it as a best-guess template requiring review. Configuration generation.
Cleanup means forgetting ownership, not deleting data
Before adoption, cleanup involves only local review artifacts. After adoption, do not run destroy to “undo the tutorial.”
For an approved handover, stop applies, preserve a protected state backup and coordinate removal of both resource declarations and import blocks. Review the exact four addresses with terraform state rm -dry-run before authorized state removal. State removal leaves AWS objects intact, but retained declarations can propose recreation and retained imports can adopt them again. Do not remove state during an active run or disable locking to bypass contention. State removal.
The result should be an explicit configuration owner and a repeatable no-change plan. An import receipt alone is not enough.
Frequently asked questions
Does importing the bucket adopt all its settings? No. Manage required facets explicitly and inventory policies, lifecycle rules and other settings outside this example.
Should the initial plan contain no changes at all? It should contain only intended imports. After adoption, review an ordinary plan for unintended infrastructure changes.
Can prevent_destroy protect a removed declaration? No. The rule must remain in the configuration to enforce its protection.
Can I import an object already owned by another state? Arrange a coordinated ownership transfer first; concurrent ownership can produce conflicting reconciliation.