Docs
Open the console →
Guides

Import existing resources

Adopt resources you already have in your connected clouds into Terraform management: generated HCL + an import block, committed to your connected repo, marked managed, and shown in the topology diagram.

Most cloud estates already exist before you connect them. Import brings a resource that Cushy discovered during sync under Terraform management without recreating it: it generates real HCL for the resource plus a Terraform 1.5+ import block that binds the config to the existing cloud object, records the adopt in the platform model, marks the resource managed, and best-effort commits the HCL to your connected Git repository.

Where to start

Open the Import screen from the sidebar, or the Import existing resource entry on the Terraform screen. The screen lists every discovered resource in your connected accounts that isn't managed yet (type, id, region, provider, status). Select one or more and click Import. You need the mutate_infra capability (Deployer and above) — a read-only role sees the list but can't import.

  1. Select unmanaged resources

    The list is fetched from GET /api/cloud/resources (the ones with managed:false). Tick the resources you want to adopt.

  2. Choose the target folder

    Next to the Import button, the into selector picks which Terraform folder (workspace) the selected resources are adopted into. Leave it on Auto for the backward-compatible per-cloud/region imported-<provider>-<region> folder, or pick an existing folder to group the imports there. A chosen folder has one provider — importing a mismatched-provider resource into it is refused with a clear message, so pick a matching folder or Auto. (For a single resource, the per-row ⚙ Terraform button also offers a brand-new blank folder.)

  3. Import

    POST /api/terraform/import with { nativeId, targetWorkspaceId? } per resource (targetWorkspaceId omitted = Auto). The engine reads the discovered synced row, generates HCL from its real attributes, and adds a import { to, id } block.

  4. Review the result

    Each import reports the generated address (e.g. aws_vpc.prod-use1), the target folder (workspace), the state version, and — if a repo is connected — the committed file path + commit ref. The resource is now managed and drops off the unmanaged list.

Recreate after a manual console deletion

An imported resource's generated Terraform is complete enough to redeploy it. Discovery captures the create-critical attributes (an EC2 instance's real AMI, key pair, subnet and security groups; an RDS instance's engine, version, class, storage and encryption; a Lambda's runtime, handler, memory, timeout and role), and the generated config uses them — a REPLACE_ME placeholder appears only when an attribute is genuinely undiscoverable (an RDS master password, a Lambda code artifact), and every such gap is listed in the import result as "review before a recreate" so fidelity is honest, never assumed.

The import block has a lifecycle: once a member is in the workspace's applied state, its import block is omitted from the compiled module — so if the resource is deleted manually in the cloud console, the next Recompile detects the deleted member and opens a planned recreate run (diff +1, policy + cost evaluated) that you approve and apply through the normal pipeline. With provisioning enabled, that apply genuinely recreates the resource in your cloud (the provider assigns a new native id). The standalone repo artifact never carries import blocks — its purpose is recovery, where a plain init && apply recreates everything.

What "managed" means today

After import the resource is adopted in the platform model: a Terraform workspace owns it, an encrypted state version records it, the generated HCL is stored (and committed to your repo when one is connected), and the resource is flagged managed everywhere — including a TF badge on its node in the topology diagram. The discovered attributes are preserved, so the resource keeps rendering unchanged in Inventory and the network map.

Type coverage

The types Cushy actually syncs have first-class Terraform mappings, populated from the discovered attributes:

Discovered typeAWSGCPAttributes populated
networkaws_vpcgoogle_compute_networkcidr_block / name
subnetaws_subnetgoogle_compute_subnetworkcidr, vpc/network id, zone/region
computeaws_instancegoogle_compute_instanceinstance/machine type, zone (ami is a placeholder)
storageaws_s3_bucketgoogle_storage_bucketbucket / name, location
kubernetes / database / loadbalancer / serverlessaws_eks_cluster / aws_db_instance / aws_lb / aws_lambda_functiongoogle_container_cluster / google_sql_database_instance / …discovered attributes carried into a best-effort block
internetgateway / natgateway / vpcendpoint / routetable / tgwattachmentaws_internet_gateway / aws_nat_gateway / aws_vpc_endpoint / aws_route_table / aws_ec2_transit_gateway_vpc_attachmentthe network plumbing nodes from the dependency graph — vpc id, service name, subnet, route-table id (some ids are placeholders)
iam_role / iam_policy / iam_user / iam_groupaws_iam_role / aws_iam_policy / aws_iam_user / aws_iam_groupname, path, arn; roles carry the discovered assume-role policy (policy documents are a placeholder)

