mcpField
P
pulumi-migrate-from-discovered-stack

pulumi-migrate-from-discovered-stack

github.com/pulumi云与 DevOps78195 次安装3 周前更新
代码仓库

关于 pulumi-migrate-from-discovered-stack

此 skill 用于将 Pulumi Cloud Discovery 已扫描的云资源迁移到 Pulumi stack,采用先读取云状态再导入的流程。它通过 API 标注跟踪进度与例外,并以零差异预览和提交拉取请求为目标。

主要功能

  1. 01获取并分类已发现资源
  2. 02根据云状态导入资源
  3. 03通过 API 标注跟踪迁移
  4. 04用 pulumi preview 验证零差异
  5. 05生成迁移代码和报告

适用场景

  1. 01迁移已扫描的 CloudFormation stack
  2. 02迁移已扫描的 Azure ARM 资源
  3. 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.md
cloudformation.md
refactor.md
use_cases.yaml
agents/
openai.yaml
scripts/
build_import.py
triage.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 文件里。


计划调整

如果你在加载此技能前已经生成了迁移计划,请根据下方的工作流审查并更新计划——此处的阶段优先于任何先前的计划。继续操作前,请向用户说明调整后的计划。

成功标准

迁移完成需满足以下条件:

  1. 完整覆盖资源——每个已发现的资源都已导入,或者附有说明未导入原因的注释。
  2. 零差异——pulumi preview 不显示任何更改。这证明代码与云状态完全一致。
  3. 通过 API 跟踪进度——使用 compareTo 和迁移注释,让进度显示在 Pulumi Cloud 中,而不只是保存在 Agent 的记忆里。
  4. 以 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 物理 ID
  • resourceType — CF 类型(例如 AWS::IAM::Role)
  • resourceStatus — 例如 CREATE_COMPLETE、DELETE_COMPLETE
  • driftStatus — 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。按顺序匹配,命中第一项后即停止:

  1. Migrated — 已在 compareTo Pulumi 堆栈中找到该资源。它已由 Pulumi 管理;跳过。
  2. Ready — 已设置 providerType 和 providerId,且扫描器确认资源存在。使用 pulumi import <providerType> <name> <providerId> --generate-code --out <file>.ts 导入(绝不能省略 --generate-code --out — 参见阶段 4)。
  3. NotFound — 已设置 providerType 和 providerId,但扫描器无法确认资源的当前状态。资源可能已删除、映射可能不准确,或扫描器未能覆盖该资源。导入前请先核实。
  4. NotApplicable — 容器或包装类型(AWS::CloudFormation::Stack、Microsoft.Resources/deployments、Microsoft.Resources/resourceGroups、pulumi:providers:*)无法单独迁移。直接跳过。
  5. NoMatch — providerType 为 null。未找到映射。常见示例:
    • CF 自定义资源(例如 Custom::VpcRestrictDefaultSG)— 没有直接对应的 Pulumi 类型。
    • 内联策略 — AWS::IAM::Policy 被建模为 aws:iam/role:Role 的内联属性。父级 Role 迁移后,将该策略注释为已迁移。
  6. 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 可通过以下两个渠道获取备注:

  1. 起始提示词——用户启动迁移任务时,所选资源的任何备注都会附加到该资源对应的行中:

    - aws:s3/bucket:Bucket "my-bucket" (provider ID: my-bucket-prod) — note: use logical name "appBucket" in code
    

    调用 API 前请先阅读这些内容。

  2. 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 代码,请先阅读——它能告诉你项目约定、现有资源,以及之前的尝试进展到哪一步。

  1. 获取已发现的资源:GET .../discovered-stacks/{projectName}/{stackName}/resources。
    • 目标堆栈已存在(继续迁移):追加 ?compareTo=<targetProject>/<targetStack>。
    • 目标堆栈不存在(全新迁移):省略 compareTo——如果找不到目标堆栈,API 会返回 404。阶段 2 创建堆栈后,后续调用即可包含该参数。
  2. 将响应保存到磁盘——./.migration/resources-baseline.json。
  3. 运行初步分类:python3 <skill-base-dir>/scripts/triage.py .migration/resources-baseline.json——输出状态计数(计入注释覆盖项)和逐资源表格。
  4. 向用户说明计划:

    “发现 N 个资源。A 个已迁移,M 个就绪,K 个未找到,J 个不匹配,L 个不可迁移容器。我会先导入就绪的资源,再验证未找到的资源,然后和你一起处理不匹配的资源。这样可以吗?”

获得确认后再编写任何代码。

