Skip to main content
Cloud Migration9 min read

CloudFormation-to-Pulumi Migration Starts With Inventory, Not a Rewrite

Pulumi’s Discovered Stacks feature reinforces a lesson many engineering teams learn the hard way: infrastructure modernization depends on knowing what exists before rewriting how it is managed. For teams moving from CloudFormation to Pulumi, the migration path should begin with discovery, ownership mapping, drift analysis, and incremental stack design.

Infrastructure-as-code migration rarely fails because teams cannot translate syntax. It fails because nobody can confidently answer a simpler question first: what infrastructure do we actually have, who owns it, and is it still aligned with production reality?

That is why Pulumi’s recent announcement of Discovered Stacks matters. The feature is positioned as one place for viewing infrastructure, and Pulumi has also published specific guidance for migrating CloudFormation to Pulumi using Discovered Stacks. For developers, platform engineers, and CTOs, the key takeaway is clear: CloudFormation-to-Pulumi migration is becoming an inventory problem before it is a rewrite problem.

Context: IaC modernization is not just code conversion

CloudFormation-to-Pulumi Migration Starts With Inventory, Not a Rewrite
CloudFormation-to-Pulumi Migration Starts With Inventory, Not a Rewrite

CloudFormation has been the default AWS-native infrastructure-as-code tool for many organizations. It is widely used, deeply integrated with AWS, and often embedded in years of deployment processes, account structures, templates, nested stacks, service catalogs, and hand-built operational practices.

Pulumi offers a different model: define infrastructure using general-purpose programming languages such as TypeScript, Python, Go, C#, Java, and YAML. For engineering teams that want stronger abstractions, reusable components, automated testing, and closer alignment between application and infrastructure development, Pulumi can be attractive.

But a migration from CloudFormation to Pulumi is not just a matter of converting JSON or YAML into TypeScript or Python. Most long-lived cloud environments include:

  • CloudFormation stacks created by different teams at different times
  • Manually created infrastructure outside any IaC system
  • Resources imported into CloudFormation after the fact
  • Drift between declared templates and deployed infrastructure
  • Deprecated environments nobody wants to touch
  • Shared resources with unclear ownership
  • Compliance-sensitive resources that cannot be recreated casually

In other words, the hard part is not only writing the new Pulumi program. The hard part is deciding what should be represented, where it belongs, who owns it, and how to move safely without disrupting production.

What Pulumi announced

Pulumi announced Discovered Stacks for Pulumi Cloud in its blog post, Discovered Stacks: One Place for All Your Infrastructure. Pulumi positions the capability as a way to view infrastructure in one place, including resources that may not yet be managed by Pulumi programs.

Pulumi also published migration guidance in Migrate CloudFormation to Pulumi with Discovered Stacks. That article connects discovery directly to CloudFormation migration, showing how teams can use discovered infrastructure as a starting point for organizing and migrating resources into Pulumi-managed stacks.

The important pattern is not simply that a new feature exists. The pattern is that modern IaC migration tooling is moving upstream. Instead of beginning with a blank repository and asking teams to rewrite everything, the process starts by surfacing the actual infrastructure footprint.

That shift reflects the operational reality many organizations face. Before you modernize infrastructure code, you need a reliable infrastructure inventory.

Why inventory comes before rewrite

1. Cloud environments are rarely as clean as the repo suggests

Many organizations treat the IaC repository as the source of truth. In a healthy environment, that should be true. In a long-lived environment, it often is not.

Production systems accumulate exceptions. An urgent incident leads to a manual security group update. A data team creates a bucket for a one-off workflow. A networking change is tested manually and never backfilled into templates. A CloudFormation stack is updated out-of-band. Over time, the gap between intended state and actual state grows.

If a team begins a Pulumi migration by translating only the templates in Git, it may miss unmanaged or drifted resources that production depends on. That creates risk. The new IaC system looks clean, but the environment remains partially unmanaged.

Inventory helps expose the real migration surface area.

2. Ownership is a modernization dependency

IaC migrations frequently uncover an uncomfortable truth: some infrastructure has no clear owner. That may include old load balancers, IAM roles, DNS records, S3 buckets, VPC peering connections, Lambda functions, databases, or queues.

This is not just an organizational inconvenience. Ownership determines who can approve changes, who understands runtime impact, who pays for the resource, and who is accountable for security posture.

