AI-narrated article. Prose is preserved with pronunciation adjustments; code and image cues refer to the written article.
Terraform Drift Versus Unmanaged Infrastructure: Audit the Ownership Boundary
This is the AI-narrated article. The prose follows the written post; pronunciation is adjusted, and code and images are announced separately.
Separate Terraform attribute drift from inventory outside a state, then compare single-account S3 coverage without guessing ownership or applying changes.
TL;DR: Terraform drift and unmanaged infrastructure require different evidence. Ordinary plans inspect managed bindings and propose alignment with configuration; they do not inventory an entire account. Refresh-only reconciles records without changing the desired HCL contract. This article provides a complete read-only S3 review workflow and a Python comparison of paginated inventory with recursive state bindings. Results identify coverage candidates, not globally unowned resources. Account assumptions, provider aliases, incomplete exports and sensitive state require explicit controls. The workflow was statically reviewed without cloud execution, and recovery preserves ownership decisions before remediation, import or deletion.
A clean Terraform plan can coexist with an AWS account containing resources your platform team has never reviewed. Conversely, a resource outside one state may already be correctly managed by another workspace or controller. These are ownership-coverage questions, not simply drift alerts.
The distinction changes the response. Drift investigation asks whether an already managed object's observed settings differ from its declared contract. Inventory reconciliation asks which observed objects have an established controller, regardless of whether their settings look healthy.
This article builds a bounded S3 coverage review alongside normal and refresh-only plans. It supplies complete commands and a comparison script for an existing, audited single-account state. No AWS calls or Terraform plans were executed for the article, and no findings below are presented as measured account results.
Distinguish bindings, observations and desired configuration
Terraform state binds resource addresses to remote objects. Its ordinary plan reads managed objects and proposes actions to align them with configuration. It does not automatically enumerate every resource in the account and decide which should be managed. Terraform state, Planning behavior.
An ordinary plan's changes also need context: they can reflect a deliberate configuration edit, an external change, or both. Preserve the repository revision and review its diff before calling every proposed update drift. The JSON plan's resource_drift describes differences between refreshed observations and saved state; resource_changes describes proposed actions. Those are related evidence, not interchangeable fields. JSON plan representation.
Provider schemas, ignored attributes and inventory permissions constrain visibility. “No changes” is a statement about the evaluated configuration and observed scope, not a declaration that the whole account conforms to policy.
Read ownership coverage separately from drift
Image. Account inventory compared with this state's ownership boundary
The schematic places A and B inside this state's bindings: A is in sync and B has an attribute discrepancy. C is outside this state but belongs to another controller; D needs ownership investigation. The labels are illustrative, not discovered bucket names.
A resource outside this state is a candidate for reconciliation. Check other states, CloudFormation stacks, service-managed infrastructure and team ownership records before classifying it as globally unmanaged. Even a resource ultimately judged unmanaged may be legitimate; ownership decisions should precede import or deletion.
The same distinction applies within a bucket. A bucket resource binding does not prove ownership of its policy, lifecycle configuration, versioning or stored objects. The comparison below intentionally counts bucket bindings only.
Use refresh-only to reconcile records, not approve configuration
A refresh-only plan proposes updating Terraform's state and root outputs to reflect remote changes. It does not edit the HCL contract or propose restoring the infrastructure to that contract. Applying it, if authorized, updates records; an ordinary subsequent plan can still propose reversing a conflicting external change. Refresh-only workflow.
Do not use the absence of proposed infrastructure writes in a refresh-only plan as evidence that configuration drift has been accepted. Acceptance requires an explicit decision about the desired configuration. Similarly, turning off refresh or targeting a subset of resources narrows the evidence; neither is a sound shortcut for a comprehensive drift review.
HCP Terraform health assessments can provide configuration-drift detection within eligible workspaces. Its documentation distinguishes configuration drift from state drift and does not describe the feature as account-wide unmanaged-resource discovery. Health assessment scope.
Capture a bounded review without applying changes
Prerequisites are Terraform 1.16.0, Python 3.11, AWS CLI v2 and Bash. Use an existing initialized configuration with a committed lock file and an audited hashicorp/aws provider version; this example assumes 6.0.0. Preserve the selected backend, workspace, repository revision and ordinary input files.
The state must have been audited to manage general-purpose buckets in one known AWS account, including any provider aliases or assumed roles. The inventory profile must list that same account. The script's caller check validates inventory identity; the exported state does not independently prove which account every provider binding used.
Save the comparison code below as compare_s3.py. Then save this as review.sh, set an already authenticated approved AWS_PROFILE and EXPECTED_ACCOUNT, and run bash review.sh from the existing Terraform root:
Code example. Refer to the written article for the complete code.
The commands create local evidence and prospective plans, without applying them. Initialization may download dependencies; it must not select a different backend or workspace. The shell stops on failed commands rather than treating an unreadable state or denied inventory request as an empty result.
The bucket listing uses pagination without truncation or regional filtering. AWS documents that this operation lists general-purpose buckets owned by the authenticated sender and excludes directory buckets. It does not cover every resource service or cross-account resource accessible to the role. ListBuckets scope.
State JSON can reveal sensitive values in plain text. The restrictive directory permissions are a starting point, not a replacement for the organization's artifact controls. Restrict access to all exports and plan files. Terraform show.
Compare bucket membership without assigning ownership
Code example. Refer to the written article for the complete code.
The script walks child modules, selects managed bucket resources and compares their IDs with inventory names. It fails on an unsupported format, missing state structure, account mismatch, incomplete pagination or duplicate bucket binding. Data sources and bucket configuration facets are deliberately excluded.
The three output categories are conclusions about coverage only. An outside-state candidate needs an ownership lookup. A binding absent from inventory needs investigation into deletion, account assumptions or collection completeness. A binding present in inventory still needs its plan examined for settings drift.
The exports are not an atomic snapshot. Another operation can change state or infrastructure between reads. Coordinate the review window, retain collection timestamps and repeat collection if concurrent changes could affect a decision. Successful collection is evidence of this scope, not proof that all relevant accounts or controllers were included.
Recover from the false-clean review
A realistic failure is a console change to a managed bucket's public-access configuration followed by a refresh-only review. An operator sees no infrastructure correction in that mode and reports the drift resolved. The HCL still declares the previous settings; the next ordinary plan can propose changing them back.
Recovery is to inspect both observations and proposed actions, determine whether the external change was authorized, and either update the desired configuration through review or approve a fresh remediation plan. Do not apply either plan merely to silence a dashboard.
A second failure is treating every outside-state candidate as unowned and bulk importing it. Confirm another controller's ownership before adoption. Import establishes a state binding; it does not coordinate control between states. Import workflow.
Neither scenario was injected into a live account here. For an actual review, retain the discrepancy, affected binding, controller decision and approved next action. Cleanup removes only local review artifacts according to retention policy. It does not run destroy, remove state bindings or import candidates.
Frequently asked questions
Does a clean plan prove the AWS account has no unmanaged resources? No. It evaluates the selected configuration and managed scope; inventory and ownership reconciliation are separate work.
Does refresh-only accept a conflicting console change? It can update records when applied, but it does not change the desired HCL configuration.
Is every bucket outside this state unmanaged? No. It may belong to another state, controller or legitimate service boundary.
Does the comparison detect policy or versioning drift? No. It compares bucket membership; inspect the relevant managed facets and plans separately.