Skip to Content

Infrastructure-as-Code Import & Export

Beyond live MCP discovery (see MCP Integration), Strata imports declared infrastructure from existing Infrastructure-as-Code into the same InfrastructureGraph that the canvas renders, across AWS, GCP and Azure. The shared engine is src/aws/iac.ts — pure and dependency-light (only js-yaml, for CloudFormation YAML) — and the provider adapters (src/gcp/iac.ts, src/azure/iac.ts) reuse its exported buildGraph / importTerraform. Supported formats:

  • CloudFormation templates (JSON or YAML) — AWS.
  • Azure ARM templates (JSON, the typed resources[] array).
  • Terraform (and OpenTofu) terraform show -json / plan -json (or tofu show -json) for the aws, google, and azurerm providers (a single state file may mix them). OpenTofu is a Terraform fork emitting the identical JSON schema, so it flows through importTerraform unchanged — which is why the import/export UI labels read “Terraform / OpenTofu” and the file pickers accept .tofu.

The unified entry point importAnyIaC(content, opts?) (src/lib/importIac.ts) auto-detects format/provider and routes to the right importer; it lives in lib/ to avoid a cycle (the provider adapters import the shared engine). The “Import IaC” dialog calls it.

Entry points

FunctionPurpose
importAnyIaC(content, opts?)Multi-cloud entry point (src/lib/importIac.ts). Detects ARM / CFN / Terraform and routes; uses a merged aws_*+google_*+azurerm_* TF type map.
importIaC(content, opts?)AWS string entry point. Parses JSON/YAML, then routes by opts.format or auto-detects.
importCloudFormation(template, name?)Per-format importer for an already-parsed CloudFormation object.
importTerraform(tf, name?, opts?)Shared Terraform importer; opts.typeMap / opts.containmentKeys parameterise it per provider.
importArm(template, name?)Azure ARM template importer (src/azure/iac.ts).
importGcpTerraform / importAzureTerraformProvider wrappers over importTerraform with the GCP / Azure type maps.
detectFormat(doc)Heuristic classifier returning "cloudformation" or "terraform" (ARM is detected separately by isArmTemplate).

importIaC first parses the document (JSON or YAML), then — unless opts.format forces a format — calls detectFormat(doc), which classifies the parsed object as:

  • "cloudformation" when it has a top-level Resources map or an AWSTemplateFormatVersion, or
  • "terraform" when it has values, planned_values, resource_changes, terraform_version, or format_version.

All importers return an IacImportResult:

interface IacImportResult { graph: InfrastructureGraph; format: IacFormat; // "cloudformation" | "terraform" | "arm" unmappedTypes: string[]; // source types with no registry mapping warnings: string[]; // skipped resources, ambiguities, … }

Join keys

The two formats differ only in how a source resource is matched to a registry ServiceDefinition:

  • CloudFormation / ARM: a resource’s type ("AWS::EC2::Instance" / "Microsoft.Compute/virtualMachines") is the registry nativeType, so getServiceByNativeType(provider, type) resolves it directly — the same join key used by live discovery. (getServiceByCfnType remains as the AWS wrapper.)
  • Terraform: HCL carries no native type, so each provider ships a join table — TF_TYPE_TO_SERVICE_ID (AWS), GCP_TF_TYPE_TO_SERVICE_ID, AZURE_TF_TYPE_TO_SERVICE_ID (Record<tfType, serviceId>). importAnyIaC merges all three (the aws_*/google_*/azurerm_* prefixes are disjoint, so no collisions). Each is exported and extended as the catalog grows.

YAML short-tag handling

CloudFormation YAML uses short-form intrinsic tags (!Ref, !GetAtt, !Sub, …). cfnYamlSchema() registers a js-yaml type for every tag in CFN_TAGS (across scalar/sequence/mapping kinds) that constructs the equivalent { Ref: … } / { "Fn::<Tag>": … } object form. The rest of the importer then sees a uniform structure regardless of whether the source was JSON or YAML.

Shared builder, containment & relationship derivation

Both paths normalise their source resources into a common internal ResolvedItem[] (id, serviceId, name, optional parentId, properties, relationships) and feed it through one buildGraph(), which:

  • filters each resource’s raw properties down to the service’s known configFields keys,
  • lays nodes out on a simple grid (auto-layout),
  • keeps a parentId only when it resolves to another imported resource, and
  • emits typed Relationships, de-duplicated with self-loops and dangling targets dropped (all source: "imported").

Containment and relationships are derived per format:

  • CloudFormation: parentId comes from the first matching CFN_CONTAINMENT_PROPS property (SubnetId, VpcId, ClusterArn, …) whose Ref/Fn::GetAtt (cfnRefTarget / collectCfnRefs) points at another in-template resource; remaining Ref/GetAtt references plus DependsOn become depends_on edges.
  • ARM: child resource types (e.g. Microsoft.Sql/servers/databases) nest under their parent, and dependsOn / resourceId(…) references become depends_on edges. Ids are made unique (cross-type duplicate names are suffixed) so the graph is always valid.
  • Terraform: containment references are indexed by id, name, and self_link (matched exactly or by last path segment), so AWS vpc_id/subnet_id and GCP network/subnetwork self-links both resolve to a parentId; depends_on addresses become depends_on edges.

Lossless sidecar — faithful re-emit (CloudFormation / ARM)

The graph keeps only registry-known config (clean inspector, typed model), which would make export lossy. To allow faithful round-trip, import also captures the verbatim source in a sidecar the renderer ignores:

