pulumi-arm-to-pulumi

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

IMPORTANT: PLAN ADJUSTMENT REQUIRED

重要提示:需调整计划

If you have already generated a migration plan before loading this skill, you MUST:
  1. Review your existing plan against the workflows and requirements in this skill
  2. Identify any gaps, missing steps, or incorrect assumptions
  3. Update and revise your plan to align with this skill's guidance
  4. Communicate the adjusted plan to the user before proceeding
如果您在加载此技能前已生成迁移计划,必须执行以下操作:
  1. 对照此技能中的工作流和要求,审核现有计划
  2. 找出任何缺口、缺失步骤或错误假设
  3. 更新并修订计划,使其与本技能的指导方针保持一致
  4. 在继续操作前,将调整后的计划告知用户

CRITICAL SUCCESS REQUIREMENTS

关键成功要求

The migration output MUST meet all of the following:
  1. Complete Resource Coverage
    • Every ARM template resource MUST:
      • Be represented in the Pulumi program OR
      • Be explicitly justified in the final report.
  2. Successful Deployment
    • The produced Pulumi program must be structurally valid and capable of a successful
      pulumi preview
      (assuming proper config).
  3. Zero-Diff Import Validation (if importing existing resources)
    • After import,
      pulumi preview
      must show:
      • NO updates
      • NO replaces
      • NO creates
      • NO deletes
    • Any diffs must be resolved using the Preview Resolution Workflow. See arm-import.md.
  4. Final Migration Report
    • Always output a formal migration report suitable for a Pull Request.
    • Include:
      • ARM → Pulumi resource mapping
      • Provider decisions (azure-native vs azure)
      • Behavioral differences
      • Missing or manually required steps
      • Validation instructions
迁移输出必须满足以下所有条件:
  1. 完整资源覆盖
    • 每个ARM模板资源必须:
      • 在Pulumi程序中有所体现 或者
      • 在最终报告中给出明确的合理说明。
  2. 部署成功
    • 生成的Pulumi程序必须结构有效,并且在配置正确的情况下能够成功执行
      pulumi preview
  3. 零差异导入验证(如果导入现有资源)
    • 导入后,
      pulumi preview
      必须显示:
      • 无更新
      • 无替换
      • 无创建
      • 无删除
    • 任何差异必须使用预览解决工作流来处理。请参阅arm-import.md
  4. 最终迁移报告
    • 始终输出适合用于拉取请求的正式迁移报告。
    • 报告需包含:
      • ARM → Pulumi资源映射
      • 提供商选择决策(azure-native vs azure)
      • 行为差异
      • 缺失或需要手动执行的步骤
      • 验证说明

WHEN INFORMATION IS MISSING

当信息缺失时

If a user-provided ARM template is incomplete, ambiguous, or missing artifacts, ask targeted questions before generating Pulumi code.
If there is ambiguity on how to handle a specific resource property on import, ask targeted questions before altering Pulumi code.
如果用户提供的ARM模板不完整、模糊或缺少相关构件,在生成Pulumi代码前请提出针对性问题
如果在导入时对如何处理特定资源属性存在疑问,在修改Pulumi代码前请提出针对性问题

MIGRATION WORKFLOW

迁移工作流

Follow this workflow exactly and in this order:
请严格按照以下顺序执行此工作流:

1. INFORMATION GATHERING

1. 信息收集

1.1 Verify Azure Credentials

1.1 验证Azure凭据

Running Azure CLI commands (e.g.,
az resource list
,
az resource show
). Requires initial login using ESC and
az login
  • If the user has already provided an ESC environment, use it.
  • If no ESC environment is specified, ask the user which ESC environment to use before proceeding with Azure CLI commands.
Setting up Azure CLI using ESC:
  • ESC environments can provide Azure credentials through environment variables or Azure CLI configuration
  • Login to Azure using ESC to provide credentials, e.g:
    pulumi env run {org}/{project}/{environment} -- bash -c 'az login --service-principal -u "$ARM_CLIENT_ID" --tenant "$ARM_TENANT_ID" --federated-token "$ARM_OIDC_TOKEN"'
    . ESC is not required after establishing the session
  • Verify credentials are working:
    az account show
  • Confirm subscription:
    az account list --query "[].{Name:name, SubscriptionId:id, IsDefault:isDefault}" -o table
For detailed ESC information: Load the
pulumi-esc
skill by calling the tool "Skill" with name = "pulumi-esc"
运行Azure CLI命令(例如:
az resource list
az resource show
)。需要先使用ESC和
az login
登录
  • 如果用户已提供ESC环境,请使用该环境。
  • 如果未指定ESC环境,在执行Azure CLI命令前请询问用户使用哪个ESC环境
使用ESC设置Azure CLI:
  • ESC环境可通过环境变量或Azure CLI配置提供Azure凭据
  • 使用ESC登录Azure以提供凭据,例如:
    pulumi env run {org}/{project}/{environment} -- bash -c 'az login --service-principal -u "$ARM_CLIENT_ID" --tenant "$ARM_TENANT_ID" --federated-token "$ARM_OIDC_TOKEN"'
    。建立会话后无需再使用ESC
  • 验证凭据是否可用:
    az account show
  • 确认订阅:
    az account list --query "[].{Name:name, SubscriptionId:id, IsDefault:isDefault}" -o table