A CloudFormation-to-Pulumi migration is a chance to reestablish ownership boundaries. Rather than creating Pulumi stacks that mirror historical accidents, teams can group resources around maintainable domains: application, platform service, network foundation, data system, shared security layer, or environment.

The code migration should follow the ownership model, not the other way around.

3. Drift changes the migration plan

Drift is one of the biggest sources of migration surprise. A CloudFormation template may say one thing while the live infrastructure says another. If the migration imports or recreates based only on templates, it can accidentally overwrite changes that were made directly in the cloud provider.

This is why discovery is so valuable. It gives teams a chance to compare declared infrastructure, actual infrastructure, and desired future state. Some drift should be eliminated. Some drift may represent intentional emergency fixes that should be codified. Some drift may reveal abandoned resources that should be retired.

A rewrite treats drift as an obstacle. A modernization program treats drift as information.

Rethinking stacks as maintainable units

One of the most important design decisions in Pulumi migration is how to define stacks. A common mistake is to map every CloudFormation stack directly to a Pulumi stack. That may be appropriate in some cases, but it can also preserve old coupling and make the new system harder to operate.

A better approach is to use inventory data to decide stack boundaries deliberately.

Group by lifecycle

Resources that change together usually belong together. For example, an application service, its container infrastructure, its task roles, and its service-specific alarms may fit naturally into one stack. A shared VPC, central DNS zone, or organization-wide security baseline may have a different lifecycle and should likely be managed separately.

Lifecycle-based grouping reduces accidental blast radius. It also makes CI/CD easier because each stack can have a clear deployment cadence.

Group by ownership

If two teams must approve changes independently, their resources probably should not be tightly coupled in the same stack. Inventory makes these ownership seams visible.

For CTOs and engineering leaders, this is where IaC migration intersects with operating model design. The stack structure should reflect how teams actually build, run, and support systems.

Group by risk

Some resources are low-risk to refactor. Others, such as production databases, IAM trust policies, networking foundations, and DNS records, require extra care. Migration plans should separate high-risk resources from fast-moving application infrastructure.

A practical pattern is to start with read-only discovery, then import low-risk resources, then handle critical shared resources with dedicated migration runbooks and review gates.

Practical implications for engineering teams

Start with a discovery sprint

Before writing much Pulumi code, run a focused discovery sprint. The goal is to answer four questions:

  1. What infrastructure exists across accounts, regions, and environments?
  2. Which resources are managed by CloudFormation, Pulumi, another IaC tool, or nothing at all?
  3. Who owns each major resource group?
  4. Where is there drift, duplication, or obvious technical debt?

Pulumi’s Discovered Stacks can support this inventory-first workflow by giving teams a centralized view of infrastructure before everything is fully converted into Pulumi programs.

Build a migration map, not just a backlog

A list of CloudFormation templates to convert is not enough. Teams need a migration map that links resources to target stacks, owners, dependencies, risk levels, and migration method.

For each resource group, document whether the plan is to:

  • Import it into Pulumi as-is
  • Refactor it into a new component abstraction
  • Leave it temporarily under CloudFormation
  • Retire it as unused infrastructure
  • Replace it during a larger platform upgrade

This prevents the migration from becoming a mechanical rewrite that carries forward every historical decision.

Use migration as a debt reduction opportunity

IaC modernization is a rare moment when teams have permission to inspect infrastructure assumptions. Use it to reduce operational debt.

Look for unused IAM roles, overly broad policies, untagged resources, old security groups, unsupported runtime versions, duplicated networking patterns, and inconsistent naming conventions. Not every issue should block migration, but each should be captured and prioritized.

At Vibgrate, we often see modernization succeed when teams separate inventory, migration, and remediation into related but distinct workstreams. Trying to fix everything during the rewrite can stall the program. Ignoring debt entirely means missing much of the value.

Avoid the big-bang rewrite

A big-bang IaC migration is tempting because it promises a clean cutover. In practice, it increases risk. Infrastructure has too many dependencies, and production systems rarely tolerate sweeping ownership changes well.

