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-awsversion 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.
Related
- Building a Reusable VPC Component in Pulumi (Python) — the component this package distributes.
- Pulumi Component Resources — the parent guide on
ComponentResource, parenting, andregister_outputs. - Structuring Pulumi Stacks per Environment — consume the packaged component from per-environment stacks.