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/mystorageaccountFINDING 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/mySubnetDocumentation: 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 tsvFinding 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 AppsWebAppConnectionStrings- 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: falseResolution: 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 jsonAdded 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:
- Are shown as added (+) in the preview diff
- Do NOT exist in the source ARM template
- 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 => falseResolution: 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 tsvStep 2: Determine the correct value:
- If Azure shows
true, update your code totrue - If the ARM template specified
false, investigate why Azure hastrue(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:
Run preview with details:
pulumi preview --diff --show-config --show-secretsFor each diff, identify the property path:
~ azure-native:web:WebApp: (update) [urn=...] ~ siteConfig.alwaysOn: false => trueQuery Azure for actual value:
az webapp show \ --name mywebapp \ --resource-group myResourceGroup \ --query "siteConfig.alwaysOn" \ --output tsvCheck Pulumi Registry for property documentation:
- Search for the resource type in Pulumi Registry
- Check property descriptions, types, and defaults
Determine resolution strategy:
- Added (+) and not in ARM template ->
ignoreChanges - Removed (-) and exists in Azure -> Add to code
- Changed (~) -> Update code to desired value
- Added (+) and not in ARM template ->
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 addedencryption.services.*-> may need to be added with defaults
Web Apps
state,hostNames,repositorySiteName-> ignore (computed)httpsOnly,clientAffinityEnabled-> often need to be addedsiteConfignested 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:
- Import resources with initial code
- Run
pulumi preview - Identify diffs (+ / - / ~)
- Apply resolution strategy for each diff
- Run
pulumi previewagain - Repeat until zero diff
Expected iterations: 2-5 preview runs for complex resources
COMMON PITFALLS TO AVOID
- Using
ignoreChangesfor 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