Migration + adoption
Bring existing infrastructure under formae management without breaking what's running.
Scenarios covered:
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
- Discover and extract — set up discovery, review resources, extract to Pkl
- Add a stack label — assign resources to a stack
- 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
- Start with discovery — observe without managing
- Import a low-risk subset — bring a few resources under management
- 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 |
I want to link resources together instead of hard-coding IDs
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
- Declare resources with
local— so they can be referenced - Wire them with
.res— replace hard-coded IDs with live references - 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
localvariable 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
- Discover resources — let formae scan your account (read-only)
- See what's unmanaged — identify what still lives in legacy tools
- Start moving ownership — bring low-risk resources under formae
- 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 |