All skills
pulumi avatar

/pulumi-arm-to-pulumi

@2f41625 official
by pulumipulumi/agent-skills70 stars
6

Convert or migrate Azure ARM (Azure Resource Manager) templates, Bicep templates, or code to Pulumi, including importing existing Azure resources. This skill MUST be loaded whenever a user requests migration, conversion, or import of ARM templates, Bicep templates, ARM code, Bicep code, or Azure resources to Pulumi.

Use this Skill: https://skilld.dev/gh/pulumi/agent-skills/pulumi-arm-to-pulumi

This session only. Nothing lands on disk.

arm-import.md

≈3.4k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Importing Azure Resources into Pulumi

This document provides detailed procedures for importing existing Azure resources into Pulumi state and resolving preview diffs to achieve zero-diff validation.

Key Principle: Azure will return many default values it has set dynamically that are not represented in code/state. You must systematically resolve each diff type to achieve zero-diff.

IMPORT APPROACH: INLINE IMPORT IDS

ARM migration uses inline imports. Use Pulumi's import resource option to specify Azure Resource IDs directly in the code.

Example

TypeScript:

const storageAccount = new azure_native.storage.StorageAccount("storageAccount", {
    accountName: "mystorageaccount",
    resourceGroupName: "myResourceGroup",
    location: "eastus",
    sku: { name: "Standard_LRS" },
    kind: "StorageV2",
}, {
    import: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myResourceGroup/providers/Microsoft.Storage/storageAccounts/mystorageaccount"
});

Python:

storage_account = storage.StorageAccount("storageAccount",
    account_name="mystorageaccount",
    resource_group_name="myResourceGroup",
    location="eastus",
    sku=storage.SkuArgs(name="Standard_LRS"),
    kind="StorageV2",
    opts=pulumi.ResourceOptions(
        import_="/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myResourceGroup/providers/Microsoft.Storage/storageAccounts/mystorageaccount"
    ))

Go:

storageAccount, err := storage.NewStorageAccount(ctx, "storageAccount", &storage.StorageAccountArgs{
    AccountName:       pulumi.String("mystorageaccount"),
    ResourceGroupName: pulumi.String("myResourceGroup"),
    Location:          pulumi.String("eastus"),
    Sku: &storage.SkuArgs{
        Name: pulumi.String("Standard_LRS"),
    },
    Kind: pulumi.String("StorageV2"),
}, pulumi.Import(pulumi.ID("/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myResourceGroup/providers/Microsoft.Storage/storageAccounts/mystorageaccount")))

C#:

var storageAccount = new StorageAccount("storageAccount", new StorageAccountArgs
{
    AccountName = "mystorageaccount",
    ResourceGroupName = "myResourceGroup",
    Location = "eastus",
    Sku = new SkuArgs { Name = "Standard_LRS" },
    Kind = "StorageV2",
}, new CustomResourceOptions
{
    ImportId = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myResourceGroup/providers/Microsoft.Storage/storageAccounts/mystorageaccount"
});

Java:

var storageAccount = new StorageAccount("storageAccount", StorageAccountArgs.builder()
    .accountName("mystorageaccount")
    .resourceGroupName("myResourceGroup")
    .location("eastus")
    .sku(SkuArgs.builder().name("Standard_LRS").build())
    .kind("StorageV2")
    .build(), CustomResourceOptions.builder()
        .importId("/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myResourceGroup/providers/Microsoft.Storage/storageAccounts/mystorageaccount")
        .build());

YAML:

resources:
  storageAccount:
    type: azure-native:storage:StorageAccount
    properties:
      accountName: mystorageaccount
      resourceGroupName: myResourceGroup
      location: eastus
      sku:
        name: Standard_LRS
      kind: StorageV2
    options:
      import: /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myResourceGroup/providers/Microsoft.Storage/storageAccounts/mystorageaccount

FINDING IMPORT IDS

Azure Resource IDs follow a predictable pattern and can be generated by convention or queried.

Azure Resource ID Format

/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName}

For child resources, the pattern extends:

/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{parentResourceType}/{parentResourceName}/{childResourceType}/{childResourceName}

Examples:

# Storage Account
/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myRG/providers/Microsoft.Storage/storageAccounts/mystorageaccount

# Web App
/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myRG/providers/Microsoft.Web/sites/mywebapp

# Virtual Network Subnet (child resource)
/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myRG/providers/Microsoft.Network/virtualNetworks/myVNet/subnets/mySubnet

Documentation: Azure Resource ID Documentation

Method 1: Convention-Based Generation

// Pattern: /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName}

const subscriptionId = "00000000-0000-0000-0000-000000000000";
const resourceGroupName = "myResourceGroup";
const storageAccountName = "mystorageaccount";

