Migration + adoption

Bring existing infrastructure under formae management without breaking what's running.


I want to turn existing cloud resources into infrastructure code

When to use this

Use this workflow when you:

  • Have resources created via the cloud console, CLI, or another tool and want them represented as code
  • Need to start managing existing resources — change them, tag them, evolve them through code

formae discovers your existing cloud resources, extracts them as Pkl code, and lets you bring them under management — all without touching or recreating anything.

What you'll do

  1. Discover and extract — set up discovery, review resources, extract to Pkl
  2. Add a stack label — assign resources to a stack
  3. Bring resources under management — apply and verify

Step-by-step

Before you begin: review the baseline prerequisites and confirm discovery is enabled (it is by default).

Step 1: Discover and extract your resources

Follow the First Steps guide to set up a discoverable target, wait for discovery, and view what's in your account. Then extract the resources you want to manage:

formae extract --query="managed:false" discovered.pkl

For large accounts, extract by type:

formae extract --query="managed:false type:AWS::EC2::VPC" networking.pkl
formae extract --query="managed:false type:AWS::S3::Bucket" storage.pkl

Note: These examples use AWS resource types. formae also supports other providers — use the equivalent provider types for your environment.

Step 2: Add a stack label

Open the extracted file and uncomment the stack label. This tells formae you want to manage these resources:

forma {
  new formae.Stack {
    label = "imported-networking"  // <-- uncomment and set this
  }

  // ... your extracted resources
}

Step 3: Bring resources under management

Apply the edited file:

formae apply --mode reconcile --watch networking.pkl

formae matches the extracted resources with the discovered ones by their cloud IDs. Nothing is recreated — the resources simply move from "unmanaged" to "managed" status.

Verify:

formae inventory resources --query="stack:imported-networking"

Your resources are now under formae management.

Tips + gotchas

Tip Details
Nothing is recreated Bringing resources under management is a metadata operation. Your live infrastructure isn't touched.
Extract is a snapshot The extracted file reflects the state at extraction time. If the resource changes before you apply, formae will detect the difference.

Common gotcha: The extracted stack label is commented out on purpose. If you apply without uncommenting it, the resources won't be assigned to a stack and won't become managed.

What's next

Goal Guide
Adopt gradually without risk I want to adopt formae incrementally (below)
Explore before importing First Steps → Explore your cloud account
Understand managed vs unmanaged Core Concepts → Unmanaged Resources
Configure discovery Configuration → Discovery

I want to adopt formae incrementally without breaking what's running

When to use this

Use this workflow when you:

  • Already have production infrastructure managed manually, by Terraform, or another tool
  • Can't do a big-bang migration — you need to onboard gradually, one piece at a time
  • Need zero downtime — nothing should break during adoption

The key insight: formae doesn't require you to import everything at once. You can manage some resources with formae, leave others alone, and expand scope over time.

What you'll do

  1. Start with discovery — observe without managing
  2. Import a low-risk subset — bring a few resources under management
  3. Grow with patch mode — add new resources without affecting what's already managed

Step-by-step

Before you begin: review the baseline prerequisites and complete the discovery setup from First Steps or the import guide above.

Step 1: Start with visibility (discovery only)

If you haven't already, set up discoverable targets and let discovery run. This gives you a full inventory without touching anything:

formae inventory resources --query="managed:false" --max-results=100

At this point, formae is purely observational. Your existing infrastructure is completely unaffected.

Step 2: Pick a low-risk starting point

Choose resources that are:

  • Standalone — few or no dependencies on other resources
  • Non-critical — dev/staging before production
  • Simple — S3 buckets, security groups, or DNS records are good candidates

Extract your candidates:

formae extract --query="managed:false type:AWS::S3::Bucket" s3-buckets.pkl

Step 3: Bring the first resources under management

Edit the extracted file to add a stack label, then apply:

formae apply --mode reconcile --watch s3-buckets.pkl

Verify:

formae inventory resources --query="stack:imported-s3"

These resources are now managed by formae. Everything else remains untouched and unmanaged.

Step 4: Add new resources alongside existing ones

Use patch mode to add new infrastructure to an existing stack without affecting what's already there:

formae apply --mode patch --watch new-resources.pkl

Patch mode creates and updates but never destroys — the safest way to grow your formae-managed footprint.

Tips + gotchas

Tip Details
Import dependencies together A subnet depends on a VPC. Import the VPC first (or together in the same forma) so references resolve.
One stack per concern Don't dump everything into one stack.

What's next

Goal Guide
Import specific resources I want to turn existing resources into code (above)
Automate deployments CI/CD Integration
Build self-service for teams Platform Engineering → Self-service infrastructure
Understand sync in depth Core Concepts → Synchronization

When to use this

Use this workflow when you:

  • Have resources that depend on each other — for example a subnet needs a VPC ID
  • Are pasting cloud IDs into your forma and realize that's fragile — IDs change across environments, accounts, and regions
  • Want automatic dependency ordering — create resources in the right sequence without thinking about it

Hard-coded IDs break the moment you redeploy to a different account or region. .res solves this: reference a property from another resource, and formae figures out the rest — ordering, waiting, injecting the value at the right time.

What you'll do

  1. Declare resources with local — so they can be referenced
  2. Wire them with .res — replace hard-coded IDs with live references
  3. Verify with formae eval — confirm everything resolves correctly

Step-by-step

Before you begin: review the baseline prerequisites and familiarize yourself with the local pattern.

Step 1: Identify the dependency chain

Think about which resources feed into others. A common pattern:

VPC → Subnet → DB Subnet Group → Database

The subnet needs the VPC's ID. The subnet group needs the subnet's ID. The database needs the subnet group. Each resource depends on something upstream.

