Skip to content

Terraform Import for a Manually Created Bedrock Resource

10 min read · updated August 11, 2026

Somebody built a guardrail in the console during an incident, and it is now in the path of production traffic. You need it in Terraform and you cannot let Terraform destroy it. The mechanics are ordinary; the identifiers are not, and that is what makes this fiddly.

Why recreating is not an option here

For most resources, delete-and-recreate is an ugly but available fallback. Several Bedrock resources make it genuinely unsafe.

A guardrail has an ARN and a version, and those are referenced by running application code and by agents. Recreating it changes the ID, which breaks every caller that named it — and does so at the moment the guardrail is briefly absent, meaning unfiltered traffic passes in the gap. Provisioned model throughput is worse: it represents purchased capacity, potentially under a commitment term that AWS documents as OneMonth or SixMonths, and destroying it does not refund the commitment while recreating it may not succeed immediately if capacity is unavailable.

A custom model is the extreme case — it is the output of a customisation job that consumed real training time and money, and it cannot be recreated at all without rerunning that job.

The identifiers, which are not uniform

There is no single Bedrock import ID convention, and this is the part worth getting from the provider documentation rather than guessing. Two concrete examples from the AWS provider’s own resource documentation:

  • aws_bedrock_guardrail imports on a comma-delimited string of the guardrail ID and the version — the documented example is guardrail-id-12345678,DRAFT. Not an ARN, and the version is mandatory.
  • aws_bedrock_provisioned_model_throughput imports on the provisioned model ARN, of the form arn:aws:bedrock:us-west-2:123456789012:provisioned-model/1y5n57gh5y2e.

Passing the ARN where the comma-delimited form is expected produces a confusing not-found error rather than a helpful one, because the provider parses before it calls. Read the Import section of the specific resource page — the guardrail resource documentation is the model for what to look for — and do not assume the neighbouring resource works the same way.

The set of Bedrock resources in the AWS provider has grown quickly and continues to. If the thing you want to import has no resource type, import is not the blocker — coverage is. Check the provider changelog before designing around a resource you have not confirmed exists.

Generating the configuration

Use import blocks rather than the terraform import command. The block is code, it survives in version control, and it can generate the configuration for you.

# imports.tf
import {
  to = aws_bedrock_guardrail.pii
  id = "abcd1234efgh,DRAFT"
}
  1. Write the import block with no corresponding resource block.
  2. Run terraform plan -generate-config-out=generated_guardrail.tf. The path must be a file that does not yet exist, or Terraform errors.
  3. Read the generated file properly. HashiCorp documents this feature as experimental and warns that generated configuration may contain conflicting arguments — its own example is a resource with two mutually exclusive attributes both populated, which must be resolved by hand before the configuration will apply.
  4. Delete the noise. Generation emits every attribute, including computed ones and empty blocks. A guardrail generates large nested content_policy_config and sensitive_information_policy_config blocks that are far easier to read once the defaults are stripped.
  5. Run terraform plan again and iterate until it reports no changes. That, not a successful import, is the finish line.

Do all of this on a branch with the state lock in mind — an import is a state write like any other, and running it against shared state while a colleague applies is the exact scenario state locking prevents.

Import by identity

Terraform 1.12 added an identity argument to import blocks: a structured object of key-value pairs instead of a single string. Providers opt in per resource, and the AWS provider documents the identity form for Bedrock resources alongside the ID form. The provisioned throughput example from the provider documentation reads:

import {
  to = aws_bedrock_provisioned_model_throughput.example
  identity = {
    "provisioned_model_arn" = "arn:aws:bedrock:us-west-2:123456789012:provisioned-model/a1b2c3d4567890ab"
  }
}

Two rules. You cannot use identity and id in the same import block. And identity support is per-resource, so the fact that one Bedrock resource accepts it says nothing about the next one — check the documentation for each. Where both are available, identity is worth preferring: the key names are self-documenting, and a multi-part identifier stops being a comma-separated string whose field order you have to remember.

The diff after the import

A plan that is not empty after import usually means one of four things, and only one of them is a mistake in your configuration.

  • Default-valued arguments. The console set something you did not declare. Declare it, or accept the change if it is genuinely wrong.
  • Tags. A provider-level default_tags block will want to add tags to a console-created resource. That is a real change and usually a desirable one — apply it deliberately, not by accident inside a larger plan.
  • Versioned sub-resources. A guardrail imported at DRAFT is the working copy, not a published version. If production references a numbered version, importing DRAFT and applying can change what DRAFT contains without touching what production uses — which looks safe and is not, because the next publish inherits it.
  • Ordering inside sets. Nested policy blocks may come back in a different order than you wrote them, producing a diff with no semantic content. Reorder your configuration to match rather than adding ignore_changes, which would hide real drift later.

Once the plan is clean, delete the import block. Leaving it in place is harmless for a while — Terraform treats an already-imported resource as a no-op — but it becomes a confusing artefact, and the next person to read the repository cannot tell whether it did anything.

Undoing an import, and the block that is not import

Sooner or later you import the wrong thing — the wrong guardrail version, or a resource that turns out to belong in another configuration. The reflex is to delete the resource block, and that is precisely the wrong move: Terraform reads a removed resource block as an instruction to destroy the thing, which on a live guardrail is the outcome the whole page exists to avoid.

What you want is to forget the resource without touching it. Terraform 1.7 added a removed block for exactly this, and it is declarative in the same way the import block is:

removed {
  from = aws_bedrock_guardrail.pii

  lifecycle {
    destroy = false
  }
}

destroy = false is the load-bearing line. It tells Terraform to drop the resource from state and leave the real guardrail running. Delete the resource block and the removed block together, apply, then delete the removed block in a follow-up commit. The older imperative equivalent is terraform state rm, which does the same thing without leaving a record in version control — fine at a terminal, worse in a repository somebody will read in six months.

The adjacent block worth knowing is moved, which handles the case people reach for import to solve and should not. If a resource is already in state and you are only renaming it — refactoring aws_bedrock_guardrail.pii into a module, say — a moved block rewrites the address in state with no API calls at all:

moved {
  from = aws_bedrock_guardrail.pii
  to   = module.guardrails.aws_bedrock_guardrail.pii
}

Doing that with import instead means removing from state and importing back, which is two applies and a window in which the resource is unmanaged. Use moved for anything Terraform already knows about and import only for things it has never seen.

A last caution about scope. It is tempting to import the whole Bedrock estate in one plan — every guardrail, the provisioned throughput, the knowledge base — because the import blocks are cheap to write. Resist it. A single plan mixing eight imports with generated configuration produces a diff nobody can review, and one wrong identifier fails the whole apply. Import one resource, reach an empty plan, commit, and move to the next. It is slower and it is the version where you can tell what happened.