Skip to main content
Use this guide to bring infrastructure that already exists — a production S3 bucket, an RDS database, a VPC, or any other resource a Terraform provider can import — under the management of a Ravion module without destroying or recreating anything. It works for standard library modules and custom module definitions alike.
This page covers modules where the definition owns the Terraform source. If you’re importing resources into your own Terraform code running through an rvn-stack module, see Import into a Terraform Stack — it has a simpler path using import blocks.

How it works

  1. Applying a project config file that adds a stack-backed module creates the instance, its managed state backend workspace, and starts a change pipeline run that stops at the approval gate.
  2. That run’s plan enumerates exactly which resource addresses the module manages, so you know what to import. You cancel the run instead of approving it.
  3. ravion terraform shell wires your local terraform/tofu to the managed workspace, so terraform import writes directly into the module’s state. Import only needs stub resource blocks locally — not the module’s real Terraform source.
  4. Applying the config again plans against the imported state. When your module inputs match the real infrastructure, the plan shows no creates — only benign in-place updates such as tags.

Prerequisites

  • The Ravion CLI installed and authenticated — verify with ravion whoami
  • Local credentials for the provider that owns the resources — for AWS, aws sts get-caller-identity must return the same account that’s connected to Ravion (compare with ravion aws account list --json)
  • OpenTofu (or Terraform, matching the module’s tool) installed locally

Use an agent

The import flow is mechanical but detail-heavy: reading resource addresses from the plan, inspecting real resource settings, matching module inputs, mapping addresses to import IDs. A coding agent with access to your provider’s CLI handles it well. The steps are the manual walkthrough below — this prompt points your agent at it and sets the guardrails:

Create the module, import existing infrastructure into it, and verify a clean plan.

Open in Cursor

Manual walkthrough

The example below imports an existing AWS RDS PostgreSQL instance into an rvn-rds module. The same flow applies to any module and any provider — only the resource inspection commands and import IDs change.
1

Add the module to your project config and apply

Pull your project config if you don’t already manage it in source control:
Add the module instance. The inputs that identify the existing infrastructure — here the DB instance identifier (name), engine, region, and account — must match it exactly, since many are immutable after the first apply. Best-guess the rest; you’ll refine them after seeing the plan. Check the schema with ravion module schema rvn-rds.Since the existing database lives in a VPC that Ravion doesn’t manage, set network: null and pass the VPC and subnets directly. (If the VPC is already an rvn-aws-network module, reference it instead: network: {moduleGivenIdRef: <module-given-id>}.)
ravion.yaml
Preview, then apply — without --autoapprove:
The apply creates the module, its managed state workspace, and starts a change run that stops at the approval gate. Nothing is provisioned until a plan is approved.
Prefer managing modules through the config file. For a one-off without config as code, ravion module create --initial-stack-run PLAN gets you to the same place.
2

Read the resource addresses from the plan

The run that just started tells you exactly which resource addresses the module manages:
Example output
Each CREATE row is a resource to import. Cancel the run — you’re importing instead of letting it create anything, and its plan becomes stale the moment you do:
3

Match module inputs to the real resource

Inspect the existing resource and record every setting the module’s inputs cover:
For RDS, describe-db-instances covers most inputs in one call: engine and version, instance class, allocated storage, storage type, Multi-AZ, backup retention, Performance Insights, CloudWatch log exports, deletion protection, and the attached subnet group, parameter group, and security groups.Update the module’s inputs in the config file so they match reality — the closer the match, the cleaner the post-import plan. If the real resource has a setting the module’s inputs can’t express, note it and decide whether you can live without it before continuing. Don’t apply yet; the apply after importing doubles as your verification run.
4

Write a stub config for the import

terraform import requires each address to exist in configuration, but it doesn’t validate resource arguments — so empty stub blocks are enough. You don’t need the module’s real Terraform source. In a scratch directory, write one stub per address from the plan, mirroring the address shapes exactly:
  • An indexed address like aws_db_parameter_group.this[0] needs count = 1 on its stub.
  • A keyed address like ...egress_rule.this["0"] needs a matching for_each.
  • Addresses under module.security_group[0] need a stub child module with the same name.
main.tf
security_group/main.tf
If you want to read the module’s real source — for import ID formats or to understand conditional resources — the bottom of ravion module schema <type> links it. For custom module definitions, the repo, ref, and base_path are in the stack config from ravion module version get <module-version-id> --json. Viewing it is enough; there’s no need to clone it.
5

Import into the managed workspace

From the stub config directory, get the workspace name from the stack, then initialize and import. ravion terraform shell wires TF_CLOUD_* and token env vars so tofu talks directly to Ravion’s state backend:
Import IDs vary by resource type — the DB instance, parameter group, and subnet group import by name, while the security group and its egress rule import by AWS-assigned ID (sg-..., sgr-...). Each provider’s docs list the format in the resource’s Import section. Single-quote addresses containing [ or " so the shell passes them through intact. Export whatever credentials and settings the provider needs (here, AWS credentials and the region).
6

Apply again and verify the plan

Apply the config file with your refined inputs — the run it starts is your verification plan:
If the config file didn’t change since the first apply, start the run manually instead:
Inspect the plan:
A successful import shows no CREATE rows. Expect:
  • NO_OP for resources whose inputs match exactly.
  • An in-place UPDATE on the main resource for tags — Ravion merges standard ownership tags (Owner, ProjectGivenId, EnvironmentGivenId, ModuleGivenId, ModuleId) on top of yours. This is expected and safe.
Drill into any UPDATE to confirm it’s benign:
If the plan shows CREATE, DELETE, or REPLACE, your module inputs don’t match the real infrastructure. Cancel the run, fix the inputs in the config file, apply, and re-check.When the plan looks right, approve it in the dashboard or via the CLI:
After the apply succeeds, the resource is fully managed by the module — future config file changes flow through the normal change pipeline.

Tips

  • One module instance per pass: a module usually manages several Terraform resources — an RDS module also manages the parameter group, DB subnet group, and security group — but unrelated infrastructure belongs in separate module instances. Repeat the flow for each.
  • Immutable inputs: inputs marked immutable in the schema (like the DB instance identifier, engine, and region) can’t change after the first apply, so get them right in the config file before importing.
  • Conditional resources: some resources only exist when an input enables them. In rvn-rds, the parameter group and security group exist because parameter_group_creation_enabled and security_group_creation_enabled default to true — that’s why their addresses are indexed with [0]. Match the inputs to reality: if the real database uses an existing security group, disable creation, pass its ID instead, and skip those imports.
  • Data sources (like data.aws_vpc.this and data.aws_caller_identity.current here) appear in state list output alongside resources; they don’t need importing.
  • Optional sub-resources you don’t have: if the plan wants to CREATE a sub-resource the real infrastructure genuinely lacks (rather than one you forgot to import), letting the apply create it is usually fine — review the diff to be sure.

Next steps

Import into a Terraform Stack

Importing into your own Terraform code? Import blocks make it much simpler.

Project config file

Manage your project’s environments and modules declaratively from source control.