Azure (azurerm_*) and Alibaba (alicloud_*) have the same mapping table and their sync adapters are live, so their discovered resources import as first-class too. Any service without a first-class mapping still imports via a best-effort generic block (a <provider>_<type> guess with the discovered attributes carried verbatim and a #note) plus the import block, so you can adopt anything discovery finds — you refine that generic block before a real apply.

IAM and other global services

IAM is global (not region-scoped), so Cushy discovers it once and lists it with the region global. Discovery is customer-managed only: it enumerates your ListRoles, ListPolicies (scope Local — AWS-managed policies are not listed), ListUsers and ListGroups, and it excludes AWS service-linked roles (/aws-service-role/…) to cut noise. IAM roles, policies, users and groups appear in Inventory and on the Import screen just like any other resource, and import to aws_iam_role / aws_iam_policy / aws_iam_user / aws_iam_group.

The import id differs per IAM entity

Terraform imports IAM roles, users and groups by their name (not their ARN), and a managed policy by its ARN. Cushy emits the correct id in the generated import block automatically for each type.

IAM discovery needs read permission — and degrades gracefully without it

The connector role needs iam:ListRoles, iam:ListPolicies, iam:ListUsers and iam:ListGroups (all covered by the AWS-managed IAMReadOnlyAccess, or SecurityAudit / ReadOnlyAccess). If a permission is missing, IAM discovery for that call simply returns nothing — it never fails the rest of the sync sweep, and the account stays healthy.

A workspace is a folder of resources

A Terraform workspace is a folder/group of member resources — not a single resource. Every resource whose folder is this workspace is compiled together into one workspace module (each member's resource block + its import block, merged). You manage membership from the Inventory page: each row shows its Terraform folder, and a control per row lets you add an unmanaged resource to a folder or move a managed one to a different folder (or a new blank folder created inline).

  1. Add / move from Inventory

    Click ⚙ Add to folder (unmanaged) or Move (managed) on a resource row, pick an existing folder or + New blank folder, and confirm. POST /api/terraform/workspaces/[id]/resources { nativeId } assigns/moves the resource INTO that folder.

  2. The folder recompiles

    The target folder recompiles its module from ALL its members; a move also recompiles the previous folder (the moved-away resource shows as a delete there). You see the recompiled diff (+add / −remove) and any conflicts to review — Keep or Undo.

  3. Remove from a folder (unmanage)

    DELETE /api/terraform/workspaces/[id]/resources/[nativeId] unmanages the resource (it drops back to the unmanaged list) and recompiles the folder — the cloud resource itself is kept. On the Terraform screen the workspace lists its members with a per-member Remove + a Recompile action; on Inventory the row's ✕ Unmanage does the same.

  4. Destroy ONE member resource

    The same route with ?destroy=1 instead destroys that one cloud resource through the engine: the folder module is recompiled WITHOUT the member and a PLANNED run opens whose diff shows the resource as a delete (−1, with the monthly savings as a negative Δcost). The normal approve/apply finishes it — only a successful apply removes the resource from Inventory and from the folder; discarding the plan restores everything. On Inventory this is the row's Destroy… (typed-name confirm); the rest of the folder is untouched and the workspace stays active.

Three remove semantics on the Inventory row

A draft (planned/failed phantom) row is ✕ Removed directly — nothing exists in the cloud. A live adopted member offers two clearly-labeled options: Destroy… (tears down THAT one resource via plan → approve → apply) and ✕ Unmanage (folder-only removal, cloud resource kept). A live blueprint-folder member is destroyed via its folder (typed folder-name confirm) — the blueprint's variables define it, so the folder is the teardown unit. A draft answers 400 to ?destroy=1 (use Remove) and a blueprint member answers 409 (destroy the folder or edit its variables).

Conflicts are surfaced, never silently overwritten

If two members compile to the same Terraform address (e.g. two resources with the same name), Cushy deduplicates the local names so the module stays valid AND flags the collision as a conflict. A conflicting compile is not committed — you review the drift and rename one resource before it lands. Recompile once the collision is resolved to commit the folder's HCL to the connected repo.

Edit a resource's settings

A managed resource (a member of a Terraform folder) can have its settings edited straight from Inventory — just click the row — or from the Network map — just tap the node. There is no separate Edit button: clicking a managed, live resource of an editable type opens the parameter editor directly, and one Save & apply button carries the change through. The edit flows through the same plan → policy/cost → approve → apply pipeline as everything else (one mutation path). Each resource type exposes a small allowlisted set of editable settings; everything else is read-only this round: compute instanceType/machineType · storage public/storageClass · network & subnet cidr · database engineVersion/multiAz/ha · loadbalancer scheme/tier · serverless runtime/memoryMb · kubernetes version · route table routes (a full-replacement list of destinationtarget entries — destination an IPv4 CIDR or local, target a gateway/endpoint id like igw-…/nat-… or local, up to 50) · internet/NAT gateway, VPC endpoint, TGW attachment tags only — plus tags on every editable type. (Unmanaged resources, drafts, non-editable types, and read-only Viewers open the read-only detail view instead.)

  1. Open the parameter page

    Click a managed Inventory row, or tap a managed node on the Network topology — the parameter editor opens directly (no intermediate detail, no Edit button). Change the typed fields (tags as key=value rows; route tables get a routes list editor with add/remove per row).

  2. Save & apply

    Press the single Save & apply button. It plans the change (PATCH /api/terraform/workspaces/[id]/resources/[nativeId] { edits } validates against the allowlist, applies it to the resource's attributes, and recompiles the folder), shows you the diff (~1 to change) + the policy result (no-public-bucket, region-allowlist) + the Δ$/mo estimate + a REAL-vs-recorded banner as a brief confirm, and — on a standard folder whose policy passes — automatically approves and applies it. One click, applied.

  3. Production & blocked cases

    A production folder can't self-apply: Save plans the change and leaves it awaiting a second operator's approval on the Terraform screen (the run stays planned). A policy violation (e.g. making a bucket public) stops at the review with the violation — it never auto-applies a failing plan. A failed apply shows an honest error and leaves your values editable to adjust and re-plan. On success the edited settings ARE the resource's settings — every page re-reads them: the Inventory row, the topology node's detail (a route table's route rows), the Terraform folder's compiled module, and network reachability (removing a public subnet's 0.0.0.0/0 → igw-… route makes instance→internet not reachable at the route layer).

  4. Unmanaged resources

    An unmanaged resource can't be edited — add it to a folder first (⚙ Add to folder in Inventory; the Network inspector shows the same hint).

Edits stage immediately — sync may refresh them before apply

A submitted edit is written to the resource's attributes right away (the folder module compiles from them). If a sync sweep runs before you apply, discovery refreshes the attributes and can overwrite a staged edit — re-open the editor and re-plan (the diff always shows the current module vs state). A discarded edit run likewise leaves the staged values until you re-edit them or the next sweep restores the discovered ones. After a real apply, the next sweep re-discovers the actual cloud value — which is the truth.

Limitations

The adopt itself never touches your cloud

Import generates the correct real HCL + import block, commits it to your connected repo, and marks the resource managed in the platform model — the adopt is a zero-change recording, never a cloud mutation. Subsequent plans/applies on the folder are recorded-only by default, or real OpenTofu once the account has provisioning enabled (see *Real vs recorded deploys* in the Terraform guide).

  • Codegen coverage. The types Cushy syncs (network, subnet, compute, storage, kubernetes, database, loadbalancer, serverless, the network plumbing, and IAM roles/policies/users/groups) have first-class mappings. Any other service still imports via a documented generic fallback block that needs manual completion before a real apply.
  • Attributes are best-effort. The generated HCL carries the attributes discovery captured (cidr, vpc/network id, instance/machine type, zone). Fields discovery doesn't collect (e.g. an EC2 ami) are emitted as REPLACE_ME placeholders. Review the HCL before a real apply.
  • The committed config is standalone-applyable. Each import is committed to terraform/imported/ as native HCL alongside a versions.tf (required providers) and provider.tf (region; credentials from your environment) with no backend block — so terraform -chdir=terraform/imported init && apply runs it outside Cushy on the LOCAL backend (a portable copy). Because imported attributes are best-effort, review any `REPLACE_ME` / `//`-noted placeholders before a real apply.
  • Repo commit requires a connected repo with write access. The commit is best-effort: if no Git repo is connected (or the token is read-only / lacks Contents: write), the resource is still adopted in-platform and the HCL is stored on the workspace, but nothing is committed. Connect a repo from the Git screen to enable the commit.
  • Network diagram shows the real dependency graph. The topology diagram draws the discovered VPC/subnet nodes, the network plumbing (internet gateways, NAT gateways, VPC endpoints, route tables, transit-gateway attachments), their dependency edges (associations, routes, attachments) and the real synced_links (peering / VPN / transit-gateway). Traffic volume is overlaid when VPC Flow Logs are enabled and ingested; where they are not, the map shows the dependency graph *without* traffic and says so — it never invents flows.
  • Folders compile to one module. A workspace's stored main.tf is the folder module compiled from ALL its members (committed to terraform/workspace-<id>/). The bulk Import screen still groups auto-imports into an imported-<provider>-<region> workspace, and you can then move those resources into any folder from Inventory.
The managed marker survives re-sync

Marking a resource managed is a dedicated field the sync engine never overwrites, so a later inventory sync keeps the resource managed (it won't reappear as unmanaged).