Step 2: Declare referenced resources with local

Any resource you need to reference later gets the local keyword. Then instantiate it on a separate line — otherwise it won't be rendered:

forma {
  // ...stack and target...

  local myVpc = new vpc.Vpc {
    label = "main-vpc"
    cidrBlock = "10.0.0.0/16"
  }
  myVpc // Instantiate it — without this line, the VPC won't be created
}

Resources that nothing references don't need local. Just declare them directly.

Step 3: Wire references with .res

Replace hard-coded IDs with .res.propertyName:

forma {
  // ...stack and target...

  local myVpc = new vpc.Vpc {
    label = "main-vpc"
    cidrBlock = "10.0.0.0/16"
  }
  myVpc

  local subnet1 = new subnet.Subnet {
    label = "subnet-1"
    vpcId = myVpc.res.vpcId       // Dynamic reference, not a hard-coded ID
    cidrBlock = "10.0.1.0/24"
    availabilityZone = "us-east-1a"
  }
  subnet1

  new dbsubnetgroup.DBSubnetGroup {
    label = "db-subnet-group"
    dbSubnetGroupDescription = "Subnet group for RDS"
    subnetIds {
      subnet1.res.subnetId         // Chain continues
    }
  }
}

formae handles all the ordering automatically. The subnet waits for the VPC. The subnet group waits for the subnet.

Step 4: Reference secrets the same way

Secrets follow the same pattern. Create a secret, then reference it with .res:

local dbSecret = new secret.Secret {
  label = "db-password"
  secretString = formae.value(random.password(12, false)).opaque.setOnce
}
dbSecret

new dbinstance.DBInstance {
  label = "my-database"
  engine = "postgres"
  masterUsername = "admin"
  masterUserPassword = dbSecret.res.secretString  // Secure reference
}

The .opaque marker ensures the password is never displayed. .setOnce ensures it's generated once and reused on subsequent applies.

Step 5: Verify with formae eval

Check that everything resolves before applying:

formae eval my-forma.pkl

This shows the full evaluated output. If a .res reference is wrong (e.g. a typo in the property name), you'll see the error here — before anything touches your cloud.

Tips + gotchas

Tip Details
Rule of thumb If you need to reference it, make it local and instantiate it. If you just need to create it, skip local.
Chain as deep as you need VPC → Subnet → Security Group → Instance — .res works at any depth.

Common gotcha: Declaring a local variable but forgetting to instantiate it. The resource simply won't appear in the output — no error, just silence. Always add the bare variable name on its own line after the declaration.

What's next

Goal Guide
Deep dive on .res Core Concepts → Res
Values, secrets, and .opaque Core Concepts → Values
Fundamentals: the local pattern Formae 101 → Fundamentals

I want to test how formae co-exists alongside other IaC tools

When to use this

Use this workflow when you:

  • Already use Terraform, Pulumi, or CloudFormation and don't want to rip it out
  • Want to evaluate formae without risking your existing infrastructure
  • Want a practical transition path away from legacy IaC tools

Use coexistence as a migration phase. Start safely, prove confidence, then move ownership to formae stack by stack.

What you'll do

  1. Discover resources — let formae scan your account (read-only)
  2. See what's unmanaged — identify what still lives in legacy tools
  3. Start moving ownership — bring low-risk resources under formae
  4. Verify and expand — repeat stack by stack until formae is primary

Step-by-step

Before you begin: review the baseline prerequisites and set up a discoverable target.

Step 1: Let discovery find everything

Once your discoverable target is applied, formae automatically scans your account for supported resource types. Those resources appear in inventory whether they were created by Terraform, the console, a script, or formae itself:

formae inventory resources --query="managed:false" --max-results=100

These are unmanaged resources. formae tracks them but won't modify or delete them.

Step 2: Understand the boundary

Two categories, clean separation:

Status What it means
Managed (managed:true) formae controls the lifecycle. Apply, update, destroy through formas.
Unmanaged (managed:false) formae sees it but won't touch it. Terraform, Pulumi, console — whatever created it still owns it.

Check what formae manages vs. what it doesn't:

# What formae manages
formae inventory resources --query="managed:true"

# What lives outside formae (Terraform, console, etc.)
formae inventory resources --query="managed:false"

Step 3: Start the ownership handoff

Use patch mode for low-risk growth while you transition ownership:

formae apply --mode patch --watch new-resources.pkl

Patch mode creates and updates only — it never destroys. That's ideal while you are still proving changes in production.

To migrate existing resources, follow the extract workflow and apply them under a stack:

formae extract --query="managed:false type:AWS::S3::Bucket" s3-buckets.pkl
formae apply --mode reconcile --watch s3-buckets.pkl

Step 4: Verify and expand

Confirm the handoff worked. Managed resources should grow over time while unmanaged shrinks:

# Resources now managed by formae
formae inventory resources --query="stack:my-new-stack"

# Resources still outside formae (next migration candidates)
formae inventory resources --query="managed:false"

Tips + gotchas

Tip Details
Use patch to start, reconcile to finish During coexistence, patch mode reduces blast radius. Once a stack is fully owned by formae, reconcile mode gives you full lifecycle control.
Sync detects external changes If synchronization is enabled and Terraform updates a resource formae also manages, synchronization detects the change and updates its state automatically.

Tip: Dual ownership works. Keep synchronization enabled, and define team conventions for when external changes are accepted vs. overwritten on the next apply.

What's next

Goal Guide
Adopt formae incrementally I want to adopt formae incrementally
Bring discovered resources under management I want to turn existing cloud resources into infrastructure code
Understand managed vs unmanaged Core Concepts → Unmanaged Resources
How discovery works Core Concepts → Discovery
How sync detects external changes Core Concepts → Synchronization