Packaging Pulumi Components for Reuse

Packaging a Pulumi component for reuse turns a ComponentResource class into an installable, versioned Python package that many stacks can pip install and import instead of copying source. This task sits under Pulumi Component Resources within Pulumi Patterns & Provider Management, and it covers project layout, semantic versioning, building a wheel, and publishing to a private package index so teams consume one canonical version.

Context

A component is only reusable in practice if other projects can depend on it by name and version. Copying the source file into each project recreates the drift problem components were meant to solve. Packaging gives you a single artifact with a version number, a declared provider dependency, and a changelog — the same rigor you apply to structuring stacks per environment, now applied to the shared building blocks themselves.

Prerequisites

  • Python 3.9+ and a build frontend: pip install build twine (python -m build --version).
  • The component code from Building a Reusable VPC Component in Pulumi (Python) or equivalent.
  • A private package index (CodeArtifact, GCP Artifact Registry, GitLab/GitHub Packages, or a self-hosted devpi) and credentials to publish to it.
  • A pinned pulumi-aws version that consumers will inherit as a dependency.

Implementation

1. Lay out the package

Use a src/ layout so the import package is unambiguous and tests do not accidentally import from the working directory.

pulumi-myorg-network/
├── pyproject.toml
├── README.md
└── src/
    └── myorg_network/
        ├── __init__.py
        └── vpc.py        # VpcComponent + VpcArgs

Re-export the public surface from __init__.py so consumers import from the package root:

# src/myorg_network/__init__.py
# CLI: import myorg_network in a consuming Pulumi program
from .vpc import VpcComponent, VpcArgs

__all__ = ["VpcComponent", "VpcArgs"]
__version__ = "0.1.0"

2. Declare metadata and the provider dependency

The pyproject.toml pins the Pulumi provider so every consumer resolves a compatible SDK, and it sets the version that drives reuse.

# pyproject.toml
# CLI: python -m build
[build-system]
requires = ["setuptools>=61"]
build-backend = "setuptools.build_meta"

[project]
name = "pulumi-myorg-network"
version = "0.1.0"
requires-python = ">=3.9"
dependencies = [
    "pulumi>=3.0,<4.0",
    "pulumi-aws>=6.0,<7.0",   # Provider note: consumers inherit this constraint.
]

[tool.setuptools.packages.find]
where = ["src"]

3. Build the distribution artifacts

# CLI: produces a wheel and sdist under dist/
python -m build
# State implication: none — building an artifact does not touch any Pulumi state.
ls dist/   # pulumi_myorg_network-0.1.0-py3-none-any.whl  pulumi_myorg_network-0.1.0.tar.gz

4. Publish to a private index and consume it

Upload the artifacts, then depend on the package by version from any stack.

# CLI: publish to your private index (URL/credentials from env or ~/.pypirc)
twine upload --repository-url "$PRIVATE_INDEX_URL" dist/*
# In a consuming project:
pip install pulumi-myorg-network==0.1.0 --index-url "$PRIVATE_INDEX_URL"
# consumer/__main__.py
# CLI: pulumi up --stack dev
import pulumi
from myorg_network import VpcComponent, VpcArgs

# State implication: the component is recorded in the consumer's state like any resource.
network = VpcComponent("app", VpcArgs(cidr_block="10.0.0.0/16", az_count=2))
pulumi.export("vpc_id", network.vpc_id)

Verification

Confirm the artifact installs and imports cleanly, ideally in a throwaway virtual environment:

# CLI: prove the published package is consumable
python -m venv /tmp/verify && /tmp/verify/bin/pip install \
    pulumi-myorg-network==0.1.0 --index-url "$PRIVATE_INDEX_URL"
/tmp/verify/bin/python -c "import myorg_network; print(myorg_network.__version__)"

A test in the package repo asserts the public surface is importable and the version is exposed:

# tests/test_packaging.py
# CLI: pytest tests/test_packaging.py -q
import myorg_network


def test_public_surface() -> None:
    assert hasattr(myorg_network, "VpcComponent")
    assert hasattr(myorg_network, "VpcArgs")
    assert myorg_network.__version__ == "0.1.0"

Gotchas & Edge Cases

Version drift between the package and its declared type token. A component's type token (myorg:network:Vpc) becomes part of consumers' resource URNs. Changing the token in a new package version forces resource replacement on upgrade. Keep the token stable across minor versions and treat a token change as a major version bump.

Loose provider constraints cause silent SDK mismatches. If pyproject.toml pins pulumi-aws>=6.0 with no upper bound, a consumer may resolve a v7 provider with breaking schema changes. Use a bounded range (>=6.0,<7.0) and bump it deliberately, mirroring the reproducible-install discipline applied across Python IaC dependencies.

src/ layout means editable installs need pip install -e .. Running tests against the working tree without an editable install will import a stale copy or fail outright. Install the package (editable in development, pinned in CI) before importing it.