An incremental approach is safer:

  1. Discover infrastructure and classify resources.
  2. Define target stack boundaries.
  3. Import or migrate one domain at a time.
  4. Validate state and drift after each step.
  5. Decommission old CloudFormation stacks only when ownership is clear.
  6. Add policy, testing, and CI/CD controls as the Pulumi footprint grows.

This approach lets teams modernize while continuing to ship software.

What CTOs should watch

For engineering leaders, the CloudFormation-to-Pulumi conversation should not be framed only as tooling preference. It is a governance, maintainability, and delivery conversation.

The most important metrics are not lines of YAML converted or number of Pulumi programs created. Better signals include:

  • Percentage of infrastructure with clear ownership
  • Percentage of resources managed through IaC
  • Number of unmanaged production resources
  • Drift detected and resolved over time
  • Deployment frequency for infrastructure changes
  • Lead time for safe environment provisioning
  • Reduction in duplicated infrastructure patterns

These metrics connect IaC migration to business outcomes: faster delivery, lower operational risk, better compliance posture, and easier platform evolution.

Conclusion: discovery is the new starting line

Pulumi’s Discovered Stacks announcement and CloudFormation migration guidance highlight a broader modernization principle: teams cannot safely rewrite what they have not inventoried. Before choosing abstractions, languages, or component libraries, organizations need to understand the infrastructure they already run.

For developers and platform teams, that means treating discovery, ownership mapping, and drift analysis as first-class migration work. For CTOs, it means funding IaC modernization as an incremental reliability and maintainability program, not a one-time translation project.

CloudFormation-to-Pulumi migration can absolutely be a path toward more expressive, testable, and maintainable infrastructure code. But the winning teams will start with inventory, group resources into sensible stacks, reduce unmanaged infrastructure, and modernize one safe step at a time.

Vibgrate CLI

See a real scan run

A replay of the actual CLI running against our test repositories — live progress, real findings, a genuine DriftScore. Nothing executes in your browser.

Replay
demo@vibgrate — bash
npx @vibgrate/cli scan
 
╭──────────────────────────────────────────╮
Vibgrate Drift Report
╰──────────────────────────────────────────╯
 
── node-turborepo (node) .
Runtime: >=18.0.0 (6 majors behind)
Frameworks:
Turbo: 1.13.4 → 2.10.12 (1 behind)
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Dependencies:
1 current 1 1-behind 3 2+ behind 1 unknown
 
── @repo/admin (node) apps/admin
Frameworks:
TanStack Query: 5.102.5 → 5.102.5 (current)
React: 18.3.1 → 19.2.8 (1 behind)
React DOM: 18.3.1 → 19.2.8 (1 behind)
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Vite: 5.4.21 → 8.2.2 (3 behind)
Dependencies:
3 current 9 1-behind 3 2+ behind 4 unknown
 
── @repo/api (node) apps/api
Frameworks:
Express: 4.22.2 → 5.2.1 (1 behind)
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Vitest: 1.6.1 → 4.1.11 (3 behind)
Dependencies:
7 current 5 1-behind 3 2+ behind 4 unknown
 
── @repo/web (node) apps/web
Frameworks:
Next.js: 14.2.35 → 16.3.3 (2 behind)
React: 18.3.1 → 19.2.8 (1 behind)
React DOM: 18.3.1 → 19.2.8 (1 behind)
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Dependencies:
2 current 6 1-behind 3 2+ behind 5 unknown
 
── @repo/config (node) packages/config
Frameworks:
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Dependencies:
2 current 2 1-behind 5 2+ behind 0 unknown
 
── @repo/database (node) packages/database
Frameworks:
Prisma: 5.22.0 → 7.10.0 (2 behind)
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Dependencies:
1 current 0 1-behind 3 2+ behind 1 unknown
 
── @repo/types (node) packages/types
Frameworks:
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Dependencies:
0 current 0 1-behind 1 2+ behind 1 unknown
 
── @repo/ui (node) packages/ui
Frameworks:
React: 18.3.1 → 19.2.8 (1 behind)
TypeScript: 5.9.3 → 7.0.2 (2 behind)
React: 18.3.1 → 19.2.8 (1 behind)
Dependencies:
1 current 4 1-behind 1 2+ behind 1 unknown
 
── @repo/utils (node) packages/utils
Frameworks:
TypeScript: 5.9.3 → 7.0.2 (2 behind)
Vitest: 1.6.1 → 4.1.11 (3 behind)
Dependencies:
0 current 1 1-behind 2 2+ behind 1 unknown
 
