The Philosophical Developer -- Chapter 32: Pulumi on Azure
2026-07-21 · 4 min read

AWS and GCP were the proof points. Two clouds, same methodology, same patterns. But two is not a pattern. Three is a pattern. If the methodology works for AWS, GCP, and Azure, then it is truly provider-independent.
I sat down with OrsonRius and we ported the local-cloud-az infrastructure to Pulumi Go. Five modules this time, each one an Azure service with its own resource types and its own configuration surface.
The Azure module structure
The Azure local-cloud has five modules. Each one maps to an Azure service.
| Module | What It Creates | OpenTofu Equivalent |
|---|---|---|
| Resource Group | Resource group + tags | modules/resource-group |
| Storage | Storage account + blob container | modules/storage |
| Container Registry | ACR with admin credentials | modules/container-registry |
| Cosmos DB | Account with Session consistency | modules/cosmos-db |
| Key Vault | Vault + secret | modules/key-vault |
The Azure Pulumi SDK is the oldest of the three. The SDK path is github.com/pulumi/pulumi-azure/sdk/v6, and the package structure follows the Azure resource manager naming conventions.
The resource group module
The resource group is the foundation of every Azure deployment. Everything else depends on it.
func createResourceGroup(ctx *pulumi.Context) error {
rg, err := core.NewResourceGroup(ctx, "local-cloud-az-rg",
&core.ResourceGroupArgs{
Name: pulumi.String("rg-local-cloud-local-cloud"),
Location: pulumi.String("westus"),
Tags: pulumi.StringMap{
"managed_by": pulumi.String("pulumi"),
"environment": pulumi.String("local"),
},
})
if err != nil {
return err
}
ctx.Export("resourceGroupName", rg.Name)
return nil
}
The storage module
The Azure storage module is the most complex of the three cloud storage modules. It creates a resource group, a storage account, and a blob container. The storage account name is globally unique in Azure, limited to 24 lowercase alphanumeric characters.
account, err := storage.NewAccount(ctx, "local-cloud-az-storage",
&storage.AccountArgs{
Name: pulumi.String("startifactslocalcloud"),
ResourceGroupName: rg.Name,
Location: rg.Location,
AccountTier: pulumi.String("Standard"),
AccountReplicationType: pulumi.String("LRS"),
})
container, err := storage.NewContainer(ctx, "local-cloud-az-blobs",
&storage.ContainerArgs{
Name: pulumi.String("artifacts"),
StorageAccountName: account.Name,
ContainerAccessType: pulumi.String("private"),
})
The container registry module
The container registry module creates an Azure Container Registry with Basic SKU and admin credentials enabled. The ACR name follows the same naming convention as the storage account, limited to 50 alphanumeric characters.
The Cosmos DB module
The Cosmos DB module creates a GlobalDocumentDB account with Session consistency. The configuration includes the geo-location for the primary region, which maps to the resource group location.
account, err := cosmosdb.NewAccount(ctx, "local-cloud-az-cosmos",
&cosmosdb.AccountArgs{
Name: pulumi.String("cdb-local-cloud"),
ResourceGroupName: rg.Name,
Location: rg.Location,
OfferType: pulumi.String("Standard"),
Kind: pulumi.String("GlobalDocumentDB"),
ConsistencyPolicy: &cosmosdb.AccountConsistencyPolicyArgs{
ConsistencyLevel: pulumi.String("Session"),
},
GeoLocations: cosmosdb.AccountGeoLocationArray{
&cosmosdb.AccountGeoLocationArgs{
Location: rg.Location,
FailoverPriority: pulumi.Int(0),
},
},
})
The key vault module
The key vault module creates a vault with a single secret. The vault configuration includes the tenant ID (a placeholder for local development) and the SKU name.
vault, err := keyvault.NewKeyVault(ctx, "local-cloud-az-kv",
&keyvault.KeyVaultArgs{
Name: pulumi.String("kv-local-cloud"),
ResourceGroupName: rg.Name,
Location: rg.Location,
TenantId: pulumi.String("00000000-0000-0000-0000-000000000000"),
SkuName: pulumi.String("standard"),
})
secret, err := keyvault.NewSecret(ctx, "local-cloud-az-secret",
&keyvault.SecretArgs{
Name: pulumi.String("example-secret"),
Value: pulumi.String("super-secret-value"),
KeyVaultId: vault.ID(),
})
The tests
The Azure tests follow the same pattern as AWS and GCP. A mock struct intercepts the resource creation calls. Each module gets its own test function. A full stack test composes all five modules.
The mock configuration for Azure is the most extensive because the Azure SDK has more resource types and more computed properties. The storage account, for example, generates a primary blob endpoint that the mock returns as a predictable URL.
=== RUN TestAzureResourceGroupModule --- PASS
=== RUN TestAzureStorageModule --- PASS
=== RUN TestAzureContainerRegistryModule --- PASS
=== RUN TestAzureCosmosDBModule --- PASS
=== RUN TestAzureKeyVaultModule --- PASS
=== RUN TestAzureFullStack --- PASS
PASS ok local-cloud-az-pulumi 0.012s
What we learned
Azure is the hardest of the three clouds to port. The resource naming conventions are strict. The dependencies between resources are more entangled. The SDK is the oldest and least ergonomic of the three.
But the methodology still holds. The same code structure, the same test patterns, the same CI pipeline. The Azure SDK is different from the AWS and GCP SDKs, but the wrapper functions abstract away the differences. The test file is the same shape. The CI job is the same steps.
The resource group pattern is the most important lesson. Every Azure resource needs a resource group, and each module in the original OpenTofu code creates its own resource group. The Pulumi port follows the same convention. Each module that needs a resource group creates one. This is not the most efficient approach for production, but it preserves the module independence that the methodology requires.
The code is in the local-cloud-az repo. The tests pass. Three clouds, three SDKs, one methodology.
Repos:
- github.com/dark5un/local-cloud-az – infrastructure/pulumi/
LinkedIn:
Here I geek out with my young Padawan, OrsonRius.