Skip to main content
You don’t migrate an entire platform overnight. Real migrations happen incrementally — one service at a time, one team at a time. The network infrastructure that took two years to stabilise in Terraform stays in Terraform while new services come up in ubx. The challenge is the handoff: your new ubx stacks need the VPC ID, the subnet IDs, the EKS cluster endpoint — values that Terraform owns and manages. The from = "terraform://..." syntax on an input block solves this. ubx reads the Terraform state file directly from S3, GCS, or Azure Blob Storage, extracts the output value you specify, and makes it available as a regular resolved input. Terraform keeps managing what it manages. ubx starts managing what it manages. They share outputs through state, not through manual copy-paste.

What you’ll learn

  • How from = "terraform://..." reads outputs from existing Terraform state
  • The supported path syntax for S3, GCS, and Azure Blob backends
  • How to migrate incrementally without disrupting existing Terraform-managed infra

Why this matters

Terraform state bridge is the canonical migration path for teams moving from Terraform to ubx. You don’t need to migrate everything at once — keep Terraform managing stable infra and use ubx for new work, consuming Terraform outputs via from = "terraform://..." until you’re ready to migrate the rest.

The source code

# Use Terraform-managed VPC and subnets to place new ubx-managed resources.
unit "aws_db_subnet_group" "main" {
  name       = "myapp-${input.env}-subnet-group"
  subnet_ids = input.private_subnets
}

unit "aws_rds_instance" "db" {
  identifier           = "myapp-${input.env}"
  allocated_storage    = 20
  engine               = "postgres"
  engine_version       = "15"
  instance_class       = "db.t3.micro"
  username             = "admin"
  password             = "placeholder"
  db_subnet_group_name = unit.aws_db_subnet_group.main.name
  skip_final_snapshot  = true
}

How it works

1

ubx parses and validates the from= path at compile time

The terraform:// URI syntax is validated by the ubx parser — it checks that the backend type is one of s3, gcs, azblob, and that the #output-name fragment is present. Invalid paths produce a compile error, not a runtime failure.
2

The state file is read at plan and apply time

When you run ubx plan or ubx apply, ubx downloads the specified Terraform state file from the remote backend (using your current AWS/GCP/Azure credentials), parses the JSON, and extracts the named output value. The Terraform state file is never modified.
3

The value is treated as a resolved input

Once extracted, the Terraform output value is treated as a regular Resolved<T> input — available for string interpolation, resource attributes, and references throughout the stack. It is resolved at plan time, not pending on a resource output.

What ubx generates

// Generated by ubx — do not edit
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

const config = new pulumi.Config();
const env = config.get("env") || "dev";

// Terraform state bridge — values read from Terraform state at apply time
// ubx reads s3://my-terraform-state/networking/dev.tfstate and extracts outputs
const tfNetworking = pulumi.output(
    // Internal: ubx generates a data source call to read the Terraform state
    { vpcId: "...", privateSubnets: ["...", "..."] } // resolved from state
);

const mainSubnetGroup = new aws.rds.SubnetGroup("main", {
    name: `myapp-${env}-subnet-group`,
    subnetIds: tfNetworking.apply(n => n.privateSubnets),
});

const db = mainSubnetGroup.name.apply(sgName =>
    new aws.rds.Instance("db", {
        identifier: `myapp-${env}`,
        allocatedStorage: 20,
        engine: "postgres",
        engineVersion: "15",
        instanceClass: "db.t3.micro",
        username: "admin",
        password: "placeholder",
        dbSubnetGroupName: sgName,
        skipFinalSnapshot: true,
    })
);

export const dbEndpoint = db.apply(d => d.endpoint);

Common mistakes

The from = "terraform://..." path must match your actual Terraform state structure exactly — the bucket name, the key path, and the output name are all case-sensitive. If the output name in your Terraform code is vpc_id but you write vpc-id in the ubx path, the apply will fail with a state-parsing error, not a compile error.
For incremental migration, it’s cleaner to have ubx consume Terraform outputs via from = "terraform://..." than to migrate the Terraform resources into ubx prematurely. Migrate resources when the time is right — not because you need to wire values between stacks.

Run it

ubx validate   # validates the terraform:// path syntax — does not read state
ubx plan       # reads Terraform state file; requires backend credentials
ubx apply      # provisions resources using Terraform-managed values

What you learned

from = "terraform://..." reads a named output from an existing Terraform state file
Supported backends: s3, gcs, azblob — the same backends Terraform uses
The Terraform state file is read-only — ubx never modifies it
Migrate incrementally: keep Terraform managing stable infra, use ubx for new work

Next steps

Migrate from Terraform

Full step-by-step Terraform migration guide

ubx vs Terraform

Why ubx, when to migrate, and what stays the same