  • ResourceInstance.raw — the exact source type + properties (intrinsic functions intact), dependsOn, condition, apiVersion. Tagged with format ("cloudformation" | "arm" | "terraform").
  • InfrastructureGraph.iacSource — template-level sections with no per-node home (CloudFormation Parameters/Mappings/Conditions/Outputs/Metadata; the ARM equivalents). Persisted via graphSchema.ts’s WRITABLE_FIELDS.

Export re-emits these verbatim when present (see below), so an imported template round-trips faithfully; manually-built nodes still scaffold. Both fields hold template data only — never credentials.

Example: CloudFormation (YAML)

A VPC with a subnet nested under it and an instance nested under the subnet. The subnet’s VpcId (a !Ref) drives containment, so the subnet nests under the VPC and the instance under the subnet:

AWSTemplateFormatVersion: "2010-09-09" Resources: MyVpc: Type: AWS::EC2::VPC Properties: CidrBlock: 10.0.0.0/16 MySubnet: Type: AWS::EC2::Subnet Properties: VpcId: !Ref MyVpc CidrBlock: 10.0.1.0/24 MyInstance: Type: AWS::EC2::Instance Properties: SubnetId: !Ref MySubnet InstanceType: t3.micro
import { importIaC } from "@/aws/iac"; const result = importIaC(yamlString); // format auto-detected as "cloudformation" // result.graph → renderable InfrastructureGraph // result.unmappedTypes → e.g. [] (all three types are in the registry)

Example: Terraform (terraform show -json)

Generate the JSON, then feed it to importIaC:

terraform show -json > state.json # or: tofu show -json > state.json (OpenTofu)
{ "format_version": "1.0", "values": { "root_module": { "resources": [ { "address": "aws_vpc.main", "type": "aws_vpc", "name": "main", "values": { "id": "vpc-123", "cidr_block": "10.0.0.0/16" } }, { "address": "aws_subnet.app", "type": "aws_subnet", "name": "app", "values": { "id": "subnet-456", "vpc_id": "vpc-123" } } ] } } }

Here the subnet’s vpc_id ("vpc-123") matches the VPC’s id attribute, so the subnet nests under aws_vpc.main. Nested child_modules are walked recursively.

Registry gaps: unmappedTypes

Source resource types with no registry mapping — an unknown cfnType, or a Terraform type absent from TF_TYPE_TO_SERVICE_ID — are skipped and collected into IacImportResult.unmappedTypes, with a count surfaced in warnings. These are the direct candidates for new catalog entries (see Service Registry) or new TF_TYPE_TO_SERVICE_ID rows.

Extending Terraform coverage (TF_TYPE_TO_SERVICE_ID)

TF_TYPE_TO_SERVICE_ID is an exported Record<string, string> in src/aws/iac.ts. To map a new Terraform resource type, add one row keyed by the HCL type, valued by an existing registry serviceId:

export const TF_TYPE_TO_SERVICE_ID: Record<string, string> = { // …existing rows… aws_cloudfront_distribution: "cloudfront", aws_globalaccelerator_accelerator: "global-accelerator", };

If no suitable serviceId exists yet, add the service to the registry first (one catalog entry — see the Service Registry docs), then reference its id here. Multiple Terraform types may map to the same serviceId (e.g. aws_lb, aws_alb, and aws_elb all map to "elastic-load-balancer").

Export (iacExport.ts)

The reverse transform lives in src/aws/iacExport.ts, mirroring the importer:

  • exportCloudFormation(graph) → { json, yaml, report }
  • exportTerraform(graph, serviceIdToTfType?) → { hcl, report }
  • exportIaC(graph, format) → { content, filename, report }

exportTerraform is parameterised by a serviceId→type map, so the provider adapters wrap it: exportGcpTerraform (src/gcp/iac.ts) and exportAzureTerraform (src/azure/iac.ts), alongside Azure’s exportArm(graph) → { json, report }. Join keys run in reverse: CloudFormation/ARM use each service’s nativeType directly; Terraform uses the inverse type maps (first-listed wins as canonical for many-to-one services). Output is deterministic — resources are id-sorted, so the same graph always serialises identically (important for tests).

UI scope. The Export dialog currently wires the AWS formats (CloudFormation JSON/YAML, Terraform). The GCP/Azure exporters above exist and are tested, but surfacing them in the dialog is a pending follow-up.

Honest scaffolding, not faithful round-trip

Export is intentionally lossy, the mirror image of import: the graph only holds registry-known config, so the generator emits a scaffold a human finishes. An ExportReport quantifies the gaps:

interface ExportReport { exported: number; faithful: number; // of `exported`, how many were re-emitted verbatim from `raw` skipped: { id: string; serviceId: string; reason: string }[]; todos: { address: string; field: string }[]; warnings: string[]; }
  • Resources whose serviceId has no target type are skipped and reported.
  • Resources imported with a raw sidecar (above) are re-emitted verbatim — real property names, intrinsic functions, conditions, and the captured template sections all survive. These count toward report.faithful.
  • Manually-built (or raw-less) resources scaffold: property names follow Strata’s model (with the optional cfnPropertyNames map applied), required configFields that aren’t set become TODO placeholders tallied in todos, and typed edges coarsen to DependsOn / depends_on.

A faithful round-trip test asserts that importCloudFormation → exportCloudFormation → parse preserves resource types, intrinsic functions, and the Parameters/Conditions/Outputs sections, and that re-importing yields a structurally identical graph.

Last updated on