Terragrunt Import lets you bring brownfield infrastructure under Terraform control across multi-repo and multi-account setups. Done right, you’ll avoid state drift, unstable addresses, and risky access patterns.

The goal is a reproducible, auditable workflow with clean plans and minimal permissions. Use a consistent remote state, pin tooling versions, and validate every step in CI. 

At a Glance: Terragrunt Import Best Practices

  • Standardize remote state and lock it
  • Pin Terraform, providers, and Terragrunt versions
  • Document intent with Terraform import blocks
  • Automate plans and halt on drift or diffs
  • Use least-privilege, short-lived credentials

Do: Prepare State, Providers, and Repo for Safe Terragrunt Import

Use a remote backend with locking and encryption (e.g., S3/GCS/Azure Blob) and inherit backend config via a root terragrunt.hcl. This prevents divergent state and protects you from concurrent writes.

Pin Terraform, provider, and Terragrunt versions; run terraform init -upgrade only in controlled windows, and validate in CI with terraform validate and terraform plan -detailed-exitcode gates.

Preflight with snapshots: enable bucket/container versioning and take a state backup before each import; start with a read-only discovery run.

Two-sentence mini-story: An engineer imported dozens of resources on a laptop with a newer provider than CI. The next pipeline showed a wall of “changes,” all caused by version drift; pinning would have caught this earlier.

Do: Use Import Blocks and Hooks for Terragrunt Import With Clear, Stable Addresses

Prefer Terraform 1.5+ import blocks to document import intent in code and keep resource addresses stable across runs. Combine with Terragrunt before/after hooks to generate import IDs, run a plan immediately after import, and fail on any nonzero exit or unwanted diff. Start with a skeleton HCL: declare essential arguments only; add lifecycle ignore_changes for noisy attributes until parity is verified. Caveat: import blocks require Terraform 1.5 or later.

Canonical snippet (HCL):

```hcl

# modules/storage/main.tf

resource "aws_s3_bucket" "logs" {

  bucket = var.bucket_name

  lifecycle {

    ignore_changes = [tags]

  }

}

# modules/storage/import.tf (Terraform ≥ 1.5)

import {

  to = aws_s3_bucket.logs

  id = "my-company-logs"

}

# live/prod/storage/terragrunt.hcl

terraform {

  source = "../../../modules/storage"

}

inputs = {

  bucket_name = "my-company-logs"

}

# Optional Terragrunt hook to halt on drift after import

hook "after_import_plan" {

  commands = ["import"]

  execute  = ["bash", "-lc", "terraform plan -detailed-exitcode || exit 1"]

}

Reference the import block snippet above in each environment and keep module paths stable.

Don’t: Change Module Structure Mid-Import or Apply Without a Clean Plan

Don’t refactor module names, move modules, or rename resources during a Terragrunt Import; that changes addresses and breaks state mapping. Never apply after an import unless the plan shows no unintended creates or destroys; enforce this with -detailed-exitcode in CI. If you discover an address mismatch, fix it with terraform state mv rather than re-importing or editing state by hand. Caveat: state mv operations should be reviewed in PRs and run from the same pinned toolchain as your plans.

Do: Enforce Least-Privilege and Short-Lived Access for Terragrunt Import

Use assume-role (or equivalent) with external IDs/MFA and short sessions, scoped to import-only APIs for the target services. Separate read-only discovery from write operations, rotate credentials, and store secrets in CI securely. Keep audit trails: confirm who imported what and when using provider logs (e.g., CloudTrail, Activity Logs, Audit Logs). Caveat: provider logs can lag; keep local CI run metadata for cross-checks.

Example: Importing a Storage Bucket (Concepts Apply Across Clouds)

Create a minimal resource block and keep the terragrunt.hcl path stable. Add a Terraform 1.5 import block with the bucket’s canonical ID. Run terraform init, then nudge Terragrunt to run a plan: terragrunt run-all plan -detailed-exitcode. The plan should show “no changes” except legitimate drift. If noise appears (e.g., tags, server-generated fields), add temporary lifecycle ignore_changes as in the snippet above, reconcile configuration to match reality, then remove ignores once parity is achieved. Finally, commit the import block and configuration together so future plans remain clean.

For syntax specifics, see Terraform’s Import documentation (Terraform import language reference).

## Bring It Together With Guardrails

A disciplined Terragrunt Import flow yields reproducible, auditable results with clean plans and least-privilege access. Codify intent with import blocks, keep addresses stable, and block applies on drift. ControlMonkey can accelerate this with discovery, safe sequencing, and policy guardrails. Request a demo to see it in action.

Bottom CTA Background

A 30-min meeting will save your team 1000s of hours

A 30-min meeting will save your team 1000s of hours

Book Intro Call

Author

Daniel Alfasi

Daniel Alfasi

Backend Developer and AI Researcher

Backend Developer at ControlMonkey, passionate about Terraform, Terragrunt, and AI. With a strong computer science background and Dean’s List recognition, Daniel is driven to build smarter, automated cloud infrastructure and explore the future of intelligent DevOps systems.

    Sounds Interesting?

    Request a Demo