2.1. Project Structure

In this lab you will initialise a local Pulumi state backend and scaffold a new Python project.

Step 1 — Login with a Local Backend

Execute the following command to store your Pulumi state locally:

pulumi login --local

Verify the login:

pulumi whoami -v

You should see User: <local> and the backend URL pointing to your home directory.

Explanation

Before Pulumi can manage any resources it needs a state backend — a place to store the JSON record of every deployed resource and its properties.

BackendWhere state is storedBest for
Pulumi CloudManaged SaaSTeams, audit history, UI
Azure Blob StorageAzure Storage AccountAzure-native teams
AWS S3S3 bucketAWS-native teams
Google Cloud StorageGCS bucketGCP-native teams
S3-compatibleMinIO, Cloudflare R2, etc.Self-hosted / air-gapped
Local (--local)~/.pulumi/stacks/Solo dev, this lab

pulumi login --local points Pulumi at your home directory. No account or network access required. Later in the workshop you will switch to an Azure Blob Storage backend for team use.


Step 2 — Bootstrap a New Project

Create a new directory and scaffold a Python Pulumi project from the built-in template:

mkdir pulumi-basics && cd pulumi-basics
pulumi new python

When prompted:

PromptSuggested value
Project namepulumi-basics
Project description(press Enter to accept the default)
Stack namedev
Passphrase(press Enter to leave it empty)
Toolchainpip

If you left the passphrase empty, set the environment variable so Pulumi does not prompt on every command:

export PULUMI_CONFIG_PASSPHRASE=""

After the scaffold completes, inspect the generated files:

ls -la
cat Pulumi.yaml
cat __main__.py
cat requirements.txt

Explanation

pulumi new python scaffolds a complete Python project:

pulumi-lab/
├── Pulumi.yaml          # project metadata (name + runtime)
├── __main__.py          # resource declarations — your IaC code lives here
├── requirements.txt     # Python package dependencies
├── venv/                # virtual environment (created automatically, gitignored)
└── .gitignore           # pre-configured for Python + Pulumi
FilePurpose
Pulumi.yamlDeclares the project name and runtime. Pulumi reads this to know how to execute the program.
__main__.pyThe entry point. Add, modify, or delete cloud resources by editing this file.
requirements.txtPython dependency manifest. Add provider packages here (e.g. pulumi-azure-native).
venv/Virtual environment created by pulumi new. Managed by Pulumi — never activate manually.

Pulumi.yaml declares the project name and runtime:

name: pulumi-basics
runtime: python
description: A minimal Python Pulumi program

__main__.py is intentionally minimal — every resource you manage gets declared here by importing a provider SDK and instantiating resource classes:

"""A Python Pulumi program"""

import pulumi

The stack config file Pulumi.dev.yaml is created in the project root alongside Pulumi.yaml. In Step 3 you will move it to a config/ subdirectory.

What to commit

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


Step 3 — Move Stack Config to a Subdirectory

By default, Pulumi writes Pulumi.dev.yaml next to Pulumi.yaml in the project root. Add stackConfigDir to keep stack files in a dedicated config/ folder:

mkdir config
mv Pulumi.dev.yaml config/

Then add stackConfigDir to Pulumi.yaml:

name: pulumi-basics
runtime: python
description: A minimal Python Pulumi program
stackConfigDir: config

Verify Pulumi still resolves the stack:

pulumi stack ls

Explanation

stackConfigDir tells Pulumi where to look for Pulumi.<stack>.yaml files. Moving them out of the root keeps the project tidy as the number of stacks grows — each environment (dev, staging, prod) gets its own file under config/ rather than cluttering the project root.

What to commit

Commit config/Pulumi.dev.yaml along with the updated Pulumi.yaml. The config/ directory is not gitignored by default.