3.2. Project Setup

In this lab you will scaffold a new Pulumi project connected directly to the Azure backend provisioned in 3.1, and wire up the pulumi-azure-native provider.

Step 1 — Log in to Azure

Authenticate the Azure CLI — Pulumi reads credentials from this session automatically:

az login

Confirm the active subscription:

az account show

You should see output similar to:

{
  "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "name": "<your-subscription-name>",
  "state": "Enabled",
  "tenantId": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"
}

If multiple subscriptions are available, target the correct one:

az account set --subscription "<name or id>"

Explanation

az login opens a browser or device-code flow and writes a token to ~/.azure/. Pulumi resolves Azure credentials in this order:

SourceWhen to use
Active az CLI sessionLocal development — no extra config needed
ARM_* environment varsCI/CD pipelines and automation
Managed IdentityAzure-hosted compute (VMs, AKS nodes)
OIDC Workload IdentityGitHub Actions, Azure DevOps pipelines

For this workshop the active az session is sufficient. No ARM_* variables need to be exported.

Why Owner permissions are required

Standard Contributor access is not enough for this workshop. Three operations require more:

OperationWhy Contributor falls short
pulumi login to Azure Blob StorageBlob data-plane access is separate from control-plane RBAC — even Owners must explicitly hold Storage Blob Data Contributor on the storage account
AKS assigns AcrPull to its managed identityCreating role assignments requires Microsoft.Authorization/roleAssignments/write
ACR image pushAcrPush is a data-plane role not included in Contributor

Each participant works in a dedicated sandbox subscription that contains only their own lab resources and is isolated from production. In this context, Owner is the pragmatic choice: it grants the necessary data-plane and role-assignment permissions without manual pre-provisioning for each participant.


Step 2 — Create the Project

Discover the backend resources created in Lab 3.1 and log in to the blob backend:

STORAGE_ACCOUNT=$(az storage account list \
  --resource-group "rg-pulumi-$NAME" \
  --query '[0].name' --output tsv)

KEYVAULT=$(az keyvault list \
  --resource-group "rg-pulumi-$NAME" \
  --query '[0].name' --output tsv)

pulumi login "azblob://pulumi-state?storage_account=$STORAGE_ACCOUNT"

Create a new directory and scaffold the project with the Key Vault secrets provider:

mkdir cloud-native-lab && cd cloud-native-lab
pulumi new python \
  --name cloud-native-lab \
  --stack dev \
  --secrets-provider "azurekeyvault://$KEYVAULT.vault.azure.net/keys/pulumi"

When prompted:

PromptSuggested value
Project description(press Enter to accept the default)
Toolchainpip

Move stack config into a dedicated subdirectory:

mkdir config
mv Pulumi.dev.yaml config/

Add stackConfigDir to Pulumi.yaml:

name: cloud-native-lab
runtime: python
description: A minimal Python Pulumi program
stackConfigDir: config

Verify Pulumi resolves the stack:

pulumi stack ls

Explanation

By running pulumi login before pulumi new, the stack is created directly in Azure Blob Storage — no local state is ever written. Passing --secrets-provider at project creation time means the stack is born with Key Vault encryption; there is no passphrase prompt and no migration step later.

--name and --stack skip the interactive prompts for those fields so pulumi new only asks for the project description and toolchain.

What to commit

Commit Pulumi.yaml, __main__.py, requirements.txt, and config/Pulumi.dev.yaml. The scaffold already adds venv/ to .gitignore.


Step 3 — Fix the Subscription

Pin the Azure subscription ID so the provider always targets the correct subscription, regardless of which account is active in the az session:

pulumi config set azure-native:subscriptionId $(az account show --query id -o tsv)

Confirm the subscription ID was written to the stack config:

cat config/Pulumi.dev.yaml

Expected output:

config:
  azure-native:subscriptionId: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

Explanation

Without an explicit azure-native:subscriptionId, the provider falls back to whatever subscription the az session currently has selected. If a team member switches subscription with az account set, the next pulumi up targets a different subscription silently. Pinning it in config makes the target explicit and auditable in version control.

$(az account show --query id -o tsv) reads the active subscription ID directly from the CLI — no copy-paste required.


Step 4 — Set the Default Location

Configure the default Azure region so every resource uses it unless explicitly overridden:

pulumi config set azure-native:location switzerlandnorth

Confirm both provider keys are present in the stack config:

cat config/Pulumi.dev.yaml

Expected output:

config:
  azure-native:location: switzerlandnorth
  azure-native:subscriptionId: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

Explanation

azure-native:location is a well-known config key read by the provider at deploy time. Setting it once here means individual resource declarations do not need to repeat it — though they can override it when needed.

Region nameLocation code
Switzerland Northswitzerlandnorth
Switzerland Westswitzerlandwest
West Europewesteurope
North Europenortheurope

Step 5 — Create the Login Script

Create a helper script that re-establishes the backend connection whenever you open a new terminal:

mkdir scripts
touch scripts/pulumi_login.sh
chmod +x scripts/pulumi_login.sh

Add the following content to scripts/pulumi_login.sh:

#!/usr/bin/env bash
set -euo pipefail

: "${NAME:?Set NAME before running this script}"

STACK=${1:-dev}

SUBSCRIPTION=$(yq '.config["azure-native:subscriptionId"]' "config/Pulumi.$STACK.yaml")
az account set --subscription "$SUBSCRIPTION"

STORAGE_ACCOUNT=$(az storage account list \
  --resource-group "rg-pulumi-$NAME" \
  --query '[0].name' --output tsv)

pulumi login "azblob://pulumi-state?storage_account=$STORAGE_ACCOUNT"
pulumi stack select --create "$STACK"

Run it to verify the connection:

./scripts/pulumi_login.sh dev

Explanation

The script encodes the conventions of this workshop so there is nothing to copy-paste:

StepHow
SubscriptionRead from config/Pulumi.<stack>.yaml via yq
az sessionSwitched to the correct subscription with az account set
Storage accountDiscovered from rg-pulumi-$NAME via az storage account list
Backend URLazblob://pulumi-state?storage_account=<account> — no env var needed
StackSelected (or created) with pulumi stack select --create

Run pulumi_login.sh whenever you open a new terminal or switch stacks. Pulumi remembers the secrets provider from the stack state — no extra flag is needed here.


Step 6 — Add the Azure Native Provider

Add pulumi-azure-native to requirements.txt and install it:

pulumi==3.243.0
pulumi-azure-native==3.19.0
source venv/bin/activate
pip install -r requirements.txt

Explanation

pulumi-azure-native wraps the full Azure Resource Manager API surface — hundreds of namespaces, one per Azure service. The namespaces used in this workshop are:

NamespaceAzure service
azure_native.resourcesResource Groups
azure_native.networkVNet, Subnet, Public IP, DNS
azure_native.containerserviceAKS clusters
azure_native.containerregistryContainer Registry (ACR)
azure_native.authorizationRole Assignments (RBAC)