Skip to main content
The native .iac component format (tutorial 18) is great for portability — it compiles to whatever runtime the consuming stack uses. But sometimes you need the full Python SDK: pulumi_awsx for opinionated VPC patterns, custom retry logic, dynamic attribute construction, or a provider that hasn’t been mapped to the ubx type system yet. A local Python component gives you all of that while keeping the call site in .iac identical to any other component block. Drop a __main__.py and a requirements.txt into a subdirectory, and ubx auto-detects the Python runtime, merges the pip dependencies into the stack’s requirements.txt, and emits a native Python ComponentResource class in the generated __main__.py. The consumer never knows what language the component is written in.

What you’ll learn

  • The directory structure for a local Python component
  • How ubx auto-detects the Python runtime from file presence
  • How requirements.txt from the component is merged into the stack automatically
  • How schema.json validates inputs at the call site before any code runs
  • How to call a local Python component from a .iac stack

Why this matters

pulumi_awsx provides high-level VPC, ECS, and EKS constructs that create dozens of child resources behind a single call. These constructs are only available in the Pulumi SDKs — they can’t be expressed as raw unit blocks. A local Python component lets you wrap an awsx.ec2.Vpc call and expose its outputs as typed values consumed by other blocks in your stack.

The source code

component "vpc" {
  source       = "./components/vpc"
  cidr_block   = "10.0.0.0/16"
  nat_strategy = "Single"
}

output "vpc_id" {
  value = component.vpc.vpc_id
}

output "private_subnet_ids" {
  value = component.vpc.private_subnet_ids
}

How it works

1

ubx auto-detects the Python runtime from file presence

When source = "./components/vpc" points at a directory containing __main__.py and requirements.txt, ubx identifies it as a Python component. No explicit runtime declaration is needed in the component directory — the presence of these files is the signal. If only .iac files are present, ubx falls back to the native IR pipeline.
2

requirements.txt is merged into the stack's dependencies

ubx reads components/vpc/requirements.txt and merges its entries into the stack-level requirements.txt written alongside __main__.py in .ubx/. Duplicate package names are de-duplicated; the stack’s own constraint wins on version conflict. The result: running pip install -r .ubx/requirements.txt installs everything the stack and all its components need.
3

schema.json validates inputs before compilation

When schema.json is present, ubx validate checks that all inputs declared as required (no default) are provided at the call site, and that all component.vpc.X output references in the consuming stack exist in the schema’s outputs map. Type mismatches in inputs and references to undeclared outputs become compile errors rather than runtime surprises.
4

ubx inlines the component class in the generated __main__.py

The compiler reads components/vpc/__main__.py, wraps it, and emits it at the top of the generated stack __main__.py alongside the call site instantiation. No packaging, no pip install of the component, no import from an external module — the class is self-contained in the build artifact.

Plan output

● Compile   networking/ → .ubx/__main__.py
● Stack     dev · eu-west-1
● Packages  ready

────────────────────────────────────────────────────

+ component.vpc  will be created

    cidr_block    "10.0.0.0/16"
    nat_strategy  "Single"

    ↳ VPC · 6 subnets · Internet Gateway · NAT Gateway · Elastic IP · 6 route tables

────────────────────────────────────────────────────
  +31 to create    ~0 to change    -0 to destroy
────────────────────────────────────────────────────

  ubx apply --env dev

What ubx generates

# Generated by ubx — do not edit
from typing import Optional
import pulumi
import pulumi_awsx as awsx


# ── inlined from ./components/vpc/__main__.py ──────────────────────────────

class VpcComponentArgs:
    def __init__(self, cidr_block: str = "10.0.0.0/16", nat_strategy: str = "Single"):
        self.cidr_block = cidr_block
        self.nat_strategy = nat_strategy


class VpcComponent(pulumi.ComponentResource):
    vpc_id: pulumi.Output[str]
    private_subnet_ids: pulumi.Output[list]

    def __init__(self, name: str, args: VpcComponentArgs, opts: Optional[pulumi.ResourceOptions] = None):
        super().__init__("ubx:component:Vpc", name, {}, opts)
        vpc = awsx.ec2.Vpc(
            f"{name}-vpc",
            cidr_block=args.cidr_block,
            nat_gateways=awsx.ec2.NatGatewayConfigurationArgs(
                strategy=awsx.ec2.NatGatewayStrategy[args.nat_strategy],
            ),
            opts=pulumi.ResourceOptions(parent=self),
        )
        self.vpc_id = vpc.vpc_id
        self.private_subnet_ids = vpc.private_subnet_ids
        self.register_outputs({"vpc_id": self.vpc_id, "private_subnet_ids": self.private_subnet_ids})


# ── stack ─────────────────────────────────────────────────────────────────

config = pulumi.Config()
env = config.get("env") or "dev"

# component "vpc"
vpc = VpcComponent("vpc", VpcComponentArgs(
    cidr_block="10.0.0.0/16",
    nat_strategy="Single",
))

# output "vpc_id"
pulumi.export("vpc_id", vpc.vpc_id)

# output "private_subnet_ids"
pulumi.export("private_subnet_ids", vpc.private_subnet_ids)

Common mistakes

schema.json is optional but strongly recommended. Without it, ubx validate skips input type-checking and output reference validation for this component. A missing required input or a typo in component.vpc.vpd_id (note the typo) becomes a Python AttributeError at apply time rather than a compile error.
The "ubx:component:Vpc" type URN passed to super().__init__() appears in pulumi stack output and in ubx state list. Use a consistent naming scheme: "ubx:component:YourComponentName" in PascalCase. The Pulumi state will group child resources under this URN.

When to use each component type

ApproachUse when
Native .iac componentYou want portability across TypeScript, Python, and Go runtimes. No external SDK calls needed.
Local Python componentYou need pulumi_awsx, pulumi_kubernetes high-level constructs, or any Python library not available in raw .iac. Stack runtime must be python.
Registry component (registry.ubiquex.io/…)You want versioning, semantic releases, and sharing across repositories via the Strata registry.

Run it

ubx validate              # checks schema.json inputs and output refs
ubx plan --env dev        # compiles and previews — shows component.vpc summary
ubx apply --env dev       # creates all 31 resources in correct order

What you learned

A local Python component is auto-detected from __main__.py + requirements.txt — no explicit runtime declaration needed
requirements.txt from each component directory is merged into the stack’s requirements.txt automatically
schema.json turns missing inputs and bad output references into compile errors via ubx validate
The component class is inlined in the generated __main__.py — no packaging or publishing step required

Next steps

Local .iac component

The portable, runtime-agnostic alternative

Multi-stack dependency resolution

Use component outputs across stacks with @name.output