Tech Stack
Frontend: React, React DOM
Meta-frameworks: Next.js
Bundlers: tsx, Turbo, Vite
CSS / UI: Autoprefixer, PostCSS, Tailwind CSS
Backend: Express
ORM / Database: Prisma, Prisma Client
Testing: Vitest
Lint & Format: ESLint, ESLint Prettier, ESLint React, Prettier, typescript-eslint
 
Services & Integrations
Auth: JWT 9.0.3
Databases: Prisma 5.22.0
 
TypeScript
v5.3.3 · strict ✔ · MIXED · target: ES2022
 
Build & Deploy
Package Managers: pnpm
Monorepo: npm-workspaces, pnpm-workspaces, turbo
 
Product Purpose Signals
Frameworks: react, nextjs
Evidence: 177
Top Signals:
- [heading] Dashboard (apps/admin/src/pages/Dashboard.tsx)
- [title] Revenue Overview (apps/admin/src/pages/Dashboard.tsx)
- [copy] workspace:* (packages/ui/package.json)
- [copy] ./dist (packages/ui/tsconfig.json)
- [copy] ./src/index.ts (packages/ui/package.json)
- [copy] @repo/config/tsconfig-base.json (packages/ui/tsconfig.json)
- [copy] @repo/ui (packages/ui/package.json)
- [copy] #3b82f6 (apps/admin/src/pages/Dashboard.tsx)
Unknowns:
- No pricing or billing evidence found.
- No integrations/connectors evidence found.
- No route structure evidence found.
 
Security Posture
Lockfile ✖ · .env ✔ · node_modules ✔
 
Platform
Native modules: turbo
 
Code Quality
Files: 36 · Functions: 183 · Avg complexity: 2.62 · Avg length: 21.13 lines
Max nesting: 2 · Circular deps: 0 · Dead code: 0%
God files: apps/admin/src/pages/Products (448 lines)
 
Database Schema
postgresql · 8 models · 1 enum
Models: Address, CartItem, Category, Order, OrderItem (+3 more)
 
Findings (16 errors, 11 warnings)
Node.js runtime ">=18.0.0" reached end-of-life on 2025-04-30 (latest: 24.0.0).
vibgrate/runtime-eol in .
TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in .
60% of dependencies are 2+ major versions behind in node-turborepo.
vibgrate/dependency-rot in .
@types/node is 6 major versions behind (spec: ^20.11.0, latest: 26.3.0).
vibgrate/dependency-major-lag in .
TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in apps/admin
Vite is 3 major versions behind (current: 5.4.21, latest: 8.2.2).
vibgrate/framework-major-lag in apps/admin
vite is 3 major versions behind (spec: ^5.0.12, latest: 8.2.2).
vibgrate/dependency-major-lag in apps/admin
TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in apps/api
Vitest is 3 major versions behind (current: 1.6.1, latest: 4.1.11).
vibgrate/framework-major-lag in apps/api
@types/node is 6 major versions behind (spec: ^20.11.0, latest: 26.3.0).
vibgrate/dependency-major-lag in apps/api
vitest is 3 major versions behind (spec: ^1.2.1, latest: 4.1.11).
vibgrate/dependency-major-lag in apps/api
Next.js is 2 major versions behind (current: 14.2.35, latest: 16.3.3).
vibgrate/framework-major-lag in apps/web
TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in apps/web
@types/node is 6 major versions behind (spec: ^20.11.0, latest: 26.3.0).
vibgrate/dependency-major-lag in apps/web
TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in packages/config
56% of dependencies are 2+ major versions behind in @repo/config.
vibgrate/dependency-rot in packages/config
eslint-plugin-react-hooks is 3 major versions behind (spec: ^4.6.0, latest: 7.1.1).
vibgrate/dependency-major-lag in packages/config
Prisma is 2 major versions behind (current: 5.22.0, latest: 7.10.0).
vibgrate/framework-major-lag in packages/database
TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in packages/database
75% of dependencies are 2+ major versions behind in @repo/database.
vibgrate/dependency-rot in packages/database
TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in packages/types
100% of dependencies are 2+ major versions behind in @repo/types.
vibgrate/dependency-rot in packages/types
TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in packages/ui
TypeScript is 2 major versions behind (current: 5.9.3, latest: 7.0.2).
vibgrate/framework-major-lag in packages/utils
Vitest is 3 major versions behind (current: 1.6.1, latest: 4.1.11).
vibgrate/framework-major-lag in packages/utils
67% of dependencies are 2+ major versions behind in @repo/utils.
vibgrate/dependency-rot in packages/utils
vitest is 3 major versions behind (spec: ^1.2.1, latest: 4.1.11).
vibgrate/dependency-major-lag in packages/utils
 