const importId = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.Storage/storageAccounts/${storageAccountName}`;

Method 2: Query Azure API

# Get resource ID directly
az resource show \
  --name mystorageaccount \
  --resource-group myResourceGroup \
  --resource-type "Microsoft.Storage/storageAccounts" \
  --query "id" \
  --output tsv

# List all resources with IDs
az resource list \
  --resource-group myResourceGroup \
  --query "[].{Name:name, Type:type, ID:id}" \
  --output table

# Get ID for specific resource type
az storage account show \
  --name mystorageaccount \
  --resource-group myResourceGroup \
  --query "id" \
  --output tsv

Finding Import IDs in Pulumi Registry

Each resource type in the Pulumi Registry has an "Import" section with the expected ID format:

Example: Storage Account Import

HANDLING CHILD RESOURCES

Some ARM properties are separate resources in Pulumi and must be imported separately:

Example: WebApp Application Settings

ARM Template (Inline):

{
  "type": "Microsoft.Web/sites",
  "properties": {
    "siteConfig": {
      "appSettings": [
        {"name": "WEBSITE_NODE_DEFAULT_VERSION", "value": "14.17.0"}
      ]
    }
  }
}

Pulumi (Separate Resource):

// Main Web App
const webApp = new azure_native.web.WebApp("webApp", {
    name: "mywebapp",
    resourceGroupName: resourceGroup.name,
    // ... other properties
}, {
    import: "/subscriptions/.../Microsoft.Web/sites/mywebapp"
});

// Application Settings (separate resource)
const appSettings = new azure_native.web.WebAppApplicationSettings("appSettings", {
    name: webApp.name,
    resourceGroupName: resourceGroup.name,
    properties: {
        "WEBSITE_NODE_DEFAULT_VERSION": "14.17.0",
    },
}, {
    import: "/subscriptions/.../Microsoft.Web/sites/mywebapp/config/appsettings"
});

Other common examples:

  • WebAppAuthSettings - Authentication settings for Web Apps
  • WebAppConnectionStrings - Connection strings for Web Apps
  • Network Security Group Rules - May be inline or separate depending on ARM template

Documentation: WebAppApplicationSettings

PREVIEW AFTER IMPORT - ZERO DIFF VALIDATION

After importing resources, you MUST run pulumi preview to ensure there are no changes. The goal is zero diff:

  • NO updates
  • NO replaces
  • NO creates
  • NO deletes

If there are changes, follow the Preview Resolution Workflow below.

PREVIEW RESOLUTION WORKFLOW

IMPORTANT: Follow these instructions. Avoid over-use of ignoreChanges resource option.

Azure will return many default values it has set dynamically that are not represented in code/state. You must resolve each diff type systematically:

Removed Properties (-) - Missing Defaults

Symptom: Preview shows properties being removed that exist in Azure:

~ azure-native:storage:StorageAccount: (update)
    - minimumTlsVersion: "TLS1_2"
    - allowBlobPublicAccess: false

Resolution: These properties were set by Azure with default values but are missing from your Pulumi code. ADD them to your code:

const storageAccount = new azure_native.storage.StorageAccount("storageAccount", {
    accountName: "mystorageaccount",
    resourceGroupName: resourceGroup.name,
    // ... other properties
    minimumTlsVersion: azure_native.storage.MinimumTlsVersion.TLS1_2, // ADD this
    allowBlobPublicAccess: false, // ADD this
}, {
    import: "...",
});

How to find the correct values:

# Query Azure for actual property values
az resource show \
  --ids "/subscriptions/.../Microsoft.Storage/storageAccounts/mystorageaccount" \
  --query "properties.minimumTlsVersion" \
  --output tsv

# Get full properties
az storage account show \
  --name mystorageaccount \
  --resource-group myResourceGroup \
  --query "properties" \
  --output json

Added Properties (+) - Computed Properties

Symptom: Preview shows properties being added that don't exist in the ARM template:

~ azure-native:storage:StorageAccount: (update)
    + creationTime: "2024-01-15T10:30:00Z"
    + statusOfPrimary: "available"

Resolution: These are computed/read-only properties set by Azure. Use ignoreChanges for these properties ONLY:

const storageAccount = new azure_native.storage.StorageAccount("storageAccount", {
    // ... properties
}, {
    import: "...",
    ignoreChanges: ["creationTime", "statusOfPrimary"],
});

CRITICAL: Only use ignoreChanges for properties that:

  1. Are shown as added (+) in the preview diff
  2. Do NOT exist in the source ARM template
  3. Are computed/read-only properties set by Azure

Documentation: ignoreChanges Resource Option

Changed Properties (~) - Value Mismatches

Symptom: Preview shows properties with different values:

~ azure-native:network:VirtualNetwork: (update)
    ~ enableDdosProtection: true => false

Resolution: Evaluate the change using preview diff and/or Azure API:

Step 1: Query Azure for the actual value:

az network vnet show \
  --name myVNet \
  --resource-group myResourceGroup \
  --query "enableDdosProtection" \
  --output tsv

Step 2: Determine the correct value:

  • If Azure shows true, update your code to true
  • If the ARM template specified false, investigate why Azure has true (manual change?)
  • Match the desired state (what you want), not necessarily what's in Azure

Step 3: Update your code to match the desired state:

const vnet = new azure_native.network.VirtualNetwork("vnet", {
    virtualNetworkName: "myVNet",
    resourceGroupName: resourceGroup.name,
    enableDdosProtection: true, // Match actual Azure state if that's desired
}, {
    import: "...",
});

DEBUGGING WORKFLOW FOR DIFFS

Step-by-step debugging process:

  1. Run preview with details:

    pulumi preview --diff --show-config --show-secrets
  2. For each diff, identify the property path:

    ~ azure-native:web:WebApp: (update)
        [urn=...]
        ~ siteConfig.alwaysOn: false => true
  3. Query Azure for actual value:

    az webapp show \
      --name mywebapp \
      --resource-group myResourceGroup \
      --query "siteConfig.alwaysOn" \
      --output tsv
  4. Check Pulumi Registry for property documentation:

    • Search for the resource type in Pulumi Registry
    • Check property descriptions, types, and defaults
  5. Determine resolution strategy:

    • Added (+) and not in ARM template -> ignoreChanges
    • Removed (-) and exists in Azure -> Add to code
    • Changed (~) -> Update code to desired value
  6. Apply fix and re-run preview until zero diff is achieved

COMMON AZURE PROPERTIES REQUIRING ATTENTION

Storage Accounts

  • creationTime, statusOfPrimary, statusOfSecondary -> ignore (computed)
  • minimumTlsVersion, allowBlobPublicAccess -> often need to be added
  • encryption.services.* -> may need to be added with defaults

Web Apps

  • state, hostNames, repositorySiteName -> ignore (computed)
  • httpsOnly, clientAffinityEnabled -> often need to be added
  • siteConfig nested properties -> many defaults need explicit values

Virtual Networks

  • subnets[*].id, subnets[*].etag -> ignore (computed)
  • enableDdosProtection, enableVmProtection -> may need to be added

Network Security Groups

  • securityRules[*].etag, securityRules[*].id -> ignore (computed)
  • Default rules -> Azure adds default rules; handle via ignoreChanges or import separately

ITERATION PROCESS

The preview resolution process is iterative:

  1. Import resources with initial code
  2. Run pulumi preview
  3. Identify diffs (+ / - / ~)
  4. Apply resolution strategy for each diff
  5. Run pulumi preview again
  6. Repeat until zero diff

Expected iterations: 2-5 preview runs for complex resources

COMMON PITFALLS TO AVOID

  • Using ignoreChanges for properties that exist in ARM template
  • Not querying Azure API to verify actual property values
  • Stopping at first preview without resolving all diffs
  • Forgetting to import child resources separately (like WebAppApplicationSettings)
  • Not documenting which properties were ignored vs. added

Source: SKILL.md on GitHub

1 warning17d5 checks · Risk SAFE
  • Gen Agent Trust Hub17d

    The skill provides a workflow and reference patterns for migrating Azure ARM/Bicep templates to Pulumi. It uses standard Azure and Pulumi CLI commands for resource discovery and state management. No security issues were detected.

  • Socket17d

    No alerts

  • Snyk17d

    Risk: LOW · No issues

  • Runlayer7mo

    2/3 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 2f41625. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 weeks ago.

Activeupdated 6 months ago
  • TypeScript
  • Python
  • azure
  • arm-templates
  • bicep
  • migration
  • pulumi
  • infrastructure-as-code
  • resource-import

README badge

README badge for pulumi/agent-skills/pulumi-arm-to-pulumi

Converts Azure ARM and Bicep templates to Pulumi infrastructure code, or imports existing Azure resources into Pulumi for management. Handles parameter mapping, conditionals, loops, dependencies, and nested templates while targeting either azure-native or azure (classic) provider based on feature coverage.

Generated from the current SKILL.md.

Does this skill handle both ARM templates and Bicep templates?
Yes. The skill converts ARM templates, Bicep templates, and code to Pulumi. There is no automated conversion tool; you perform manual translation using the conversion patterns provided.
Can I import existing Azure resources that were deployed from ARM templates?
Yes. After converting to Pulumi code, you can optionally import existing resources using inline import with Azure Resource IDs. The skill includes a Preview Resolution Workflow to achieve zero-diff validation after import.
Which Pulumi provider should I use, azure-native or azure?
Default to azure-native for full Azure Resource Manager API coverage. Use the classic azure provider as a fallback when azure-native doesn't support specific features or when you need simplified abstractions.
What languages does this skill support?
TypeScript/JavaScript, Python, C#, Go, Java, and YAML. Choose based on your preference or existing codebase.
What happens if my ARM template references resources outside the template?
The skill requires you to analyze the ARM template structure and ask targeted questions about missing artifacts or ambiguous configurations before generating Pulumi code. All resources must be represented in the output or explicitly justified.

Generated from the current SKILL.md. These answers refresh after source changes.