Pulumi Components are how you package up reusable infrastructure patterns into a single, manageable unit.
Let’s see one in action. Imagine we need to deploy a standard web application: an S3 bucket for static assets, a CloudFront distribution to serve them, and an IAM policy to grant CloudFront access to the bucket. Instead of writing this out every time, we can create a WebSite component.
import pulumi
import pulumi_aws as aws
class WebSite(pulumi.ComponentResource):
def __init__(self, name, bucket_name, opts=None):
super().__init__("custom:resources:WebSite", name, {}, opts)
# Create the S3 bucket
self.bucket = aws.s3.Bucket(f"{name}-bucket",
bucket=bucket_name,
acl="public-read", # For simplicity in this example
opts=pulumi.ResourceOptions(parent=self))
# Create the CloudFront distribution
self.distribution = aws.cloudfront.Distribution(f"{name}-distribution",
origins=[aws.cloudfront.DistributionOriginArgs(
domain_name=self.bucket.bucket_regional_domain_name,
origin_id="my-s3-origin",
)],
enabled=True,
is_ipv6_enabled=True,
default_cache_behavior=aws.cloudfront.DistributionDefaultCacheBehaviorArgs(
target_origin_id="my-s3-origin",
viewer_protocol_policy="redirect-to-https",
allowed_methods=["GET", "HEAD"],
cached_methods=["GET", "HEAD"],
),
# ... other CloudFront settings ...
opts=pulumi.ResourceOptions(parent=self))
# Grant CloudFront access to the S3 bucket
self.bucket_policy = aws.s3.BucketPolicy(f"{name}-bucket-policy",
bucket=self.bucket.id,
policy=self.bucket.id.apply(lambda id: aws.iam.get_policy_document(
statements=[aws.iam.GetPolicyDocumentStatementArgs(
actions=["s3:GetObject"],
resources=[f"arn:aws:s3:::{id}/*"],
principals=[aws.iam.GetPolicyDocumentStatementPrincipalArgs(
type="Service",
identifiers=["cloudfront.amazonaws.com"],
)],
conditions=[aws.iam.GetPolicyDocumentStatementConditionArgs(
test="StringEquals",
variable="AWS:SourceArn",
values=[self.distribution.arn],
)],
)]
).json),
opts=pulumi.ResourceOptions(parent=self))
self.register_outputs({
"bucket_name": self.bucket.bucket,
"distribution_id": self.distribution.id,
"website_url": self.distribution.domain_name,
})
# Usage in __main__.py
app_website = WebSite("my-app", bucket_name="my-unique-app-bucket-name")
pulumi.export("website_url", app_website.website_url)
The WebSite component encapsulates three AWS resources: an S3 bucket, a CloudFront distribution, and an S3 bucket policy. When you deploy this Pulumi program, Pulumi treats my-app as a single logical resource, even though it manages multiple underlying cloud resources. It ensures that the bucket policy correctly references the CloudFront distribution and that the distribution points to the bucket.
This pattern solves the problem of managing complex, multi-resource infrastructure deployments that are common across projects. Instead of copying and pasting code or relying on manual scripting, you define a clear, declarative abstraction. This makes your infrastructure code more readable, maintainable, and less prone to errors. You can then instantiate this WebSite component multiple times for different applications or environments, each with its own unique bucket name and configuration, while sharing the same underlying component logic.
The magic happens within the pulumi.ComponentResource class. When you create a new instance of WebSite, you’re essentially creating a new "logical" resource in Pulumi’s state. Any resources you define inside the __init__ method of your component, and crucially, pass parent=self to their pulumi.ResourceOptions, become children of this logical component resource. Pulumi’s engine then understands this parent-child relationship. This means that when you update or destroy the WebSite component, Pulumi knows to also manage its child resources – the S3 bucket, CloudFront distribution, and bucket policy – in the correct order.
The register_outputs method is key for exposing information about your component. In this example, we’re exporting the bucket name, distribution ID, and the website URL. This allows other Pulumi resources or even other Pulumi stacks to reference these outputs, creating dependencies and enabling more complex infrastructure compositions.
A subtle but powerful aspect of components is their ability to conditionally create resources or modify their behavior based on input properties. For instance, you could add an optional enable_logging parameter to the WebSite component. If True, it would create an S3 bucket for logs and configure the main bucket to send access logs. This allows components to be flexible and adapt to various use cases without becoming overly complex.
The next step is understanding how to compose components together to build even more sophisticated infrastructure patterns.