如需详细的ESC信息: 调用工具"Skill"并指定名称为"pulumi-esc"来加载
pulumi-esc
技能

1.2 Analyze ARM Template Structure

1.2 分析ARM模板结构

ARM templates do not have the concept of "stacks" like CloudFormation. Read the ARM template JSON file directly:
bash
undefined
ARM模板没有像CloudFormation那样的“栈”概念。直接读取ARM模板JSON文件:
bash
undefined

View template structure

查看模板结构

cat template.json | jq '.resources[] | {type: .type, name: .name}'
cat template.json | jq '.resources[] | {type: .type, name: .name}'

View parameters

查看参数

cat template.json | jq '.parameters'
cat template.json | jq '.parameters'

View variables

查看变量

cat template.json | jq '.variables'

Extract:

- Resource types and names
- Parameters and their default values
- Variables and expressions
- Dependencies (dependsOn arrays)
- Nested templates or linked templates
- Copy loops (iteration constructs)
- Conditional deployments (condition property)

**Documentation:** [ARM Template Structure](https://learn.microsoft.com/en-us/azure/azure-resource-manager/templates/syntax)
cat template.json | jq '.variables'

提取以下信息:

- 资源类型和名称
- 参数及其默认值
- 变量和表达式
- 依赖关系(dependsOn数组)
- 嵌套模板或链接模板
- 复制循环(迭代结构)
- 条件部署(condition属性)

**文档:** [ARM模板结构](https://learn.microsoft.com/en-us/azure/azure-resource-manager/templates/syntax)

1.3 Build Resource Inventory (if importing existing resources)

1.3 构建资源清单(如果导入现有资源)

If the ARM template has already been deployed and you're importing existing resources:
bash
undefined
如果ARM模板已部署,且您要导入现有资源:
bash
undefined

List all resources in a resource group

列出资源组中的所有资源

az resource list
--resource-group <resource-group-name>
--output json
az resource list
--resource-group <resource-group-name>
--output json

Get specific resource details

获取特定资源的详细信息

az resource show
--ids <resource-id>
--output json
az resource show
--ids <resource-id>
--output json

Query specific properties using JMESPath

使用JMESPath查询特定属性

az resource show
--ids <resource-id>
--query "{name:name, location:location, properties:properties}"
--output json

**Documentation:** [Azure CLI Documentation](https://learn.microsoft.com/en-us/cli/azure/)
az resource show
--ids <resource-id>
--query "{name:name, location:location, properties:properties}"
--output json

**文档:** [Azure CLI文档](https://learn.microsoft.com/en-us/cli/azure/)

2. CODE CONVERSION (ARM → PULUMI)

2. 代码转换(ARM → PULUMI)

IMPORTANT: ARM to Pulumi conversion requires manual translation. There is NO automated conversion tool for ARM templates. You are responsible for the complete conversion.
重要提示: ARM到Pulumi的转换需要手动翻译。目前没有针对ARM模板的自动化转换工具。您需负责完成完整的转换工作。

Key Conversion Principles

核心转换原则

  1. Provider Strategy:
    • Default: Use
      @pulumi/azure-native
      for full Azure Resource Manager API coverage
    • Fallback: Use
      @pulumi/azure
      (classic provider) when azure-native doesn't support specific features or when you need simplified abstractions
    Documentation:
  2. Language Support:
    • TypeScript/JavaScript: Most common, excellent IDE support
    • Python: Great for data teams and ML workflows
    • C#: Natural fit for .NET teams
    • Go: High performance, strong typing
    • Java: Enterprise Java teams
    • YAML: Simple declarative approach
    • Choose based on user preference or existing codebase
  3. Complete Coverage:
    • Convert ALL resources in the ARM template
    • Preserve all conditionals, loops, and dependencies
    • Maintain parameter and variable logic
  1. 提供商策略:
    • 默认选择:使用
      @pulumi/azure-native
      以获得完整的Azure资源管理器API覆盖
    • 备选方案:当azure-native不支持特定功能,或者您需要简化的抽象时,使用
      @pulumi/azure
      (经典提供商)
    文档:
  2. 语言支持:
    • TypeScript/JavaScript:最常用,具备出色的IDE支持
    • Python:非常适合数据团队和ML工作流
    • C#:.NET团队的自然选择
    • Go:高性能,强类型
    • Java:面向企业Java团队
    • YAML:简单的声明式方法
    • 根据用户偏好或现有代码库选择合适的语言
  3. 完整覆盖:
    • 转换ARM模板中的所有资源
    • 保留所有条件、循环和依赖关系
    • 维护参数和变量逻辑

ARM Template Conversion Patterns

ARM模板转换模式

Basic Resource Conversion
基础资源转换
ARM Template:
json
{
  "type": "Microsoft.Storage/storageAccounts",
  "apiVersion": "2023-01-01",
  "name": "[parameters('storageAccountName')]",
  "location": "[parameters('location')]",
  "sku": {
    "name": "Standard_LRS"
  },
  "kind": "StorageV2",
  "properties": {
    "supportsHttpsTrafficOnly": true
  }
}
Pulumi TypeScript:
typescript
import * as pulumi from "@pulumi/pulumi";
import * as azure_native from "@pulumi/azure-native";

const config = new pulumi.Config();
const storageAccountName = config.require("storageAccountName");
const location = config.require("location");
const resourceGroupName = config.require("resourceGroupName");

const storageAccount = new azure_native.storage.StorageAccount("storageAccount", {
    accountName: storageAccountName,
    location: location,
    resourceGroupName: resourceGroupName,
    sku: {
        name: azure_native.storage.SkuName.Standard_LRS,
    },
    kind: azure_native.storage.Kind.StorageV2,
    enableHttpsTrafficOnly: true,
});
ARM模板:
json
{
  "type": "Microsoft.Storage/storageAccounts",
  "apiVersion": "2023-01-01",
  "name": "[parameters('storageAccountName')]",
  "location": "[parameters('location')]",
  "sku": {
    "name": "Standard_LRS"
  },
  "kind": "StorageV2",
  "properties": {
    "supportsHttpsTrafficOnly": true
  }
}
Pulumi TypeScript:
typescript
import * as pulumi from "@pulumi/pulumi";
import * as azure_native from "@pulumi/azure-native";

const config = new pulumi.Config();
const storageAccountName = config.require("storageAccountName");
const location = config.require("location");
const resourceGroupName = config.require("resourceGroupName");

const storageAccount = new azure_native.storage.StorageAccount("storageAccount", {
    accountName: storageAccountName,
    location: location,
    resourceGroupName: resourceGroupName,
    sku: {
        name: azure_native.storage.SkuName.Standard_LRS,
    },
    kind: azure_native.storage.Kind.StorageV2,
    enableHttpsTrafficOnly: true,
});
ARM Parameters → Pulumi Config
ARM参数 → Pulumi配置
ARM Template:
json
{
  "parameters": {
    "location": {
      "type": "string",
      "defaultValue": "eastus",
      "metadata": {
        "description": "Location for resources"
      }
    },
    "instanceCount": {
      "type": "int",
      "defaultValue": 2,
      "minValue": 1,
      "maxValue": 10
    },
    "enableBackup": {
      "type": "bool",
      "defaultValue": true
    },
    "secretValue": {
      "type": "securestring"
    }
  }
}
Pulumi TypeScript:
typescript
const config = new pulumi.Config();
const location = config.get("location") || "eastus";
const instanceCount = config.getNumber("instanceCount") || 2;
const enableBackup = config.getBoolean("enableBackup") ?? true;
const secretValue = config.requireSecret("secretValue"); // Returns Output<string>
ARM模板:
json
{
  "parameters": {
    "location": {
      "type": "string",
      "defaultValue": "eastus",
      "metadata": {
        "description": "Location for resources"
      }
    },
    "instanceCount": {
      "type": "int",
      "defaultValue": 2,
      "minValue": 1,
      "maxValue": 10
    },
    "enableBackup": {
      "type": "bool",
      "defaultValue": true
    },
    "secretValue": {
      "type": "securestring"
    }
  }
}
Pulumi TypeScript:
typescript
const config = new pulumi.Config();
const location = config.get("location") || "eastus";
const instanceCount = config.getNumber("instanceCount") || 2;
const enableBackup = config.getBoolean("enableBackup") ?? true;
const secretValue = config.requireSecret("secretValue"); // 返回Output<string>
ARM Variables → Pulumi Variables
ARM变量 → Pulumi变量
ARM Template:
json
{
  "variables": {
    "storageAccountName": "[concat('storage', uniqueString(resourceGroup().id))]",
    "webAppName": "[concat(parameters('prefix'), '-webapp')]"
  }
}
Pulumi TypeScript:
typescript
import * as pulumi from "@pulumi/pulumi";

const config = new pulumi.Config();
const prefix = config.require("prefix");
const resourceGroupId = config.require("resourceGroupId");

// Simple variable
const webAppName = `${prefix}-webapp`;

// For uniqueString equivalent, use stack name or generate hash
const storageAccountName = `storage${resourceGroupId}`.toLowerCase();
ARM模板:
json
{
  "variables": {
    "storageAccountName": "[concat('storage', uniqueString(resourceGroup().id))]",
    "webAppName": "[concat(parameters('prefix'), '-webapp')]"
  }
}
Pulumi TypeScript:
typescript
import * as pulumi from "@pulumi/pulumi";

const config = new pulumi.Config();
const prefix = config.require("prefix");
const resourceGroupId = config.require("resourceGroupId");

// 简单变量
const webAppName = `${prefix}-webapp`;

// 替代uniqueString,使用栈名称或生成哈希
const storageAccountName = `storage${resourceGroupId}`.toLowerCase();
ARM Copy Loops → Pulumi Loops
ARM复制循环 → Pulumi循环
ARM Template:
json
{
  "type": "Microsoft.Network/virtualNetworks/subnets",
  "apiVersion": "2023-05-01",
  "name": "[concat(variables('vnetName'), '/subnet-', copyIndex())]",
  "copy": {
    "name": "subnetCopy",
    "count": "[parameters('subnetCount')]"
  },
  "properties": {
    "addressPrefix": "[concat('10.0.', copyIndex(), '.0/24')]"
  }
}
Pulumi TypeScript:
typescript
const config = new pulumi.Config();
const subnetCount = config.getNumber("subnetCount") || 3;

const subnets: azure_native.network.Subnet[] = [];
for (let i = 0; i < subnetCount; i++) {
    subnets.push(new azure_native.network.Subnet(`subnet-${i}`, {
        subnetName: `subnet-${i}`,
        virtualNetworkName: vnet.name,
        resourceGroupName: resourceGroup.name,
        addressPrefix: `10.0.${i}.0/24`,
    }));
}
ARM模板:
json
{
  "type": "Microsoft.Network/virtualNetworks/subnets",
  "apiVersion": "2023-05-01",
  "name": "[concat(variables('vnetName'), '/subnet-', copyIndex())]",
  "copy": {
    "name": "subnetCopy",
    "count": "[parameters('subnetCount')]"
  },
  "properties": {
    "addressPrefix": "[concat('10.0.', copyIndex(), '.0/24')]"
  }
}
Pulumi TypeScript:
typescript
const config = new pulumi.Config();
const subnetCount = config.getNumber("subnetCount") || 3;

const subnets: azure_native.network.Subnet[] = [];
for (let i = 0; i < subnetCount; i++) {
    subnets.push(new azure_native.network.Subnet(`subnet-${i}`, {
        subnetName: `subnet-${i}`,
        virtualNetworkName: vnet.name,
        resourceGroupName: resourceGroup.name,
        addressPrefix: `10.0.${i}.0/24`,
    }));
}
ARM Conditional Resources → Pulumi Conditionals
ARM条件资源 → Pulumi条件语句
ARM Template:
json
{
  "type": "Microsoft.Network/publicIPAddresses",
  "apiVersion": "2023-05-01",
  "condition": "[parameters('createPublicIP')]",
  "name": "[variables('publicIPName')]",
  "location": "[parameters('location')]"
}
Pulumi TypeScript:
typescript
const config = new pulumi.Config();
const createPublicIP = config.getBoolean("createPublicIP") ?? false;

let publicIP: azure_native.network.PublicIPAddress | undefined;
if (createPublicIP) {
    publicIP = new azure_native.network.PublicIPAddress("publicIP", {
        publicIpAddressName: publicIPName,
        location: location,
        resourceGroupName: resourceGroup.name,
    });
}

// Handle optional references
const publicIPId = publicIP ? publicIP.id : pulumi.output(undefined);
ARM模板:
json
{
  "type": "Microsoft.Network/publicIPAddresses",
  "apiVersion": "2023-05-01",
  "condition": "[parameters('createPublicIP')]",
  "name": "[variables('publicIPName')]",
  "location": "[parameters('location')]"
}
Pulumi TypeScript:
typescript
const config = new pulumi.Config();
const createPublicIP = config.getBoolean("createPublicIP") ?? false;

let publicIP: azure_native.network.PublicIPAddress | undefined;
if (createPublicIP) {
    publicIP = new azure_native.network.PublicIPAddress("publicIP", {
        publicIpAddressName: publicIPName,
        location: location,
        resourceGroupName: resourceGroup.name,
    });
}

// 处理可选引用
const publicIPId = publicIP ? publicIP.id : pulumi.output(undefined);
ARM DependsOn → Pulumi Dependencies
ARM DependsOn → Pulumi依赖关系
ARM Template:
json
{
  "type": "Microsoft.Web/sites",
  "apiVersion": "2023-01-01",
  "name": "[variables('webAppName')]",
  "dependsOn": [
    "[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanName'))]"
  ]
}
Pulumi TypeScript:
typescript
// Implicit dependency (preferred)
const webApp = new azure_native.web.WebApp("webApp", {
    name: webAppName,
    resourceGroupName: resourceGroup.name,
    serverFarmId: appServicePlan.id, // Implicit dependency through property reference
});

// Explicit dependency (when needed)
const webApp = new azure_native.web.WebApp("webApp", {
    name: webAppName,
    resourceGroupName: resourceGroup.name,
    serverFarmId: appServicePlan.id,
}, {
    dependsOn: [appServicePlan], // Explicit dependency
});
ARM模板:
json
{
  "type": "Microsoft.Web/sites",
  "apiVersion": "2023-01-01",
  "name": "[variables('webAppName')]",
  "dependsOn": [
    "[resourceId('Microsoft.Web/serverfarms', variables('appServicePlanName'))]"
  ]
}
Pulumi TypeScript:
typescript
// 隐式依赖(推荐)
const webApp = new azure_native.web.WebApp("webApp", {
    name: webAppName,
    resourceGroupName: resourceGroup.name,
    serverFarmId: appServicePlan.id, // 通过属性引用实现隐式依赖
});

// 显式依赖(必要时使用)
const webApp = new azure_native.web.WebApp("webApp", {
    name: webAppName,
    resourceGroupName: resourceGroup.name,
    serverFarmId: appServicePlan.id,
}, {
    dependsOn: [appServicePlan], // 显式依赖
});
Nested Templates → Pulumi Component Resources
嵌套模板 → Pulumi组件资源
ARM Template:
json
{
  "type": "Microsoft.Resources/deployments",
  "apiVersion": "2021-04-01",
  "name": "nestedTemplate",
  "properties": {
    "mode": "Incremental",
    "template": {
      "resources": [...]
    }
  }
}
Pulumi Approach:
Instead of nested templates, use Pulumi ComponentResource to group related resources:
typescript
class NetworkComponent extends pulumi.ComponentResource {
    public readonly vnet: azure_native.network.VirtualNetwork;
    public readonly subnets: azure_native.network.Subnet[];

    constructor(name: string, args: NetworkComponentArgs, opts?: pulumi.ComponentResourceOptions) {
        super("custom:azure:NetworkComponent", name, {}, opts);

        const defaultOptions = { parent: this };

        this.vnet = new azure_native.network.VirtualNetwork(`${name}-vnet`, {
            virtualNetworkName: args.vnetName,
            resourceGroupName: args.resourceGroupName,
            location: args.location,
            addressSpace: {
                addressPrefixes: [args.addressPrefix],
            },
        }, defaultOptions);

        this.subnets = args.subnets.map((subnet, i) =>
            new azure_native.network.Subnet(`${name}-subnet-${i}`, {
                subnetName: subnet.name,
                virtualNetworkName: this.vnet.name,
                resourceGroupName: args.resourceGroupName,
                addressPrefix: subnet.addressPrefix,
            }, defaultOptions)
        );

        this.registerOutputs({
            vnetId: this.vnet.id,
            subnetIds: this.subnets.map(s => s.id),
        });
    }
}
ARM模板:
json
{
  "type": "Microsoft.Resources/deployments",
  "apiVersion": "2021-04-01",
  "name": "nestedTemplate",
  "properties": {
    "mode": "Incremental",
    "template": {
      "resources": [...]
    }
  }
}
Pulumi实现方式:
不使用嵌套模板,而是使用Pulumi ComponentResource对相关资源进行分组:
typescript
class NetworkComponent extends pulumi.ComponentResource {
    public readonly vnet: azure_native.network.VirtualNetwork;
    public readonly subnets: azure_native.network.Subnet[];

    constructor(name: string, args: NetworkComponentArgs, opts?: pulumi.ComponentResourceOptions) {
        super("custom:azure:NetworkComponent", name, {}, opts);

        const defaultOptions = { parent: this };

        this.vnet = new azure_native.network.VirtualNetwork(`${name}-vnet`, {
            virtualNetworkName: args.vnetName,
            resourceGroupName: args.resourceGroupName,
            location: args.location,
            addressSpace: {
                addressPrefixes: [args.addressPrefix],
            },
        }, defaultOptions);

        this.subnets = args.subnets.map((subnet, i) =>
            new azure_native.network.Subnet(`${name}-subnet-${i}`, {
                subnetName: subnet.name,
                virtualNetworkName: this.vnet.name,
                resourceGroupName: args.resourceGroupName,
                addressPrefix: subnet.addressPrefix,
            }, defaultOptions)
        );

        this.registerOutputs({
            vnetId: this.vnet.id,
            subnetIds: this.subnets.map(s => s.id),
        });
    }
}
ARM Outputs → Pulumi Exports
ARM输出 → Pulumi导出
ARM Template:
json
{
  "outputs": {
    "storageAccountName": {
      "type": "string",
      "value": "[variables('storageAccountName')]"
    },
    "storageAccountId": {
      "type": "string",
      "value": "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]"
    }
  }
}
Pulumi TypeScript:
typescript
export const storageAccountName = storageAccount.name;
export const storageAccountId = storageAccount.id;
ARM模板:
json
{
  "outputs": {
    "storageAccountName": {
      "type": "string",
      "value": "[variables('storageAccountName')]"
    },
    "storageAccountId": {
      "type": "string",
      "value": "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]"
    }
  }
}
Pulumi TypeScript:
typescript
export const storageAccountName = storageAccount.name;
export const storageAccountId = storageAccount.id;

Azure Classic Provider Examples

Azure经典提供商示例

In some cases, you may need to use the Azure Classic provider (
@pulumi/azure
) instead of Azure Native. The Classic provider offers simplified abstractions and may be easier to work with for certain resources.
在某些情况下,您可能需要使用Azure经典提供商(
@pulumi/azure
)而非Azure Native。经典提供商提供简化的抽象,对于某些资源来说可能更易于使用。
Virtual Network with Classic Provider
使用经典提供商创建虚拟网络
ARM Template:
json
{
  "type": "Microsoft.Network/virtualNetworks",
  "apiVersion": "2023-05-01",
  "name": "[parameters('vnetName')]",
  "location": "[parameters('location')]",
  "properties": {
    "addressSpace": {
      "addressPrefixes": [
        "10.0.0.0/16"
      ]
    },
    "subnets": [
      {
        "name": "default",
        "properties": {
          "addressPrefix": "10.0.1.0/24"
        }
      },
      {
        "name": "apps",
        "properties": {
          "addressPrefix": "10.0.2.0/24"
        }
      }
    ]
  }
}
Pulumi TypeScript (Classic Provider):
typescript
import * as pulumi from "@pulumi/pulumi";
import * as azure from "@pulumi/azure";

const config = new pulumi.Config();
const vnetName = config.require("vnetName");
const location = config.require("location");
const resourceGroupName = config.require("resourceGroupName");

const vnet = new azure.network.VirtualNetwork("vnet", {
    name: vnetName,
    location: location,
    resourceGroupName: resourceGroupName,
    addressSpaces: ["10.0.0.0/16"],
    subnets: [
        {
            name: "default",
            addressPrefix: "10.0.1.0/24",
        },
        {
            name: "apps",
            addressPrefix: "10.0.2.0/24",
        },
    ],
});
Note: The Classic provider allows defining subnets inline within the VirtualNetwork resource, which can be simpler than managing them as separate resources.
ARM模板:
json
{
  "type": "Microsoft.Network/virtualNetworks",
  "apiVersion": "2023-05-01",
  "name": "[parameters('vnetName')]",
  "location": "[parameters('location')]",
  "properties": {
    "addressSpace": {
      "addressPrefixes": [
        "10.0.0.0/16"
      ]
    },
    "subnets": [
      {
        "name": "default",
        "properties": {
          "addressPrefix": "10.0.1.0/24"
        }
      },
      {
        "name": "apps",
        "properties": {
          "addressPrefix": "10.0.2.0/24"
        }
      }
    ]
  }
}
Pulumi TypeScript(经典提供商):
typescript
import * as pulumi from "@pulumi/pulumi";
import * as azure from "@pulumi/azure";

const config = new pulumi.Config();
const vnetName = config.require("vnetName");
const location = config.require("location");
const resourceGroupName = config.require("resourceGroupName");

const vnet = new azure.network.VirtualNetwork("vnet", {
    name: vnetName,
    location: location,
    resourceGroupName: resourceGroupName,
    addressSpaces: ["10.0.0.0/16"],
    subnets: [
        {
            name: "default",
            addressPrefix: "10.0.1.0/24",
        },
        {
            name: "apps",
            addressPrefix: "10.0.2.0/24",
        },
    ],
});
注意: 经典提供商允许在VirtualNetwork资源内联定义子网,这比将子网作为单独资源管理更简单。
App Service Plan and Web App with Classic Provider
使用经典提供商创建应用服务计划和Web应用
ARM Template:
json
{
  "resources": [
    {
      "type": "Microsoft.Web/serverfarms",
      "apiVersion": "2023-01-01",
      "name": "[parameters('appServicePlanName')]",
      "location": "[parameters('location')]",
      "sku": {
        "name": "B1",
        "tier": "Basic",
        "size": "B1",
        "capacity": 1
      },
      "kind": "linux",
      "properties": {
        "reserved": true
      }
    },
    {
      "type": "Microsoft.Web/sites",
      "apiVersion": "2023-01-01",
      "name": "[parameters('webAppName')]",
      "location": "[parameters('location')]",
      "dependsOn": [
        "[resourceId('Microsoft.Web/serverfarms', parameters('appServicePlanName'))]"
      ],
      "properties": {
        "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('appServicePlanName'))]",
        "siteConfig": {
          "linuxFxVersion": "NODE|18-lts",
          "appSettings": [
            {
              "name": "WEBSITE_NODE_DEFAULT_VERSION",
              "value": "~18"
            }
          ]
        }
      }
    }
  ]
}
Pulumi TypeScript (Classic Provider):
typescript
import * as pulumi from "@pulumi/pulumi";
import * as azure from "@pulumi/azure";

const config = new pulumi.Config();
const appServicePlanName = config.require("appServicePlanName");
const webAppName = config.require("webAppName");
const location = config.require("location");
const resourceGroupName = config.require("resourceGroupName");

const appServicePlan = new azure.appservice.ServicePlan("appServicePlan", {
    name: appServicePlanName,
    location: location,
    resourceGroupName: resourceGroupName,
    osType: "Linux",
    skuName: "B1",
});

const webApp = new azure.appservice.LinuxWebApp("webApp", {
    name: webAppName,
    location: location,
    resourceGroupName: resourceGroupName,
    servicePlanId: appServicePlan.id,
    siteConfig: {
        applicationStack: {
            nodeVersion: "18-lts",
        },
    },
    appSettings: {
        "WEBSITE_NODE_DEFAULT_VERSION": "~18",
    },
});
Note: The Classic provider has dedicated resources like
LinuxWebApp
and
WindowsWebApp
that provide better type safety and clearer configuration options compared to the generic
WebApp
resource.
ARM模板:
json
{
  "resources": [
    {
      "type": "Microsoft.Web/serverfarms",
      "apiVersion": "2023-01-01",
      "name": "[parameters('appServicePlanName')]",
      "location": "[parameters('location')]",
      "sku": {
        "name": "B1",
        "tier": "Basic",
        "size": "B1",
        "capacity": 1
      },
      "kind": "linux",
      "properties": {
        "reserved": true
      }
    },
    {
      "type": "Microsoft.Web/sites",
      "apiVersion": "2023-01-01",
      "name": "[parameters('webAppName')]",
      "location": "[parameters('location')]",
      "dependsOn": [
        "[resourceId('Microsoft.Web/serverfarms', parameters('appServicePlanName'))]"
      ],
      "properties": {
        "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', parameters('appServicePlanName'))]",
        "siteConfig": {
          "linuxFxVersion": "NODE|18-lts",
          "appSettings": [
            {
              "name": "WEBSITE_NODE_DEFAULT_VERSION",
              "value": "~18"
            }
          ]
        }
      }
    }
  ]
}
Pulumi TypeScript(经典提供商):
typescript
import * as pulumi from "@pulumi/pulumi";
import * as azure from "@pulumi/azure";

const config = new pulumi.Config();
const appServicePlanName = config.require("appServicePlanName");
const webAppName = config.require("webAppName");
const location = config.require("location");
const resourceGroupName = config.require("resourceGroupName");

const appServicePlan = new azure.appservice.ServicePlan("appServicePlan", {
    name: appServicePlanName,
    location: location,
    resourceGroupName: resourceGroupName,
    osType: "Linux",
    skuName: "B1",
});

const webApp = new azure.appservice.LinuxWebApp("webApp", {
    name: webAppName,
    location: location,
    resourceGroupName: resourceGroupName,
    servicePlanId: appServicePlan.id,
    siteConfig: {
        applicationStack: {
            nodeVersion: "18-lts",
        },
    },
    appSettings: {
        "WEBSITE_NODE_DEFAULT_VERSION": "~18",
    },
});
注意: 经典提供商有专门的资源,如
LinuxWebApp
WindowsWebApp
,与通用的
WebApp
资源相比,它们提供了更好的类型安全性和更清晰的配置选项。

Handling Azure-Specific Considerations

处理Azure特定注意事项

TypeScript Output Handling
TypeScript输出处理
Azure Native outputs often include undefined. Avoid
!
non-null assertions. Always safely unwrap with
.apply()
:
typescript
// ❌ WRONG - Will cause TypeScript errors
const webAppUrl = `https://${webApp.defaultHostName!}`;

// ✅ CORRECT - Handle undefined safely
const webAppUrl = webApp.defaultHostName.apply(hostname =>
    hostname ? `https://${hostname}` : ""
);
Azure Native输出通常包含undefined。避免使用
!
非空断言。始终使用
.apply()
安全地解包:
typescript
// ❌ 错误写法 - 会导致TypeScript错误
const webAppUrl = `https://${webApp.defaultHostName!}`;

// ✅ 正确写法 - 安全处理undefined
const webAppUrl = webApp.defaultHostName.apply(hostname =>
    hostname ? `https://${hostname}` : ""
);
Resource Naming Conventions
资源命名约定
ARM template
name
property maps to specific naming fields in Pulumi:
typescript
// ARM: "name": "myStorageAccount"
// Pulumi:
new azure_native.storage.StorageAccount("logicalName", {
    accountName: "mystorageaccount", // Actual Azure resource name
    // ...
});
ARM模板的
name
属性对应Pulumi中的特定命名字段:
typescript
// ARM: "name": "myStorageAccount"
// Pulumi:
new azure_native.storage.StorageAccount("logicalName", {
    accountName: "mystorageaccount", // Azure资源的实际名称
    // ...
});
API Versions
API版本
ARM templates require explicit API versions. Pulumi providers use recent stable API versions by default:
json
// ARM: "apiVersion": "2023-01-01"
// Pulumi: API version is embedded in the provider
Check the Pulumi Registry documentation for which API version each resource uses.
ARM模板需要显式指定API版本。Pulumi提供商默认使用最新的稳定API版本:
json
// ARM: "apiVersion": "2023-01-01"
// Pulumi: API版本已嵌入到提供商中
请查看Pulumi Registry文档了解每个资源使用的API版本。

Common Pitfalls to Avoid

需避免的常见陷阱

  • ❌ Not handling Output types properly (missing
    .apply()
    in TypeScript)
  • ❌ Assuming ARM property names match Pulumi property names exactly
  • ❌ Using
    azure
    provider when
    azure-native
    is available
  • ❌ Missing resource dependencies in conversion
  • ❌ Not preserving ARM template conditionals and loops
  • ❌ Forgetting to convert ARM functions like
    concat()
    ,
    uniqueString()
    , etc.
  • ❌ 未正确处理Output类型(在TypeScript中缺少
    .apply()
  • ❌ 假设ARM属性名称与Pulumi属性名称完全匹配
  • ❌ 在有azure-native可用的情况下使用
    azure
    提供商
  • ❌ 转换时遗漏资源依赖关系
  • ❌ 未保留ARM模板中的条件语句和循环
  • ❌ 忘记转换ARM函数,如
    concat()
    uniqueString()

3. RESOURCE IMPORT (EXISTING RESOURCES) - OPTIONAL

3. 资源导入(现有资源)- 可选

After conversion, you can optionally import existing resources to be managed by Pulumi. If the user does not request this, suggest it as a follow-up step to conversion.
CRITICAL: When the user requests importing existing Azure resources into Pulumi, see arm-import.md for detailed import procedures and zero-diff validation workflows.
arm-import.md provides:
  • Inline import ID patterns and examples
  • Azure Resource ID format conventions
  • Child resource handling (e.g., WebAppApplicationSettings)
  • Preview Resolution Workflow for achieving zero-diff after import
  • Step-by-step debugging for property conflicts
转换完成后,您可以选择将现有资源导入到Pulumi中进行管理。如果用户未提出此请求,可建议将其作为转换后的后续步骤。
关键提示:当用户请求将现有Azure资源导入到Pulumi时,请参阅arm-import.md了解详细的导入流程和零差异验证工作流。
arm-import.md包含:
  • 内联导入ID模式和示例
  • Azure资源ID格式约定
  • 子资源处理(例如WebAppApplicationSettings)
  • 预览解决工作流,用于在导入后实现零差异
  • 属性冲突的分步调试方法

Key Import Principles

核心导入原则

  1. Inline Import Approach:
    • Use
      import
      resource option with Azure Resource IDs
    • No separate import tool (unlike
      pulumi-cdk-importer
      )
  2. Azure Resource IDs:
    • Follow predictable pattern:
      /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName}
    • Can be generated by convention or queried via Azure CLI
  3. Zero-Diff Validation:
    • Run
      pulumi preview
      after import
    • Resolve all diffs using Preview Resolution Workflow
    • Goal: NO updates, replaces, creates, or deletes
  1. 内联导入方法:
    • 使用带Azure资源ID的
      import
      资源选项
    • 无需单独的导入工具(与
      pulumi-cdk-importer
      不同)
  2. Azure资源ID:
    • 遵循可预测的模式:
      /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName}
    • 可通过约定生成或通过Azure CLI查询获取
  3. 零差异验证:
    • 导入后运行
      pulumi preview
    • 使用预览解决工作流解决所有差异
    • 目标:无更新、无替换、无创建、无删除

4. PULUMI CONFIGURATION

4. PULUMI配置

Set up stack configuration matching ARM template parameters:
bash
undefined
设置与ARM模板参数匹配的栈配置:
bash
undefined

Set Azure region

设置Azure区域

pulumi config set azure-native:location eastus --stack dev
pulumi config set azure-native:location eastus --stack dev

Set application parameters

设置应用参数

pulumi config set storageAccountName mystorageaccount --stack dev
pulumi config set storageAccountName mystorageaccount --stack dev

Set secret parameters

设置机密参数

pulumi config set --secret adminPassword MyS3cr3tP@ssw0rd --stack dev
undefined
pulumi config set --secret adminPassword MyS3cr3tP@ssw0rd --stack dev
undefined

5. VALIDATION

5. 验证

After achieving zero diff in preview (if importing), validate the migration:
  1. Review all exports:
    bash
    pulumi stack output
  2. Verify resource relationships:
    bash
    pulumi stack graph
  3. Test application functionality (if applicable)
  4. Document any manual steps required post-migration
如果进行了导入操作,且预览已实现零差异后,验证迁移结果:
  1. 查看所有导出内容:
    bash
    pulumi stack output
  2. 验证资源关系:
    bash
    pulumi stack graph
  3. 测试应用功能(如适用)
  4. 记录迁移后需要手动执行的步骤

WORKING WITH THE USER

与用户协作

If the user asks for help planning or performing an ARM to Pulumi migration, use the information above to guide the user through the conversion and import process.
如果用户请求帮助规划或执行ARM到Pulumi的迁移,请使用上述信息指导用户完成转换和导入流程。

FOR DETAILED DOCUMENTATION

如需详细文档

When the user wants additional information, use the web-fetch tool to get content from the official Pulumi documentation:
Microsoft Azure Documentation:

OUTPUT FORMAT (REQUIRED)

输出格式(必填)

When performing a migration, always produce:
  1. Overview (high-level description)
  2. Migration Plan Summary
    • ARM template resources identified
    • Conversion strategy (language, providers)
    • Import approach (if applicable)
  3. Pulumi Code Outputs (organized by file)
    • Main program file
    • Component resources (if any)
    • Configuration instructions
  4. Resource Mapping Table (ARM → Pulumi)
    • ARM resource type → Pulumi resource type
    • ARM resource name → Pulumi logical name
    • Import ID (if importing)
  5. Preview Resolution Notes (if importing)
    • Diffs encountered
    • Resolution strategy applied
    • Properties ignored vs. added
  6. Final Migration Report (PR-ready)
    • Summary of changes
    • Testing instructions
    • Known limitations
    • Next steps
  7. Configuration Setup
    • Required config values
    • Example
      pulumi config set
      commands
Keep code syntactically valid and clearly separated by files.
执行迁移时,必须生成以下内容:
  1. 概述(高层级描述)
  2. 迁移计划摘要
    • 已识别的ARM模板资源
    • 转换策略(语言、提供商)
    • 导入方法(如适用)
  3. Pulumi代码输出(按文件组织)
    • 主程序文件
    • 组件资源(如有)
    • 配置说明
  4. 资源映射表(ARM → Pulumi)
    • ARM资源类型 → Pulumi资源类型
    • ARM资源名称 → Pulumi逻辑名称
    • 导入ID(如导入)
  5. 预览解决说明(如导入)
    • 遇到的差异
    • 应用的解决策略
    • 忽略或添加的属性
  6. 最终迁移报告(适合拉取请求)
    • 变更摘要
    • 测试说明
    • 已知限制
    • 后续步骤
  7. 配置设置
    • 必填配置值
    • 示例
      pulumi config set
      命令
确保代码语法有效,并按文件清晰分隔。