关于 pulumi-migrate-from-discovered-stack
此 skill 用于将 Pulumi Cloud Discovery 已扫描的云资源迁移到 Pulumi stack,采用先读取云状态再导入的流程。它通过 API 标注跟踪进度与例外,并以零差异预览和提交拉取请求为目标。
主要功能
- 01获取并分类已发现资源
- 02根据云状态导入资源
- 03通过 API 标注跟踪迁移
- 04用 pulumi preview 验证零差异
- 05生成迁移代码和报告
适用场景
- 01迁移已扫描的 CloudFormation stack
- 02迁移已扫描的 Azure ARM 资源
- 03恢复并跟踪现有迁移
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ pulumi/ agent-skills --skill pulumi-migrate-from-discovered-stack安装后在 Claude Code 中输入/pulumi-migrate-from-discovered-stack确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
arm.mdcloudformation.mdrefactor.mduse_cases.yamlagents/openai.yamlscripts/build_import.pytriage.py详细介绍
适用范围:此技能仅适用于已由 Pulumi Cloud 的 Discovery 功能扫描,并通过已发现堆栈 API 暴露的堆栈(GET .../discovered-stacks/{projectName}/{stackName}/resources,见下文)。此技能不适用于迁移 Discovery 尚未扫描的任意 CloudFormation/ARM 模板或账户——如果源中不存在已发现的堆栈,此技能便没有可读取的内容,也不适用。
请勿同时加载
cloudformation-to-pulumi或pulumi-arm-to-pulumi。 这两个技能规定了以模板为先的工作流(机械翻译 → 导入),并要求 AWS 使用aws-native。此技能以云状态为先(从已发现的状态导入 → 稍后可选择对照模板重构),默认使用经典aws/azure-native。这两个技能中有用的参考资料已整理到此文件夹中的cloudformation.md和arm.md文件里。
计划调整
如果你在加载此技能前已经生成了迁移计划,请根据下方的工作流审查并更新计划——此处的阶段优先于任何先前的计划。继续操作前,请向用户说明调整后的计划。
成功标准
迁移完成需满足以下条件:
- 完整覆盖资源——每个已发现的资源都已导入,或者附有说明未导入原因的注释。
- 零差异——
pulumi preview不显示任何更改。这证明代码与云状态完全一致。 - 通过 API 跟踪进度——使用
compareTo和迁移注释,让进度显示在 Pulumi Cloud 中,而不只是保存在 Agent 的记忆里。 - 以 PR 作为交付成果——提交包含迁移后代码和迁移报告的拉取请求。
已发现堆栈 API
获取资源
GET /api/preview/insights/{orgName}/discovered-stacks/{projectName}/{stackName}/resources?compareTo={targetProject}/{targetStack}
如果目标 Pulumi 堆栈存在(其中可能已有上次迁移尝试留下的状态),务必包含 compareTo。返回 DiscoveredResourceInfo 对象列表。以下 JSON 路径均为准确路径,使用前请核实:
name— 顶层:逻辑名称(CF 逻辑 ID / ARM 资源名称)。将其用作 Pulumi 资源名称。originType— 顶层:原生云类型(例如AWS::S3::Bucket、Microsoft.Storage/storageAccounts)providerType— 顶层:映射后的 Pulumi 类型令牌(例如aws:s3/bucket:Bucket)。未映射时为null。resource.inputs.providerId— 用于pulumi import的物理云 ID。resource.urn— 注释请求中用作resourceUrn的 URN(原样复制)。migrationStatus— 顶层:以下状态之一。annotation— 顶层:用户/Agent 注释(如果存在,参见下文的注释)。
resource.inputs 对象还包含云提供商的原始数据:
对于 CloudFormation — resource.inputs.cloudFormation:
physicalResourceId— 原始 CF 物理 IDresourceType— CF 类型(例如AWS::IAM::Role)resourceStatus— 例如CREATE_COMPLETE、DELETE_COMPLETEdriftStatus—NOT_CHECKED、IN_SYNC、DRIFTED
对于 CDK 合成的 CF 堆栈,还会提供 inputs.cdkPath — 有关如何使用它,请参见 cloudformation.md §5。
对于 ARM — resource.inputs.arm。ARM 资源按资源组分组,而非按部署分组。因此,即使某个资源当前没有部署作为其依据(其部署历史已超出 Azure 的保留期限,或它是在任何受跟踪的部署之外创建的),仍会返回该资源 — 只是作为占位对象,此时 resource.inputs.deploymentName 为空,且 arm 结构不同:
- 有部署依据(
resource.inputs.deploymentName非空)—arm是一个部署操作对象:properties.targetResource.id/.resourceType/.resourceName、properties.provisioningState(例如Succeeded)。 - 占位对象(
resource.inputs.deploymentName为空)—arm则是原始的 Azure 通用资源对象:顶层包含id、name、type、location、tags。没有properties.targetResource— 不要查找它。
无论哪种情况,导入 ID 都应优先使用顶层的 resource.inputs.providerId(见上文),而不是从 arm 中查找 — 该值已针对两种结构进行了统一规范化。
迁移状态
状态采用 PascalCase。按顺序匹配,命中第一项后即停止:
Migrated— 已在compareToPulumi 堆栈中找到该资源。它已由 Pulumi 管理;跳过。Ready— 已设置providerType和providerId,且扫描器确认资源存在。使用pulumi import <providerType> <name> <providerId> --generate-code --out <file>.ts导入(绝不能省略--generate-code --out— 参见阶段 4)。NotFound— 已设置providerType和providerId,但扫描器无法确认资源的当前状态。资源可能已删除、映射可能不准确,或扫描器未能覆盖该资源。导入前请先核实。NotApplicable— 容器或包装类型(AWS::CloudFormation::Stack、Microsoft.Resources/deployments、Microsoft.Resources/resourceGroups、pulumi:providers:*)无法单独迁移。直接跳过。NoMatch—providerType为null。未找到映射。常见示例:- CF 自定义资源(例如
Custom::VpcRestrictDefaultSG)— 没有直接对应的 Pulumi 类型。 - 内联策略 —
AWS::IAM::Policy被建模为aws:iam/role:Role的内联属性。父级 Role 迁移后,将该策略注释为已迁移。
- CF 自定义资源(例如
PulumiOnly— 该资源存在于compareTo堆栈中,但没有发现对应资源;或它是为迁移NoMatch资源而创建的。向用户展示。
对于包含 annotation.statusOverride 的资源,应按该覆盖值视为已解决,即使计算出的状态与之不符。
注释
PUT /api/preview/insights/{orgName}/discovered-stacks/{projectName}/{stackName}/migration
DELETE /api/preview/insights/{orgName}/discovered-stacks/{projectName}/{stackName}/migration?resourceUrn={urn}
PUT 请求正文:
{
"resourceUrn": "<copy verbatim from resource.urn in the list response>",
"note": "explanation of what happened",
"statusOverride": "Migrated" | "",
"linkedResourceUrn": "<optional: URN of the Pulumi resource paired 1:1 with this origin>"
}
所有注释请求都使用 Pulumi Cloud API。note 和 statusOverride 中至少有一个必须为非空值。仅更新备注时,请完全省略 statusOverride(不要发送 null)。DELETE 端点会清除备注和任何 statusOverride。
使用注释来:
- 将已删除的资源标记为
Migrated,并备注说明资源已不存在 - 将父资源迁移所涵盖的内联/子资源标记为
Migrated,并在备注中注明父资源——此处不要使用linkedResourceUrn(这是 1 对多关系,而 UI 只会合并 1 对 1 的资源) - 在修正映射后,将
NotFound或NoMatch来源关联到其PulumiOnly对应资源(statusOverride=Migrated+linkedResourceUrn→ PulumiOnly URN)。这是linkedResourceUrn唯一有效的用途。 - 备注说明阻碍因素或已采取的手动步骤
- 记录自动分类器无法判定的资源的处理决定
注释端点是跟踪迁移进度的共享位置——对资源采取操作前,务必先读取现有注释,并尊重用户留下的覆盖设置。
note 字段用于记录用户提供的上下文。应将其视为针对该特定资源的高优先级指令。常见用途包括:命名偏好、导入 ID 提示、需要跳过的资源或特殊处理说明。Agent 可通过以下两个渠道获取备注:
-
起始提示词——用户启动迁移任务时,所选资源的任何备注都会附加到该资源对应的行中:
- aws:s3/bucket:Bucket "my-bucket" (provider ID: my-bucket-prod) — note: use logical name "appBucket" in code调用 API 前请先阅读这些内容。
-
API 响应——调用
GET .../resources时,DiscoveredResourceInfo中每个资源的annotation.note字段。如果未设置任何注释,则会完全省略annotation对象。
迁移工作流
阶段 0 — 前提条件与范围界定
调用任何工具前,先收集并确认以下所有信息。若有任何信息缺失,请先询问,不要猜测后继续。
来源(已发现的堆栈):
- 组织名称(例如
pulumi_local) - 已发现的项目名称(例如
AcmeCdkExampleStack) - 已发现的堆栈名称(例如
dev-sandbox-disc_us-west-2__Dev)——这是扫描器生成的名称,通常包含账号、区域和 CF/ARM 堆栈名称。 - 区域(即使堆栈名称似乎已表明区域,也要确认)。
目标(Pulumi 代码和状态的存放位置):
- 目标 Git 仓库 URL——询问用户。从一开始就要在此仓库中完成所有工作。
- 子文件夹(可选)——询问用户是否有偏好;默认使用仓库根目录。
- 目标项目和堆栈名称——询问用户,不要自行编造。如果堆栈已存在,在阶段 1 使用
compareTo;如果不存在,在阶段 2 创建。 - 目标语言——默认使用 TypeScript。
重构偏好(用于阶段 7):
- 用户是否拥有原始源代码(CDK 仓库、Bicep 项目、Terraform 模块)?如果有,请提供路径或 URL。这是阶段 7 的主要结构参考——重构后的 Pulumi 代码将参考其中的组件边界、文件布局和命名约定,并根据导入模型实际生成的内容进行调整。
- 用户对 Pulumi 输出是否有偏好的程序布局?(例如“每个服务一个文件”“匹配我现有的仓库结构”“无所谓”)
- 用户是否需要执行阶段 7,还是在零差异后(阶段 6)停止?
凭据:
- 云凭据对应的 ESC 环境(如果用户未提供,请询问;绝不要自行编造)。建议使用 ESC——参见
cloudformation.md §1或arm.md §1。
**在确认以上所有信息前,不要开始阶段 1。**向用户概述计划并等待批准。
阶段 1 — 资源获取与初步分类
每次都从这里开始,即使是在继续现有迁移时也是如此。API 和任何现有注释都是已完成工作情况的真实依据。如果目标仓库中已有 Pulumi 代码,请先阅读——它能告诉你项目约定、现有资源,以及之前的尝试进展到哪一步。
- 获取已发现的资源:
GET .../discovered-stacks/{projectName}/{stackName}/resources。- 目标堆栈已存在(继续迁移):追加
?compareTo=<targetProject>/<targetStack>。 - 目标堆栈不存在(全新迁移):省略
compareTo——如果找不到目标堆栈,API 会返回 404。阶段 2 创建堆栈后,后续调用即可包含该参数。
- 目标堆栈已存在(继续迁移):追加
- 将响应保存到磁盘——
./.migration/resources-baseline.json。 - 运行初步分类:
python3 <skill-base-dir>/scripts/triage.py .migration/resources-baseline.json——输出状态计数(计入注释覆盖项)和逐资源表格。 - 向用户说明计划:
“发现 N 个资源。A 个已迁移,M 个就绪,K 个未找到,J 个不匹配,L 个不可迁移容器。我会先导入就绪的资源,再验证未找到的资源,然后和你一起处理不匹配的资源。这样可以吗?”
获得确认后再编写任何代码。
阶段 2 — 设置目标仓库和 Pulumi 堆栈
- 克隆目标 Git 仓库(来自阶段 0),并在后续所有阶段中于该仓库内操作。如果仓库中已有代码,请先阅读,了解现有约定和资源,再添加新资源。
- 如果目标堆栈已存在(阶段 0 检查),请选择该堆栈。否则:
- 创建 Pulumi 项目:
pulumi new <language> --name <project> --stack <org>/<project>/<stack> --yes。
- 创建 Pulumi 项目:
- 设置提供程序配置:
pulumi config set aws:region <r>(或azure-native:location)。 - 如果提供了 ESC 环境,请关联该环境。
无需运行空的 pulumi up。
项目和堆栈设置、区域配置以及提供程序安装的具体命令,请参阅
cloudformation.md §2或arm.md §2。
阶段 3 — 构建导入文件
生成导入文件:python3 <skill-base-dir>/scripts/build_import.py .migration/resources-baseline.json .migration/import.json。此脚本会筛选就绪/未找到的资源(排除已有注释的资源),并将 API 字段映射为 Pulumi 导入格式(type ← providerType,name ← name,id ← resource.inputs.providerId)。
阶段 4 — 导入
使用阶段 3 中的 import.json。
始终使用 --generate-code --out——否则资源会进入状态,但没有对应代码,导致 pulumi preview 无法正常工作。
pulumi import --file import.json --generate-code --out batch.<ext>
# then: append generated code into the main program file and delete the batch file
按批次循环:导入 → 预览 → 提交 → 添加注释。 每批目标约 20 个资源。对于 CDK 堆栈,按顶层 cdkPath 分组;否则按资源类型前缀分组。
- 导入该批资源。
- 运行
pulumi preview——要求差异为零。继续之前先修复所有差异。 - 提交程序变更到分支。
- 为每个导入的资源添加注释——调用
PUT .../migration,附带说明,且不包含statusOverride。注释会在上下文重置后保留,并显示在 UI 中。
?compareTo 是实时进度信号——pulumi import 会立即写入状态,因此每次导入后,migrationStatus 都会变为 Migrated。不要运行 pulumi up(参见阶段 6)。
仅在计算出的状态有误时才使用 statusOverride:
statusOverride=Migrated(带有linkedResourceUrn)——资源由另一个资源覆盖(例如,内联 IAM 策略 → 父 Role、IGW 附加项 → IGW 等)。statusOverride=Migrated(带有说明,不带linkedResourceUrn)——资源已删除、已悬空,或没有对应的 Pulumi 资源。将其标记为已解决,使其从未完成工作中移除。
阶段 5 — NotFound 和 NoMatch 分类处理
默认策略:先尝试,再标注。 对于 NotFound 和映射修正这两种情况,尝试执行 pulumi import 是了解实际问题最快的方式。错误消息精准且可据此采取行动。
NotFound(已设置 providerType,状态尚未确认):
常见结果:
-
资源已删除。
pulumi import返回Preview failed: resource '<id>' does not exist。将statusOverride=Migrated写入标注,并在备注中引用原始错误。不要重试。 实际上,相当一部分 NotFound 资源最终被证实是已删除,而非映射错误。 -
providerType映射错误。 导入因类型验证错误或架构不匹配而失败。多个 CF 类型都有多种有效的 Pulumi 映射(VPC 网关附加 / VPN 与 IGW;S3 与 s3control;RDS 实例与集群实例;SES v2 与 v1;等)——我们的扫描器会选择一个主要映射,但它并不总是与资源匹配。在cloudformation.md §7中查找originType,在导入文件中覆盖providerType,然后重试。接着处理指纹匹配的副作用(见下一条)。 -
映射错误的副作用:出现 PulumiOnly。 Agent 使用修正后的
providerType导入时,与已发现资源的指纹匹配会失败。已发现资源仍会保持NotFound(或NoMatch),并出现新的PulumiOnly条目。将原始来源行标注为statusOverride=Migrated,并设置linkedResourceUrn指向该 PulumiOnly URN。 这样可在 UI 中关联两者,并保持记录整洁。 -
资源仍然存在,且映射正确。 导入成功。状态会自动切换为
Migrated。
NoMatch(providerType 为 null):
常见情况:
-
内联 IAM 策略。 名称与已迁移 Role 的前缀匹配的
AWS::IAM::Policy,是已作为 Role 导入时的inlinePolicies捕获的内联策略。将statusOverride=Migrated写入标注,并添加备注:"inline policy of <RoleName>"。 -
AWS::SecretsManager::SecretTargetAttachment没有直接的 Pulumi 映射。 Secret 与目标(RDS 集群等)之间的关联通过集群的凭据配置隐式建立。将statusOverride=Migrated写入标注,并添加说明此关联为隐式关联的备注。 -
CDK 自定义资源(
Custom::*)。通常是执行实际工作的 Lambda 处理程序。在cloudformation.md §5中查看已知的处理程序 → Pulumi 替换映射。未经用户确认,不要标注为Migrated——说明处理程序的作用,并让用户决定。 -
其他 NoMatch 类型。 在云提供商文档(CF 资源类型参考或 ARM 资源类型参考)中查找
originType,了解资源的用途,然后在 Pulumi 注册表中搜索匹配的提供程序类型。如果映射存在歧义,请告知用户并征求意见。
预先存在的 PulumiOnly 条目。 除上述映射修正产生的条目外,PulumiOnly 也涵盖目标堆栈中已存在但不属于此次迁移的资源。保留原样即可——无需标注。
有关特定云的查询命令(验证资源是否存在、查找导入 ID、查询云资源)、提供程序选择规则(
awsclassic 与aws-native、azure-native与azure)、Preview 解决工作流以及已知导入问题,请参阅cloudformation.md §3–§6或arm.md §3–§6。
阶段 6 — 对账与 PR
-
运行
pulumi preview——确认没有任何变更。任何差异都意味着生成的代码与导入的状态不匹配。差异分为三种:- 已移除(
-)——云端存在但代码未设置的字段。将真实的云端值添加到代码中。不要使用ignoreChanges。 - 已添加(
+)——代码设置了但提供程序未返回的字段。如果是计算得出或只读字段 → 使用ignoreChanges。如果是重新声明提供程序默认值 → 从代码中移除。 - 已更改(
~)——值不匹配。查询云端,确定正确的值并更新代码。绝不要用ignoreChanges掩盖差异。
对于复杂资源,预计需要进行 2–5 轮预览。**绝不要运行
pulumi up来解决差异——它会修改云端,而不是代码。**请参阅cloudformation.md §6或arm.md §6,了解特定云平台的差异模式。 - 已移除(
-
最后执行一次
GET resources?compareTo=...,并验证预期的分布(这反映当前后端状态——由于每次pulumi import都会写入状态,因此migrationStatus已是最新状态):Migrated— 所有已导入的资源PulumiOnly— 仅由 Pulumi 管理的资源,包括通过注解关联的任何已更正映射导入- 仍为
NoMatch的资源 — 每个都应有注解 NotApplicable(容器)— 静默跳过
-
在打开 PR 之前,先进入阶段 7(提供重构)。
预览结果干净且 API 分流中没有未解决的资源时,迁移在功能上即已完成。**不要运行 pulumi up。**已导入的状态已通过 pulumi import 同步到 Pulumi Cloud;对于迁移而言,pulumi up 没有任何需要执行的操作。
阶段 7 — 重构与可维护性审查(创建 PR 之前)
已实现零差异,但导入的代码是“扁平”的——包含硬编码值,没有跨资源引用,所有资源都位于顶层。创建 PR 之前,请向用户提议进行可读性重构。
7a — 提议并说明
如果阶段 0 未记录明确的重构偏好,或者用户尚未明确拒绝,现在询问:
“预览结果干净,所有资源也都已核对。在我创建 PR 之前,我想重构代码以提高可维护性——将硬编码值替换为跨资源引用,提取配置参数,并对相关资源分组。(如果你提供了源代码仓库,我会将其用作结构蓝图,参照其预期的文件布局和组件名称。)要我继续吗?”
如果用户拒绝,跳至阶段 8。
7b — 实施重构
阅读 refactor.md,了解策略、不变量和模板阅读参考。按以下优先顺序处理:
- 首先参考源代码仓库的结构 — 如果用户提供了源代码仓库,现在读取它(克隆仓库或使用阶段 0 中的本地路径)。以其文件布局、组件/模块边界和命名规范为指导。如果一组资源自然地对应源代码中的某个类、模块或子目录,且这种分组对导入的 Pulumi 程序仍然合理,就沿用该结构;否则,避免为了机械地匹配源代码而强行进行人为分组。
- 将字面量 ARN/ID 引用替换为跨资源输出引用。
- 提取配置参数(区域、账户 ID、环境标签)。
- 在源代码仓库或 CDK 路径表明存在自然分组时,合并为
ComponentResource类。 - 最后才拆分文件——并且只有在存在自然分组时才拆分。
每次进行非平凡更改后都运行 pulumi preview。整个过程中必须保持零差异。如果预览显示有差异,在尝试其他操作之前,先撤销这项更改——恢复步骤见 refactor.md § The invariant。
7c — 向用户讲解并确认可维护性
重构完成后,在创建 PR 之前向用户展示结果:
- 总结所做的更改:新建/重命名的文件、新增的组件、替换的字面量、添加的配置键。
- 如果顶层
index.ts(或同等文件)少于约 80 行,逐行讲解;否则说明模块划分。 - 指出需要判断的地方(例如:“我将 IAM 资源归入
iam.ts,以匹配源代码仓库中的lib/iam/目录——如果你更喜欢其他名称,请告诉我”)。 - 明确询问:“这种结构符合你对维护这段代码的预期吗?” 等待用户回复。如果用户要求更改,就进行修改(之后始终运行预览),并重复讲解,直到用户满意。
只有在用户确认结构可以接受后,才能继续进入阶段 8。
阶段 8 — PR 与迁移报告
生成迁移报告(见下文)并创建 PR。PR 将包含代码当前的状态——原始导入代码(如果用户跳过阶段 7),或重构后的版本(如果用户选择进行重构)。
迁移报告格式
在 PR 描述中包含:
- 概览 — 发现的源堆栈 → 目标 Pulumi 堆栈、区域、语言。
- 分类摘要 — 开始时和结束时各状态的数量。
- 资源映射表 — 名称、来源类型、提供程序类型、状态、备注。
- 缺口 — 未映射的资源及原因,并附注释。
- 进度 URL — 指向已发现堆栈比较端点的链接,用于持续跟踪。
- 后续步骤 — 待用户决定的事项、可选的重构。