In our ongoing migration from a legacy Terraform-managed AWS environment to AWS CDK, a significant hurdle was always the state migration. The prospect of a "big bang" cutover, where we would need to recreate all foundational VPCs, security groups, and RDS instances from scratch, was a major project risk. Recently, I discovered a method within the CDK that elegantly sidesteps this issue for a large class of resources: the static `fromLookup` methods available on many L2 constructs.
The core concept is that CDK can perform a real-time lookup of existing AWS resources during the synthesis phase, using the AWS SDK. This allows you to import these resources into your CDK application's logical model without ever needing to manage their state or risk recreating them. It's a powerful tool for incremental adoption, where you can start writing new infrastructure in CDK while having it seamlessly reference the old, manually created or Terraform-managed infrastructure.
The typical pattern involves using the `fromLookup` method on a construct class. Here is a practical example for importing an existing VPC and an RDS database instance:
```typescript
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as rds from 'aws-cdk-lib/aws-rds';
// Look up an existing VPC by its tags or ID.
// CDK will perform a DescribeVpcs call during synthesis.
const existingVpc = ec2.Vpc.fromLookup(this, 'ImportedVpc', {
vpcId: 'vpc-12345678', // You can specify ID, or rely on tags
// isDefault: true // Alternatively, look up the default VPC
});
// Look up an existing RDS instance by its instance identifier.
const existingDatabase = rds.DatabaseInstance.fromLookup(this, 'ImportedDb', {
instanceIdentifier: 'legacy-production-db',
});
// Now, you can use these in your new CDK constructs.
new ec2.Instance(this, 'NewAppServer', {
vpc: existingVpc,
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MEDIUM),
machineImage: ec2.MachineImage.latestAmazonLinux2(),
// Security group referencing the imported Vpc is straightforward.
});
```
It is crucial to understand the operational mechanics and limitations:
* **Synthesis-Time Operation:** The lookup happens when you run `cdk synth`. The resolved physical IDs are stored in the generated CloudFormation template as plain parameters or embedded strings. This means the CloudFormation stack itself has no special "import" operation; it simply references the already-existing resource.
* **Read-Only Reference:** CDK treats these looked-up resources as immutable, read-only references. You cannot modify their core properties (like CIDR block for a VPC) through CDK. Any changes must be made via the original provisioning tool or manually, after which a subsequent `cdk synth` will pick up the new attributes.
* **Tag-Based Discovery:** For resources like VPCs, using tags (e.g., `tags: { 'Environment': 'Production' }`) is often more robust than hardcoding IDs, as it aligns with how many teams already manage their resources.
For our team, this approach has transformed the migration strategy. We are now able to decompose the monolithic Terraform state by layers, starting with the application-layer resources (Lambdas, ECS services, API Gateways) that we can now define in CDK while they safely depend on the looked-up network and data layers. The primary effort shifts from risky state manipulation to refactoring infrastructure definitions into TypeScript, which brings immediate benefits in terms of type safety, reusable constructs, and developer experience. While not a complete solution for every resource (some may require a formal CloudFormation import), `fromLookup` has proven to be an indispensable tool for a controlled, low-risk IaC migration.
null
It's a clever trick, but don't get too comfortable. That real-time lookup happens during synth, which means your cdk synth command now requires live AWS credentials and permissions to describe those resources. You've just tied your build process directly to a production environment. If that lookup fails because of a network blip or a temporary IAM hiccup, your entire pipeline grinds to a halt.
Also, watch out for the cache. CDK stores the results of those lookups in a file called `cdk.context.json`. If you don't commit that, everyone on your team gets to enjoy the same slow API calls. If you do commit it, you're now manually managing a pseudo-state file that can drift from reality. It's a useful escape hatch, but it comes with its own set of footguns.
Speed up your build
Real-time synth dependencies are the least of your worries. Wait until you try to reference a looked-up resource in an IAM policy or a security group rule. The CDK will cheerfully output a CloudFormation intrinsic function referencing a resource that isn't in the template. Good luck explaining that during a security audit. It's importing a ghost.
Your stack is too complicated.
You missed the best part. What's the real cost of that "incremental adoption" you're so happy about?
You've now got two state systems: Terraform's and CDK's pseudo-state in that context file. How do you plan to manage drift when someone modifies the original resource? How do you even track that?
Good luck getting that past procurement when they ask who owns what. You're paying for two management tools to do one job now.
Read the contract
You're right that dual-state management is the critical flaw in this approach. The `cdk.context.json` file isn't a state file with any enforcement mechanism, it's just a cache. Drift detection becomes a manual, operational burden you've traded for migration convenience.
I've seen teams try to mitigate this by wrapping the looked-up resources in a custom construct that adds CloudTrail alerts for configuration changes to the source assets. It's more plumbing, and you're still stuck with two sources of truth. Procurement's question about ownership is spot on, the answer usually becomes "the team that manages the original Terraform state", which defeats the purpose of migrating to CDK.
The only clean use case I've found for `fromLookup` is for truly immutable, foundational resources like a core VPC where changes are governed by a separate, infrequent process. Using it for anything mutable like security groups or RDS is asking for trouble.
The CloudTrail alert wrapper is a fascinating, if costly, workaround. It introduces another ongoing operational expense for monitoring, and you'd need to factor in the data ingestion and storage costs for those trails if they weren't already comprehensive.
Even for a foundational VPC, I'd argue the financial ownership gets murky. If that VPC's flow logs or NAT gateways are referenced via lookup, their costs remain in the legacy billing context. Migrating to CDK often aims for consolidated cost allocation via tags, but those looked-up resources might not inherit the new tag schema, breaking showback/chargeback models.
You've traded a complex migration cost for a perpetual, fragmented cost management overhead.
Always check the data transfer costs.