> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ubiquex.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Input from Terraform State

> Read outputs from existing Terraform state files during incremental migration.

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

<Info>
  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.
</Info>

***

## The source code

<CodeGroup>
  ```hcl main.iac theme={"theme":"css-variables"}
  # 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
  }
  ```

  ```hcl inputs.iac theme={"theme":"css-variables"}
  input "env" {
    type    = "string"
    default = "dev"
  }

  # from = "terraform://..." — read an output from an existing Terraform state file.
  # Syntax: terraform://<backend>/<bucket-or-path>/<key>/<state-file>#<output-name>
  # Supported backends: s3, gcs, azblob

  # Read the VPC ID from an S3-backed Terraform workspace.
  input "vpc_id" {
    type = "string"
    from = "terraform://s3/my-terraform-state/networking/dev.tfstate#vpc_id"
  }

  # Read the list of private subnet IDs.
  input "private_subnets" {
    type = "list(string)"
    from = "terraform://s3/my-terraform-state/networking/dev.tfstate#private_subnet_ids"
  }

  # Read the EKS cluster endpoint from a GCS-backed Terraform workspace.
  input "cluster_endpoint" {
    type = "string"
    from = "terraform://gcs/my-terraform-bucket/eks/dev/default.tfstate#cluster_endpoint"
  }

  # Read a value from an Azure Blob Storage backend.
  input "storage_account_name" {
    type = "string"
    from = "terraform://azblob/mycontainer/mykey/terraform.tfstate#storage_account_name"
  }
  ```

  ```hcl outputs.iac theme={"theme":"css-variables"}
  output "db_endpoint" {
    value = unit.aws_rds_instance.db.endpoint
  }

  output "cluster_endpoint" {
    value = input.cluster_endpoint
  }
  ```

  ```hcl ubx.iac theme={"theme":"css-variables"}
  ubx {
    name    = "input-from-terraform-state"
    runtime = "typescript"
    stacks  = ["dev", "staging", "prod"]
  }

  backend "s3" {
    bucket = "my-ubx-state"
    region = "us-east-1"
    key    = "${stack}/input-from-terraform-state.tfstate"
  }
  ```
</CodeGroup>

***

## How it works

<Steps>
  <Step title="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.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>
</Steps>

***

## What ubx generates

<CodeGroup>
  ```typescript index.ts theme={"theme":"css-variables"}
  // 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);
  ```

  ```python __main__.py theme={"theme":"css-variables"}
  # Generated by ubx — do not edit
  import pulumi
  import pulumi_aws as aws

  config = pulumi.Config()
  env = config.get("env") or "dev"

  # ubx reads Terraform state from s3://my-terraform-state/networking/dev.tfstate
  # and resolves the vpc_id and private_subnet_ids outputs
  private_subnets = pulumi.Output.from_input(["..."])  # resolved from Terraform state

  main_subnet_group = aws.rds.SubnetGroup("main",
      name=f"myapp-{env}-subnet-group",
      subnet_ids=private_subnets,
  )

  db = main_subnet_group.name.apply(lambda sg_name:
      aws.rds.Instance("db",
          identifier=f"myapp-{env}",
          allocated_storage=20,
          engine="postgres",
          engine_version="15",
          instance_class="db.t3.micro",
          username="admin",
          password="placeholder",
          db_subnet_group_name=sg_name,
          skip_final_snapshot=True,
      )
  )

  pulumi.export("db_endpoint", db.apply(lambda d: d.endpoint))
  ```

  ```go main.go theme={"theme":"css-variables"}
  // Generated by ubx — do not edit
  package main

  import (
      "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/rds"
      "github.com/pulumi/pulumi/sdk/v3/go/pulumi"
      "github.com/pulumi/pulumi/sdk/v3/go/pulumi/config"
  )

  func main() {
      pulumi.Run(func(ctx *pulumi.Context) error {
          cfg := config.New(ctx, "")
          env := cfg.Get("env")
          if env == "" { env = "dev" }

          // ubx reads Terraform state at apply time and resolves outputs
          privateSubnets := pulumi.ToStringArrayOutput(pulumi.StringArray{pulumi.String("subnet-abc"), pulumi.String("subnet-def")})

          mainSubnetGroup, err := rds.NewSubnetGroup(ctx, "main", &rds.SubnetGroupArgs{
              Name:      pulumi.Sprintf("myapp-%s-subnet-group", env),
              SubnetIds: privateSubnets,
          })
          if err != nil { return err }

          db, err := rds.NewInstance(ctx, "db", &rds.InstanceArgs{
              Identifier:        pulumi.Sprintf("myapp-%s", env),
              AllocatedStorage:  pulumi.Int(20),
              Engine:            pulumi.String("postgres"),
              EngineVersion:     pulumi.String("15"),
              InstanceClass:     pulumi.String("db.t3.micro"),
              Username:          pulumi.String("admin"),
              Password:          pulumi.String("placeholder"),
              DbSubnetGroupName: mainSubnetGroup.Name,
              SkipFinalSnapshot: pulumi.Bool(true),
          })
          if err != nil { return err }

          ctx.Export("dbEndpoint", db.Endpoint)
          return nil
      })
  }
  ```
</CodeGroup>

***

## Common mistakes

<Warning>
  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.
</Warning>

<Tip>
  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.
</Tip>

***

## Run it

```bash theme={"theme":"css-variables"}
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

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

***

## Next steps

<CardGroup cols={2}>
  <Card title="Migrate from Terraform" href="/v1/tutorials/54-migration-from-terraform">
    Full step-by-step Terraform migration guide
  </Card>

  <Card title="ubx vs Terraform" href="/v1/concepts/vs-terraform">
    Why ubx, when to migrate, and what stays the same
  </Card>
</CardGroup>

***

<Info>
  Full runnable example: [github.com/ubiquex/ubx-examples/17-input-from-terraform-state](https://github.com/ubiquex/ubx-examples/tree/main/17-input-from-terraform-state)
</Info>
