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 Cloud Control 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.

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 Cloud Control 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 Cloud Control 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. Cloud Control 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

    DELETE /api/terraform/workspaces/[id]/resources/[nativeId] unmanages the resource (it drops back to the unmanaged list) and recompiles the folder. On the Terraform screen the workspace lists its members with a per-member Remove + a Recompile action.

Conflicts are surfaced, never silently overwritten

If two members compile to the same Terraform address (e.g. two resources with the same name), Cloud Control 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 — or from the Network map (tap a managed node → Edit settings in the inspector) — 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.

  1. Edit from Inventory or the Network map

    Click Edit on a managed Inventory row, or tap a managed node on the Network topology and choose Edit settings. Change the typed fields (tags as key=value rows; route tables get a routes list editor with add/remove per row) and submit. PATCH /api/terraform/workspaces/[id]/resources/[nativeId] { edits } validates the edit against the allowlist, applies it to the resource's attributes, and recompiles the folder.

  2. Review the plan

    The recompile opens a planned run whose diff shows the change (~1 to change), with the policy result (no-public-bucket, region-allowlist) and the Δ$/mo cost estimate. A policy violation (e.g. making a bucket public) blocks approval.

  3. Approve & apply

    Approve and apply via the normal run pipeline — self-approve works on a standard folder; a production folder needs a second operator. 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 (an applied route change flips the reachability verdict, e.g. removing a public subnet's 0.0.0.0/0 → igw-… route makes instance→internet not reachable at the route layer). A failed apply leaves state untouched and the resource editable.

  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

Apply is the mock runner this round

Import generates the correct real HCL + import block and commits it to your connected repo, and marks the resource managed in the platform model — but it does not yet run a real terraform import/apply against your live cloud with assumed-role credentials. That lands with the real-runner round. Today "managed" = adopted in-platform + HCL in the repo; the real runner will add the live terraform import + ongoing plan/apply against the actual resource.

  • Codegen coverage. The types Cloud Control 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/ alongside a versions.tf.json (required providers) and provider.tf.json (region; credentials from your environment) with no backend block — so terraform -chdir=terraform/imported init && apply runs it outside Cloud Control 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.json 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).