Pulumi
Introduction
Section titled “Introduction”Pulumi is an infrastructure-as-code platform that lets you create, deploy, and manage cloud infrastructure using general-purpose programming languages, such as Python, TypeScript, Go, C#, and Java.
It supports a range of cloud providers, including AWS, Azure, Google Cloud, and Kubernetes.
On Azure, the Azure Native provider (azure-native) manages resources through the Azure Resource Manager (ARM) API.
LocalStack for Azure integrates with Pulumi through the configuration of the Azure Native provider. Instead of configuring an endpoint for each Azure service, you point the provider to the metadata endpoint of the emulator, and the provider discovers the login and Azure Resource Manager endpoints of the emulator from it. There are two main methods to configure Pulumi for use with LocalStack for Azure:
- Using a Pulumi ESC environment that injects the provider settings into each Pulumi operation as
ARM_*environment variables. This method requires the Pulumi Cloud backend. - Storing the provider settings as
azure-native:*keys in the stack configuration. This method works with any backend, including a local backend.
This guide shows you how to deploy a resource group, a storage account, and a blob container to the LocalStack for Azure emulator with a Python Pulumi program, using either method.
Getting started
Section titled “Getting started”This guide is designed for users who are new to LocalStack for Azure and assumes basic knowledge of Pulumi.
Prerequisites
Section titled “Prerequisites”- The Pulumi CLI.
- Python and uv.
- LocalStack for Azure and the
lstkCLI. For more information, see Installation. - A Pulumi Cloud account, if you configure the provider with a Pulumi ESC environment.
Configure a local backend (optional)
Section titled “Configure a local backend (optional)”By default, Pulumi stores the state of your stacks in Pulumi Cloud. Optionally, you can store the state on your machine by setting the following environment variables before you create the project:
export PULUMI_BACKEND_URL=file://~export PULUMI_CONFIG_PASSPHRASE=lsdevtestPULUMI_BACKEND_URL stores the state under ~/.pulumi, like pulumi login --local does, and PULUMI_CONFIG_PASSPHRASE sets the passphrase that encrypts the secrets in the stack configuration.
Pulumi ESC requires the Pulumi Cloud backend, so with a local backend, configure the provider with stack configuration.
Create a new Pulumi project
Section titled “Create a new Pulumi project”Create a directory for the project, and create the project and the dev stack from the python template:
mkdir my-azure-infracd my-azure-infrapulumi new python --name my-azure-infra --description "This is a sample Pulumi project" --stack dev --runtime-options toolchain=uv --yesThe --runtime-options toolchain=uv option makes Pulumi use uv to create the virtual environment and install the dependencies, and --yes skips the remaining prompts:
Created project 'my-azure-infra'
Created stack 'dev'
Installing dependencies...
Using CPython 3.13.2 interpreter at: ...Creating virtual environment at: .venv...Finished installing dependencies
Your new project is ready to go!
To perform an initial deployment, run `pulumi up`The command creates the following files in the project directory:
Pulumi.yaml: The project definition, including the language runtime and the toolchain.__main__.py: The Pulumi program.pyproject.tomlanduv.lock: The Python dependencies of the project..venv: The virtual environment that runs the program.
Add the Azure Native provider
Section titled “Add the Azure Native provider”The python template doesn’t include the Azure Native provider.
Add the pulumi-azure-native package to the project:
uv add pulumi-azure-nativeThe command installs the package into the .venv virtual environment and records it in pyproject.toml and uv.lock.
Write the infrastructure code
Section titled “Write the infrastructure code”Replace the contents of __main__.py with the following code.
The program creates a resource group, a storage account, and a blob container, and exports their names as stack outputs:
import pulumi_azure_native.resources as resourceimport pulumi_azure_native.storage as storagefrom pulumi import Config, exportfrom pulumi_azure_native.storage import BlobContainer, PublicAccess
config = Config()prefix = config.get("prefix", "localstack")suffix = config.get("suffix", "test")container_name = config.get("container_name", "documents")
resource_group = resource.ResourceGroup( "appservicerg", resource_group_name=f"{prefix}-rg")
storage_account = storage.StorageAccount( "appservicesa", account_name=f"{prefix}storage{suffix}", resource_group_name=resource_group.name, kind=storage.Kind.STORAGE_V2, sku=storage.SkuArgs(name=storage.SkuName.STANDARD_LRS),)
storage_container = BlobContainer( "appservice-c", container_name=container_name, account_name=storage_account.name, public_access=PublicAccess.NONE, resource_group_name=resource_group.name,)
export("resourceGroupName", resource_group.name)export("storageAccountName", storage_account.name)export("containerName", storage_container.name)The program reads the prefix, suffix, and container_name values from the stack configuration and sets the name of each resource explicitly.
Without an explicit name, Pulumi generates one by appending a random suffix to the logical name of the resource.
For more information, see Resource names.
Configure the stack settings
Section titled “Configure the stack settings”Set the values that the program expects, and the Azure region of the resources, in the dev stack:
pulumi config set prefix pulumipulumi config set suffix testpulumi config set container_name orderspulumi config set azure-native:location westeuropePulumi saves stack-specific settings in the Pulumi.dev.yaml file in the project directory.
Settings that apply to all the stacks of the project belong in Pulumi.yaml.
The name of the storage account combines the prefix and the suffix: pulumistoragetest.
Storage account names must be between 3 and 24 characters long and contain only lowercase letters and numbers.
Configure the Azure Native provider
Section titled “Configure the Azure Native provider”The Azure Native provider reads each setting from the stack configuration and, when the stack configuration doesn’t contain it, from the matching ARM_* environment variable.
Configure the provider with one of the following methods.
For a description of each setting, see Configuration.
Use a Pulumi ESC environment
Section titled “Use a Pulumi ESC environment”Pulumi ESC stores the provider settings in Pulumi Cloud and injects them into the Pulumi operations of each stack that imports the environment.
Create an environment named azure-localstack:
pulumi env init azure-localstackWithout an organization or project prefix, Pulumi creates the environment in the default project of your default organization, so its fully qualified name is <your-org>/default/azure-localstack.
Run pulumi whoami -v to list your organizations and pulumi env ls to list your environments.
Open the environment definition in your default terminal editor:
pulumi env edit azure-localstackAdd the ARM_* variables under the environmentVariables block, and encrypt the client secret with fn::secret:
values: environmentVariables: ARM_CLIENT_ID: "any-app" ARM_TENANT_ID: "00000000-0000-0000-0000-000000000000" ARM_SUBSCRIPTION_ID: "00000000-0000-0000-0000-000000000000" ARM_METADATA_HOSTNAME: "azure.localhost.localstack.cloud:4566" ARM_DISABLE_INSTANCE_DISCOVERY: "true" ARM_CLIENT_SECRET: fn::secret: "any-secret"Attach the environment to the dev stack:
pulumi config env add azure-localstack --yesKEY VALUEazure-native:location westeuropecontainer_name ordersprefix pulumisuffix testpulumi:tags {"pulumi:template":"python"}
ENVIRONMENT VARIABLE VALUEARM_CLIENT_ID any-appARM_CLIENT_SECRET [secret]ARM_DISABLE_INSTANCE_DISCOVERY trueARM_METADATA_HOSTNAME azure.localhost.localstack.cloud:4566ARM_SUBSCRIPTION_ID 00000000-0000-0000-0000-000000000000ARM_TENANT_ID 00000000-0000-0000-0000-000000000000Use the environment name without your organization prefix.
A stack can only import environments from its own organization, so Pulumi reads the first segment of a two-part name as the project: pulumi config env add <your-org>/azure-localstack fails with environment '<your-org>/<your-org>/azure-localstack' not found.
If you created the environment in a project other than default, use <project-name>/azure-localstack.
The command appends an environment block to Pulumi.dev.yaml:
config: my-azure-infra:prefix: pulumi my-azure-infra:suffix: test my-azure-infra:container_name: orders azure-native:location: westeuropeenvironment: - azure-localstackWhen you run a Pulumi command on the stack, Pulumi opens the environment and injects the ARM_* variables into the Pulumi program and the provider.
The variables aren’t set in your shell.
Use stack configuration
Section titled “Use stack configuration”To keep the provider settings with the stack, for example with a local backend, store them as azure-native:* keys in the stack configuration instead of an ESC environment:
pulumi config set azure-native:metadataHost azure.localhost.localstack.cloud:4566pulumi config set azure-native:disableInstanceDiscovery truepulumi config set azure-native:clientId any-apppulumi config set azure-native:tenantId 00000000-0000-0000-0000-000000000000pulumi config set azure-native:subscriptionId 00000000-0000-0000-0000-000000000000pulumi config set --secret azure-native:clientSecret any-secretPulumi adds the keys to Pulumi.dev.yaml and encrypts the client secret.
With a local backend, the file contains the following settings:
encryptionsalt: v1:...config: my-azure-infra:prefix: pulumi my-azure-infra:suffix: test my-azure-infra:container_name: orders azure-native:location: westeurope azure-native:metadataHost: azure.localhost.localstack.cloud:4566 azure-native:disableInstanceDiscovery: "true" azure-native:clientId: any-app azure-native:tenantId: 00000000-0000-0000-0000-000000000000 azure-native:subscriptionId: 00000000-0000-0000-0000-000000000000 azure-native:clientSecret: secure: v1:...Deploy the stack
Section titled “Deploy the stack”Start the emulator
Section titled “Start the emulator”Start the LocalStack for Azure emulator:
lstk startPulumi needs no further setup to reach the emulator, because the provider settings already point the Azure Native provider to it.
Preview the changes
Section titled “Preview the changes”The pulumi preview command runs your program, compares the resources it declares with the current state of the stack, and shows the operations Pulumi would perform, without creating, updating, or deleting anything.
Each resource is marked with its planned operation, such as + for create, ~ for update, and - for delete.
Stack outputs that depend on properties Azure returns after creating the resources are shown as [unknown].
pulumi previewThe output shows that Pulumi will create the stack and the three Azure resources:
Previewing update (dev)
View in Browser (Ctrl+O): https://app.pulumi.com/...
Type Name Plan + pulumi:pulumi:Stack my-azure-infra-dev create + ├─ azure-native:resources:ResourceGroup appservicerg create + ├─ azure-native:storage:BlobContainer appservice-c create + └─ azure-native:storage:StorageAccount appservicesa create
Outputs: containerName : [unknown] resourceGroupName : [unknown] storageAccountName: [unknown]
Resources: + 4 to createA successful preview doesn’t confirm that Pulumi can authenticate against the emulator.
In this project, the provider requests an access token only when it creates the resources during pulumi up.
Deploy the resources
Section titled “Deploy the resources”The pulumi up command shows the same preview and asks for confirmation.
Select yes to apply the changes:
pulumi up...Updating (dev)
View in Browser (Ctrl+O): https://app.pulumi.com/...
Type Name Status + pulumi:pulumi:Stack my-azure-infra-dev created (5s) + ├─ azure-native:resources:ResourceGroup appservicerg created (0.43s) + ├─ azure-native:storage:StorageAccount appservicesa created (2s) + └─ azure-native:storage:BlobContainer appservice-c created (0.57s)
Outputs: containerName : "orders" resourceGroupName : "pulumi-rg" storageAccountName: "pulumistoragetest"
Resources: + 4 created
Duration: 7sVerify the deployment
Section titled “Verify the deployment”To inspect the deployed resources with the Azure CLI, redirect the az commands to the emulator:
lstk az start-interceptionThis command changes your global Azure CLI configuration to use the LocalStack cloud until you run lstk az stop-interception.
For more information, see Azure CLI.
Read the resource names from the stack outputs, and show the storage account:
export RESOURCE_GROUP=$(pulumi stack output resourceGroupName)export STORAGE_ACCOUNT=$(pulumi stack output storageAccountName)az storage account show \ --name "$STORAGE_ACCOUNT" \ --resource-group "$RESOURCE_GROUP" \ --query "{Name:name, Kind:kind, Sku:sku.name, Location:location, State:provisioningState}" \ --output tableName Kind Sku Location State----------------- --------- ------------ ---------- ---------pulumistoragetest StorageV2 Standard_LRS westeurope SucceededList the blob containers of the storage account:
az storage container-rm list \ --storage-account "$STORAGE_ACCOUNT" \ --resource-group "$RESOURCE_GROUP" \ --query "[].{Name:name, PublicAccess:publicAccess}" \ --output tableName PublicAccess------ --------------orders NoneClean up
Section titled “Clean up”Delete the resources of the stack:
pulumi destroyLike pulumi up, this command shows a preview before it performs any changes.
Select yes to proceed.
The stack still exists, with its configuration and history, but the emulator no longer contains its resources.
To remove the stack itself, including its configuration and history, run the following command and confirm the removal when prompted:
pulumi stack rm devTo make the az commands target Azure again, stop the interception:
lstk az stop-interceptionConfiguration
Section titled “Configuration”The following table lists the Azure Native provider settings that target the emulator. Set each one either as a stack configuration key or as an environment variable, for example in a Pulumi ESC environment. When both are set, the stack configuration key takes precedence.
| Stack configuration key | Environment variable | Value | Description |
|---|---|---|---|
azure-native:metadataHost |
ARM_METADATA_HOSTNAME |
azure.localhost.localstack.cloud:4566 |
Host of the metadata endpoint, /metadata/endpoints, from which the provider discovers the login and Azure Resource Manager endpoints of the emulator. |
azure-native:disableInstanceDiscovery |
ARM_DISABLE_INSTANCE_DISCOVERY |
true |
Stops the Azure Identity library from asking Microsoft Entra ID to validate the login endpoint of the emulator. |
azure-native:clientId |
ARM_CLIENT_ID |
Any value, such as any-app. |
Client ID of the service principal. |
azure-native:clientSecret |
ARM_CLIENT_SECRET |
Any value, such as any-secret. |
Client secret of the service principal. |
azure-native:tenantId |
ARM_TENANT_ID |
00000000-0000-0000-0000-000000000000 |
ID of the Microsoft Entra tenant. |
azure-native:subscriptionId |
ARM_SUBSCRIPTION_ID |
00000000-0000-0000-0000-000000000000 |
ID of the subscription that contains the resources. |
The client secret makes the provider authenticate as a service principal, so it doesn’t depend on the Azure CLI. The emulator doesn’t validate the client ID and secret. For more information, see Credentials.
The provider reads only the environment variable names in the table.
For example, with ARM_METADATA_HOST instead of ARM_METADATA_HOSTNAME, the provider ignores the metadata host and authenticates against the public Azure cloud.
The provider disables instance discovery only when the value is exactly true.
For the complete list of settings, see Azure Native: Installation & Configuration.
Troubleshooting
Section titled “Troubleshooting”If pulumi up fails with ClientSecretCredential authentication failed, the URL of the failing request shows which setting is missing.
To check whether any request reaches the emulator, inspect the emulator logs with lstk logs.
AADSTS900021: Requested tenant identifier is not valid
Section titled “AADSTS900021: Requested tenant identifier is not valid”error: ClientSecretCredential authentication failed.GET https://login.microsoftonline.com/00000000-0000-0000-0000-000000000000/v2.0/.well-known/openid-configurationRESPONSE 400: 400 Bad Request "error": "invalid_tenant", "error_description": "AADSTS900021: Requested tenant identifier '00000000-0000-0000-0000-000000000000' is not valid. ..."The provider authenticates against the public Azure cloud instead of the emulator, so no request reaches LocalStack.
Make sure that the metadata host is set with the exact name, ARM_METADATA_HOSTNAME or azure-native:metadataHost.
AADSTS50049: Unknown or invalid instance
Section titled “AADSTS50049: Unknown or invalid instance”error: ClientSecretCredential authentication failed.GET https://login.microsoftonline.com/common/discovery/instanceRESPONSE 400: 400 Bad Request "error": "invalid_instance",The provider reached the emulator, but the Azure Identity library asked Microsoft Entra ID to validate the login endpoint of the emulator before requesting a token.
Set ARM_DISABLE_INSTANCE_DISCOVERY or azure-native:disableInstanceDiscovery to true.