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(ortofu show -json) for theaws,google, andazurermproviders (a single state file may mix them). OpenTofu is a Terraform fork emitting the identical JSON schema, so it flows throughimportTerraformunchanged — 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
| Function | Purpose |
|---|---|
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 / importAzureTerraform | Provider 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-levelResourcesmap or anAWSTemplateFormatVersion, or"terraform"when it hasvalues,planned_values,resource_changes,terraform_version, orformat_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 registrynativeType, sogetServiceByNativeType(provider, type)resolves it directly — the same join key used by live discovery. (getServiceByCfnTyperemains 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>).importAnyIaCmerges all three (theaws_*/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
configFieldskeys, - lays nodes out on a simple grid (auto-layout),
- keeps a
parentIdonly when it resolves to another imported resource, and - emits typed
Relationships, de-duplicated with self-loops and dangling targets dropped (allsource: "imported").
Containment and relationships are derived per format:
- CloudFormation:
parentIdcomes from the first matchingCFN_CONTAINMENT_PROPSproperty (SubnetId,VpcId,ClusterArn, …) whoseRef/Fn::GetAtt(cfnRefTarget/collectCfnRefs) points at another in-template resource; remainingRef/GetAttreferences plusDependsOnbecomedepends_onedges. - ARM: child resource types (e.g.
Microsoft.Sql/servers/databases) nest under their parent, anddependsOn/resourceId(…)references becomedepends_onedges. Ids are made unique (cross-type duplicate names are suffixed) so the graph is always valid. - Terraform: containment references are indexed by
id,name, andself_link(matched exactly or by last path segment), so AWSvpc_id/subnet_idand GCPnetwork/subnetworkself-links both resolve to aparentId;depends_onaddresses becomedepends_onedges.
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 sourcetype+properties(intrinsic functions intact),dependsOn,condition,apiVersion. Tagged withformat("cloudformation" | "arm" | "terraform").InfrastructureGraph.iacSource— template-level sections with no per-node home (CloudFormationParameters/Mappings/Conditions/Outputs/Metadata; the ARM equivalents). Persisted viagraphSchema.ts’sWRITABLE_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.microimport { 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
serviceIdhas no target type are skipped and reported. - Resources imported with a
rawsidecar (above) are re-emitted verbatim — real property names, intrinsic functions, conditions, and the captured template sections all survive. These count towardreport.faithful. - Manually-built (or
raw-less) resources scaffold: property names follow Strata’s model (with the optionalcfnPropertyNamesmap applied), requiredconfigFieldsthat aren’t set becomeTODOplaceholders tallied intodos, and typed edges coarsen toDependsOn/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.