╭──────────────────────────────────────────╮
Top Priority Actions
╰──────────────────────────────────────────╯
 
1. Upgrade EOL runtime in node-turborepo
End-of-life runtimes no longer receive security patches and block ecosystem upgrades.
./.
>=18.0.0 → 24.0.0 (6 majors behind)
Impact: −10 drift points (runtime & EOL)
 
2. Fix security posture: no lockfile found
Without a lockfile, installs are non-deterministic. Run the install command to generate one and commit it.
./
Missing: package-lock.json, pnpm-lock.yaml, or yarn.lock
 
3. Upgrade Vite 5.4.21 → 8.2.2 in @repo/admin (+2 more)
3 major versions behind. Major framework drift increases breaking change risk and blocks access to security fixes and performance improvements.
./apps/admin
Vite: 5.4.21 → 8.2.2 (3 majors behind)
./apps/api
Vitest: 1.6.1 → 4.1.11 (3 majors behind)
./packages/utils
Vitest: 1.6.1 → 4.1.11 (3 majors behind)
Impact: −5–15 drift points
 
4. Reduce dependency rot in @repo/types (100% severely outdated)
1 of 1 dependencies are 2+ majors behind. Run `npm outdated` and prioritise packages with known CVEs or breaking API changes.
./packages/types
typescript: 5.9.3 → 7.0.2 (2 majors behind)
Impact: −5–10 drift points
 
5. Reduce dependency rot in @repo/database (75% severely outdated)
3 of 4 dependencies are 2+ majors behind. Run `npm outdated` and prioritise packages with known CVEs or breaking API changes.
./packages/database
@prisma/client: 5.22.0 → 7.10.0 (2 majors behind)
prisma: 5.22.0 → 7.10.0 (2 majors behind)
typescript: 5.9.3 → 7.0.2 (2 majors behind)
Impact: −5–10 drift points
 
╭──────────────────────────────────────────╮
Architecture Layers
╰──────────────────────────────────────────╯
 
Archetype: nextjs (80% confidence)
Files classified: 24 (11 unclassified)
Folders classified: 8
apps/admin/src presentation 100% 4 files
apps/admin/src/pages presentation 100% 2 files
apps/api/src/middleware middleware 100% 2 files
apps/api/src/routes routing 100% 2 files
apps/web/src/app presentation 100% 4 files
apps/web/src/app/products presentation 100% 2 files
apps/web/src/app/products/[id] presentation 100% 1 file
packages/ui/src presentation 100% 6 files
Unclassified source (sample): 11
 
presentation 15 files drift ████████████████████ 100 risk high
routing 4 files drift ████████████████████ 100 risk high
middleware 2 files drift ███████▍░░░░░░░░░░░░ 37 risk moderate
config 2 files drift ░░░░░░░░░░░░░░░░░░░░ 0 risk none
shared 1 file drift ████████████████████ 100 risk high
 
╭──────────────────────────────────────────╮
DriftScore Summary
╰──────────────────────────────────────────╯
 
DriftScore: 66/100
Risk Level: HIGH
Projects: 9
Classified: 8 nano · 1 micro · 0 small · 0 standard
Billable: 0.42 · 9 detected → 0.42 billable projects (micro-project pricing)
0.1 micro · 0.32 nano
These fractions add up across repositories, then round down to whole billable projects.
 
Score Breakdown
Runtime: ████████████████████ 100
Frameworks: █████████▏░░░░░░░░░░ 46
Dependencies: ██████▏░░░░░░░░░░░░░ 31
EOL Risk: ████████████████████ 100
 
Scanned at 2026-08-26T09:08:28.481Z · 7.1s · 286 files scanned · 56 workspace files · 27 dirs
Press Run to start.