阶段 2 — 设置目标仓库和 Pulumi 堆栈
  1. 克隆目标 Git 仓库(来自阶段 0),并在后续所有阶段中于该仓库内操作。如果仓库中已有代码,请先阅读,了解现有约定和资源,再添加新资源。
  2. 如果目标堆栈已存在(阶段 0 检查),请选择该堆栈。否则:
    • 创建 Pulumi 项目:pulumi new <language> --name <project> --stack <org>/<project>/<stack> --yes。
  3. 设置提供程序配置:pulumi config set aws:region <r>(或 azure-native:location)。
  4. 如果提供了 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 分组;否则按资源类型前缀分组。

  1. 导入该批资源。
  2. 运行 pulumi preview——要求差异为零。继续之前先修复所有差异。
  3. 提交程序变更到分支。
  4. 为每个导入的资源添加注释——调用 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,状态尚未确认):

常见结果:

  1. 资源已删除。 pulumi import 返回 Preview failed: resource '<id>' does not exist。将 statusOverride=Migrated 写入标注,并在备注中引用原始错误。不要重试。 实际上,相当一部分 NotFound 资源最终被证实是已删除,而非映射错误。

  2. providerType 映射错误。 导入因类型验证错误或架构不匹配而失败。多个 CF 类型都有多种有效的 Pulumi 映射(VPC 网关附加 / VPN 与 IGW;S3 与 s3control;RDS 实例与集群实例;SES v2 与 v1;等)——我们的扫描器会选择一个主要映射,但它并不总是与资源匹配。在 cloudformation.md §7 中查找 originType,在导入文件中覆盖 providerType,然后重试。接着处理指纹匹配的副作用(见下一条)。

  3. 映射错误的副作用:出现 PulumiOnly。 Agent 使用修正后的 providerType 导入时,与已发现资源的指纹匹配会失败。已发现资源仍会保持 NotFound(或 NoMatch),并出现新的 PulumiOnly 条目。将原始来源行标注为 statusOverride=Migrated,并设置 linkedResourceUrn 指向该 PulumiOnly URN。 这样可在 UI 中关联两者,并保持记录整洁。

  4. 资源仍然存在,且映射正确。 导入成功。状态会自动切换为 Migrated。

NoMatch(providerType 为 null):

常见情况:

  1. 内联 IAM 策略。 名称与已迁移 Role 的前缀匹配的 AWS::IAM::Policy,是已作为 Role 导入时的 inlinePolicies 捕获的内联策略。将 statusOverride=Migrated 写入标注,并添加备注:"inline policy of <RoleName>"。

  2. AWS::SecretsManager::SecretTargetAttachment 没有直接的 Pulumi 映射。 Secret 与目标(RDS 集群等)之间的关联通过集群的凭据配置隐式建立。将 statusOverride=Migrated 写入标注,并添加说明此关联为隐式关联的备注。

  3. CDK 自定义资源(Custom::*)。通常是执行实际工作的 Lambda 处理程序。在 cloudformation.md §5 中查看已知的处理程序 → Pulumi 替换映射。未经用户确认,不要标注为 Migrated——说明处理程序的作用,并让用户决定。

  4. 其他 NoMatch 类型。 在云提供商文档(CF 资源类型参考或 ARM 资源类型参考)中查找 originType,了解资源的用途,然后在 Pulumi 注册表中搜索匹配的提供程序类型。如果映射存在歧义,请告知用户并征求意见。

预先存在的 PulumiOnly 条目。 除上述映射修正产生的条目外,PulumiOnly 也涵盖目标堆栈中已存在但不属于此次迁移的资源。保留原样即可——无需标注。

有关特定云的查询命令(验证资源是否存在、查找导入 ID、查询云资源)、提供程序选择规则(aws classic 与 aws-native、azure-native 与 azure)、Preview 解决工作流以及已知导入问题,请参阅 cloudformation.md §3–§6 或 arm.md §3–§6。

阶段 6 — 对账与 PR
  1. 运行 pulumi preview——确认没有任何变更。任何差异都意味着生成的代码与导入的状态不匹配。差异分为三种:

    • 已移除(-)——云端存在但代码未设置的字段。将真实的云端值添加到代码中。不要使用 ignoreChanges。
    • 已添加(+)——代码设置了但提供程序未返回的字段。如果是计算得出或只读字段 → 使用 ignoreChanges。如果是重新声明提供程序默认值 → 从代码中移除。
    • 已更改(~)——值不匹配。查询云端,确定正确的值并更新代码。绝不要用 ignoreChanges 掩盖差异。

    对于复杂资源,预计需要进行 2–5 轮预览。**绝不要运行 pulumi up 来解决差异——它会修改云端,而不是代码。**请参阅 cloudformation.md §6 或 arm.md §6,了解特定云平台的差异模式。

  2. 最后执行一次 GET resources?compareTo=...,并验证预期的分布(这反映当前后端状态——由于每次 pulumi import 都会写入状态,因此 migrationStatus 已是最新状态):

    • Migrated — 所有已导入的资源
    • PulumiOnly — 仅由 Pulumi 管理的资源,包括通过注解关联的任何已更正映射导入
    • 仍为 NoMatch 的资源 — 每个都应有注解
    • NotApplicable(容器)— 静默跳过
  3. 在打开 PR 之前,先进入阶段 7(提供重构)。

预览结果干净且 API 分流中没有未解决的资源时,迁移在功能上即已完成。**不要运行 pulumi up。**已导入的状态已通过 pulumi import 同步到 Pulumi Cloud;对于迁移而言,pulumi up 没有任何需要执行的操作。


阶段 7 — 重构与可维护性审查(创建 PR 之前)

已实现零差异,但导入的代码是“扁平”的——包含硬编码值,没有跨资源引用,所有资源都位于顶层。创建 PR 之前,请向用户提议进行可读性重构。

7a — 提议并说明

如果阶段 0 未记录明确的重构偏好,或者用户尚未明确拒绝,现在询问:

“预览结果干净,所有资源也都已核对。在我创建 PR 之前,我想重构代码以提高可维护性——将硬编码值替换为跨资源引用,提取配置参数,并对相关资源分组。(如果你提供了源代码仓库,我会将其用作结构蓝图,参照其预期的文件布局和组件名称。)要我继续吗?”

如果用户拒绝,跳至阶段 8。

7b — 实施重构

阅读 refactor.md,了解策略、不变量和模板阅读参考。按以下优先顺序处理:

  1. 首先参考源代码仓库的结构 — 如果用户提供了源代码仓库,现在读取它(克隆仓库或使用阶段 0 中的本地路径)。以其文件布局、组件/模块边界和命名规范为指导。如果一组资源自然地对应源代码中的某个类、模块或子目录,且这种分组对导入的 Pulumi 程序仍然合理,就沿用该结构;否则,避免为了机械地匹配源代码而强行进行人为分组。
  2. 将字面量 ARN/ID 引用替换为跨资源输出引用。
  3. 提取配置参数(区域、账户 ID、环境标签)。
  4. 在源代码仓库或 CDK 路径表明存在自然分组时,合并为 ComponentResource 类。
  5. 最后才拆分文件——并且只有在存在自然分组时才拆分。

每次进行非平凡更改后都运行 pulumi preview。整个过程中必须保持零差异。如果预览显示有差异,在尝试其他操作之前,先撤销这项更改——恢复步骤见 refactor.md § The invariant。

7c — 向用户讲解并确认可维护性

重构完成后,在创建 PR 之前向用户展示结果:

  1. 总结所做的更改:新建/重命名的文件、新增的组件、替换的字面量、添加的配置键。
  2. 如果顶层 index.ts(或同等文件)少于约 80 行,逐行讲解;否则说明模块划分。
  3. 指出需要判断的地方(例如:“我将 IAM 资源归入 iam.ts,以匹配源代码仓库中的 lib/iam/ 目录——如果你更喜欢其他名称,请告诉我”)。
  4. 明确询问:“这种结构符合你对维护这段代码的预期吗?” 等待用户回复。如果用户要求更改,就进行修改(之后始终运行预览),并重复讲解,直到用户满意。

只有在用户确认结构可以接受后,才能继续进入阶段 8。


阶段 8 — PR 与迁移报告

生成迁移报告(见下文)并创建 PR。PR 将包含代码当前的状态——原始导入代码(如果用户跳过阶段 7),或重构后的版本(如果用户选择进行重构)。


迁移报告格式

在 PR 描述中包含:

  1. 概览 — 发现的源堆栈 → 目标 Pulumi 堆栈、区域、语言。
  2. 分类摘要 — 开始时和结束时各状态的数量。
  3. 资源映射表 — 名称、来源类型、提供程序类型、状态、备注。
  4. 缺口 — 未映射的资源及原因,并附注释。
  5. 进度 URL — 指向已发现堆栈比较端点的链接,用于持续跟踪。
  6. 后续步骤 — 待用户决定的事项、可选的重构。

评论

支持 Markdown