1. Docs
  2. Infrastructure as Code
  3. Guides
  4. Building & Extending
  5. Components
  6. Build a Component

Build a Component

    This guide will walk you through the steps of making a Pulumi Component suitable for reuse in all languages and cloud environments.

    Prerequisites:

    Why Write a Component?

    Pulumi Components provide a way to encapsulate best practices, ensuring that security policies and deployment patterns remain consistent across projects. They also help reduce code duplication by allowing you to define reusable infrastructure patterns. By structuring infrastructure as components, maintainability improves, and teams can work more efficiently.

    Key features:

    • Sharing and Reusability: Do more with less code. Don’t repeat yourself.
    • Best Practices and Policy: Encode company standards and policy, across all languages and cloud environments.
    • Multi-language Support: Write in one language, use in any language.

    How It Works

    Pulumi Components are implemented as custom classes in any Pulumi-supported language. Once defined, they can be used locally, referenced from a Git repository, or published as a Pulumi package for broader distribution. A component extends pulumi.ComponentResource and groups multiple resources into a single, reusable abstraction. This approach enables developers to define infrastructure once and apply it consistently across multiple environments.

    Pulumi Components inherently support multi-language use. Regardless of the language a component was written in, it is a fast one-step process to generate a SDK, allowing you to use it in all Pulumi-supported languages.

    If your component uses a local package (such as any Terraform provider via terraform-provider, or another component that contains a local package), you must commit the generated SDK code to version control. See Using components with local packages for details.

    Structure of a Component

    A Pulumi Component consists of three main parts:

    • The component resource encapsulates multiple Pulumi resources, grouping them into a logical unit.
    • The component resource arguments define configurable input properties, allowing users to specify parameters that tailor the component’s behavior to specific needs.
    • The provider host registers and runs your component resources, acting as the foundational layer for component creation.
    Not all resource options apply to component resources. For example, ignoreChanges and customTimeouts have no effect on components themselves. To see which options work with components and how to apply options to child resources using transforms, see Resource options and component resources.

    Example: Static Web Page Component

    In this example, we’ll create a static website component in AWS Simple Storage Service (S3). The component will manage the following five sub-resources necessary to implement a basic S3 hosted static website:

    The component will take as input the contents of the file you wish to host, and will output the S3 endpoint used to access it.

    Example: Using the custom StaticPage component in a Pulumi Program

    name: sample-components-yaml
    description: A Pulumi YAML program that builds and uses a static page component
    runtime:
      name: yaml
    packages:
      sample-components: ./sample-components
    resources:
      my-static-page:
        type: sample-components:StaticPage
        properties:
          indexContent: "<h1>I love Pulumi!</h1>"
    outputs:
      websiteURL: http://${my-static-page.endpoint}
    

    The core implementation of the AWS API is handled by the Pulumi AWS Provider, which gives us those five underlying resource types. Our StaticPage component will work with those existing types and create a new type of resource with a simpler API.

    Setting up your Component project

    A Pulumi Component is a separate project from your Pulumi program. So, let’s create a new directory for it, and create some project files:

    $ mkdir sample-components
    $ cd sample-components
    

    PulumiPlugin.yaml

    The PulumiPlugin.yaml file tells Pulumi that this directory is a component, rather than a Pulumi program. In it, we define the language runtime needed to load the plugin.

    Example: PulumiPlugin.yaml for TypeScript

    runtime: nodejs
    

    Manage dependencies

    Next, we need to define our dependencies in package.json.

    Example: package.json for a Pulumi Component

    {
        "name": "sample-components",
        "description": "Static Page Component",
        "dependencies": {
            "@pulumi/aws": "^7.6.0",
            "@pulumi/pulumi": "^3.191.0"
        },
        "devDependencies": {
            "@types/node": "^24.0.0",
            "typescript": "^5.9.2"
        }
    }
    

    The @pulumi/pulumi SDK contains everything we need for making a component. The @pulumi/aws package is the AWS provider that we are building on top of.

    When building components that will be consumed by other projects, be aware of how package managers handle provider package versions. See Provider package version management for guidance on managing provider dependencies across component boundaries.

    TypeScript project file

    We’ll also need a TypeScript project file called tsconfig.json.

    {
        "compilerOptions": {
            "strict": true,
            "outDir": "bin",
            "target": "es2016",
            "module": "commonjs",
            "moduleResolution": "node",
            "sourceMap": true,
            "experimentalDecorators": true,
            "pretty": true,
            "noFallthroughCasesInSwitch": true,
            "noImplicitReturns": true,
            "forceConsistentCasingInFileNames": true
        },
        "files": [
            "index.ts",
            "StaticPage.ts"
        ]
    }
    

    Finally, install dependencies via NPM:

    $ npm install
    

    Build the component

    Once dependencies are installed, build the TypeScript component:

    $ npx tsc
    

    This compiles the TypeScript files and generates the JavaScript output in the bin directory.

    Example: PulumiPlugin.yaml for Python

    runtime: python
    

    Manage dependencies

    Next, we need to define our dependencies in requirements.txt.

    Example: requirements.txt for a Pulumi Component

    pulumi>=3.191.0,<4.0
    pulumi_aws>=7.6.0,<8.0
    

    The pulumi SDK contains everything we need for making a component. The pulumi_aws package is the AWS provider that we are building on top of.

    Example: PulumiPlugin.yaml for Go

    runtime: go
    

    Manage dependencies

    Next, we need to define our dependencies in go.mod.

    Example: go.mod for a Pulumi Component

    module github.com/sample-components
    
    go 1.25
    
    require (
    	github.com/pulumi/pulumi-aws/sdk/v7 v7.6.0
    	github.com/pulumi/pulumi/sdk/v3 v3.191.0
    )
    

    The pulumi SDK contains everything we need for making a component. The pulumi-aws package is the AWS provider that we are building on top of.

    Example: PulumiPlugin.yaml for C#

    runtime: dotnet
    

    Manage dependencies

    Next, we need to define our dependencies in StaticPageComponent.csproj.

    Example: StaticPageComponent.csproj for a Pulumi Component

    <Project Sdk="Microsoft.NET.Sdk">
    
      <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net8.0</TargetFramework>
        <Nullable>enable</Nullable>
        <AssemblyName>sample-components</AssemblyName>
      </PropertyGroup>
    
      <ItemGroup>
        <PackageReference Include="Pulumi" Version="3.87.0" />
        <PackageReference Include="Pulumi.AWS" Version="7.*" />
        <PackageReference Include="Newtonsoft.Json" Version="13.*" />
      </ItemGroup>
    </Project>
    

    The Pulumi SDK contains everything we need for making a component. The Pulumi.Aws package is the AWS provider that we are building on top of.

    Note that the AssemblyName specifies the name of the component package. This name will be important later on in the component implementation, so make sure it’s something unique and descriptive!

    Example: PulumiPlugin.yaml for Java

    runtime: java
    

    Manage dependencies

    Next, we need to define our dependencies and project configuration in a Maven pom.xml.

    Example: pom.xml for a Pulumi Component

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
      <modelVersion>4.0.0</modelVersion>
    
      <groupId>com.pulumi</groupId>
      <artifactId>sample-components</artifactId>
      <version>1.0-SNAPSHOT</version>
    
      <properties>
        <encoding>UTF-8</encoding>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <maven.compiler.release>11</maven.compiler.release>
        <mainClass>staticpagecomponent.App</mainClass>
        <mainArgs/>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
      </properties>
    
      <dependencies>
        <dependency>
          <groupId>org.slf4j</groupId>
          <artifactId>slf4j-nop</artifactId>
          <version>2.0.17</version>
        </dependency>
        <dependency>
          <groupId>com.google.code.gson</groupId>
          <artifactId>gson</artifactId>
          <version>2.13.1</version>
        </dependency>
        <dependency>
          <groupId>com.pulumi</groupId>
          <artifactId>pulumi</artifactId>
          <version>1.16.1</version>
        </dependency>
        <dependency>
          <groupId>com.pulumi</groupId>
          <artifactId>aws</artifactId>
          <version>(7.6.0,7.99]</version>
        </dependency>
      </dependencies>
    
      <build>
        <plugins>
          <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>exec-maven-plugin</artifactId>
            <version>3.0.0</version>
            <configuration>
              <mainClass>${mainClass}</mainClass>
              <commandlineArgs>${mainArgs}</commandlineArgs>
            </configuration>
          </plugin>
        </plugins>
      </build>
    </project>
    

    The com.pulumi.pulumi SDK contains everything we need for making a component. The com.pulumi.aws package is the AWS provider that we are building on top of. We’ve also included a couple helper libraries like gson and slf4j-nop which are helpful for this example.

    Implement the entrypoint

    First, create the index.ts file, where we will export the component class.

    Example: index.ts component export

    export { StaticPage } from "./staticpage";
    

    First, create the __main__.py file, where we will define an entry point for the component.

    Example: __main__.py component entry point

    from pulumi.provider.experimental import component_provider_host
    from staticpage import StaticPage
    
    if __name__ == "__main__":
        component_provider_host(name="sample-components", components=[StaticPage])
    

    Here, the component_provider_host call invokes a Pulumi provider implementation which acts as a shim for the component. The name we pass to it will be important later on in the component implementation, so make sure it’s something unique and descriptive!

    First, create the main.go file, where we will define an entry point for the component.

    Example: main.go component entry point

    package main
    
    import (
    	"context"
    	"fmt"
    	"os"
    
    	p "github.com/pulumi/pulumi-go-provider"
    	"github.com/pulumi/pulumi-go-provider/infer"
    	"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
    )
    
    func main() {
    	provider, err := provider()
    	if err != nil {
    		fmt.Fprintf(os.Stderr, "Error: %s", err.Error())
    		os.Exit(1)
    	}
    	err = provider.Run(context.Background(), "sample-components", "0.1.0")
    
    	if err != nil {
    		panic(err)
    	}
    }
    
    func provider() (p.Provider, error) {
    	return infer.NewProviderBuilder().
    		WithNamespace("example.com").
    		WithComponents(
    			infer.ComponentF(NewStaticPage),
    		).
    		WithModuleMap(map[tokens.ModuleName]tokens.ModuleName{
    			"sample-components": "index",
    		}).
    		Build()
    }
    

    Here, the infer.NewProviderBuilder()..Build() call builds a Pulumi provider implementation which acts as a shim for the component. Then, in the main function we call provider.Run(...) to execute the provider. The name we pass to this function and to the module map (sample-components) will be important later on in the component implementation, so make sure it’s something unique and descriptive!

    First, create the Program.cs file, where we will define an entry point for the component.

    Example: Program.cs component entry point

    using System.Threading.Tasks;
    
    class Program
    {
        public static Task Main(string []args) =>
            Pulumi.Experimental.Provider.ComponentProviderHost.Serve(args);
    }
    

    Here, the ComponentProviderHost.Serve call invokes a Pulumi provider implementation which acts as a shim for the component. Everything else about your component will be inferred by the Pulumi SDK.

    First, create the src/main/java/staticpagecomponent sub-directory and in it, create the App.java file, where we will define an entry point for the component.

    Example: App.java component entry point

    package staticpagecomponent;
    
    import java.io.IOException;
    import com.pulumi.provider.internal.ComponentProviderHost;
    
    public class App {
        public static void main(String[] args) throws IOException, InterruptedException {
            new ComponentProviderHost("sample-components", App.class.getPackage()).start(args);
        }
    }
    

    Here, the ComponentProviderHost.start(...) call invokes a Pulumi provider implementation which acts as a shim for the component. The name we pass to it will be important later on in the component implementation, so make sure it’s something unique and descriptive!

    We also need to pass the Java package so that your component classes can be inferred by the Pulumi SDK.

    Because YAML is entirely declarative, unlike in our other languages, there’s no need define an entry point.

    Implement the Component

    Next we will define the classes that implement our reusable component.

    Components typically require two parts: a subclass of pulumi.ComponentResource that implements the component, and an arguments class, which is used to configure the component during construction.

    Add the required imports

    First create a file called StaticPage.ts, and add the imports we will need:

    Example: StaticPage.ts required imports

    import * as pulumi from "@pulumi/pulumi";
    import * as aws from "@pulumi/aws";

    The @pulumi/pulumi package is the core Pulumi SDK, which provides the base classes and types needed to define components and resources. The @pulumi/aws package is the Pulumi AWS provider, which gives us the resource types we need to create AWS infrastructure.

    First create a file called static_page.py, and add the imports we will need:

    Example: static_page.py required dependencies

    import json
    from typing import Optional, TypedDict
    
    import pulumi
    from pulumi import ResourceOptions
    from pulumi_aws import s3

    The pulumi package is the core Pulumi SDK, which provides the base classes and types needed to define components and resources. The pulumi_aws package is the Pulumi AWS provider, which gives us the resource types we need to create AWS infrastructure.

    First create a directory called staticpagecomponent and create a file called static_page.go inside it. This will be our importable Go package:

    Example: staticpagecomponent/static_page.go required dependencies

    package staticpagecomponent
    
    import (
    	"encoding/json"
    	"fmt"
    
    	"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/s3"
    	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
    )

    The github.com/pulumi/pulumi/sdk/v3/go/pulumi package is the core Pulumi SDK, which provides the base types needed to define components and resources. The github.com/pulumi/pulumi-aws/sdk/v7/go/aws/s3 package is the Pulumi AWS provider’s S3 module, which gives us the resource types we need to create AWS infrastructure.

    First create a file called StaticPage.cs, and add the imports we will need:

    Example: StaticPage.cs required imports

    using System;
    using System.Collections.Generic;
    
    using Pulumi;
    using Pulumi.Aws.S3;
    using Pulumi.Aws.S3.Inputs;
    
    using Newtonsoft.Json;

    The Pulumi namespace is the core Pulumi SDK, which provides the base classes and types needed to define components and resources. The Pulumi.Aws.S3 namespace is from the Pulumi AWS provider, which gives us the resource types we need to create AWS infrastructure.

    First create a file called StaticPage.java, and add the imports we will need:

    Example: StaticPage.java required imports

    package myproject;
    
    import java.util.Map;
    
    import com.pulumi.aws.s3.BucketObject;
    import com.pulumi.aws.s3.BucketObjectArgs;
    import com.pulumi.aws.s3.BucketPolicy;
    import com.pulumi.aws.s3.BucketPolicyArgs;
    import com.pulumi.aws.s3.BucketPublicAccessBlock;
    import com.pulumi.aws.s3.BucketPublicAccessBlockArgs;
    import com.pulumi.aws.s3.Bucket;
    import com.pulumi.aws.s3.BucketWebsiteConfiguration;
    import com.pulumi.aws.s3.BucketWebsiteConfigurationArgs;
    import com.pulumi.aws.s3.inputs.BucketWebsiteConfigurationIndexDocumentArgs;
    
    import com.pulumi.core.Output;
    import com.pulumi.core.Either;
    import com.pulumi.core.annotations.Export;
    import com.pulumi.core.annotations.Import;
    
    import com.pulumi.resources.ComponentResource;
    import com.pulumi.resources.ComponentResourceOptions;
    import com.pulumi.resources.CustomResourceOptions;
    import com.pulumi.resources.ResourceArgs;

    The com.pulumi.resources and com.pulumi.core packages are from the core Pulumi SDK, which provide the base classes and types needed to define components and resources. The com.pulumi.aws.s3 package is from the Pulumi AWS provider, which gives us the resource types we need to create AWS infrastructure.

    YAML components do not need to explicitly manage dependencies or import external libraries. The necessary packages will be resolved and automatically installed by the Pulumi engine, based on the unique resource type identifiers in the component’s sub-resources.

    Define the Component arguments

    Next, we will implement the arguments class. In our example here, we will pass the contents of the webpage we want to host to the component.

    Example: StaticPage.ts the Component arguments implementation

    export interface StaticPageArgs {
        indexContent: pulumi.Input<string>;
    }

    Note that argument classes must be serializable and use pulumi.Input types, rather than the language’s default types. Certain types like union types (e.g., string | number) and functions are not supported due to schema inference limitations. For details on type requirements and limitations, see Component arguments and type requirements.

    Example: static_page.py the Component arguments implementation

    class StaticPageArgs(TypedDict):
        index_content: pulumi.Input[str]
        """The HTML content for index.html."""

    Note that argument classes must be serializable and use pulumi.Input types, rather than the language’s default types. This means certain types like union types and functions are not supported. For details on type requirements and limitations, see Component arguments and type requirements.

    Python class properties are typically written in lowercase with words separated by underscores, known as snake_case, however properties in the Pulumi package schema are usually written in camelCase, where capital letters are used to separate words. To follow these conventions, the inferred schema for a component will have translated property names. In our example index_content will become indexContent in the schema. When using a component, the property names will follow the conventions of that language, for example if we use our component from TypeScript, we would refer to indexContent, but if we use it from Python, we would use index_content.

    Example: static_page.go the Component arguments implementation

    type StaticPageArgs struct {
    	IndexContent pulumi.StringInput `pulumi:"indexContent"`
    }

    Note that argument classes must be serializable and use pulumi.Input types, rather than the language’s default types. This means complex or platform-specific types may not be supported. For details on type requirements and limitations, see Component arguments and type requirements.

    Go struct fields are typically written in title case, with the first letter capitalized and capital letters used to separate words, however properties in the Pulumi package schema are usually written in camelCase, with the first letter in lowercase and capital letters used to separate words. To follow these conventions, the inferred schema for a component will have translated property names. In our example IndexContent will become indexContent in the schema. When using a component, the property names will follow the conventions of that language, for example if we use our component from TypeScript, we would refer to indexContent, but if we use it from Go, we would use IndexContent.

    Example: StaticPage.cs the Component arguments implementation

    public sealed class StaticPageArgs : ResourceArgs {
        [Input("indexContent")]
        public Input<string> IndexContent { get; set; } = null!;
    }

    Note that argument classes must be serializable and use Pulumi.Input types, rather than the language’s default types. This means complex or platform-specific types may not be supported. For details on type requirements and limitations, see Component arguments and type requirements.

    Example: StaticPage.java the Component arguments implementation

    class StaticPageArgs extends ResourceArgs {
        @Import(name = "indexContent", required = true)
        private Output<String> IndexContent;
    
        public Output<String> indexContent() {
            return this.IndexContent;
        }
    
        private StaticPageArgs() {
        }
    
        public StaticPageArgs(Output<String> indexContent) {
            this.IndexContent = indexContent;
        }
    }

    Note that argument classes must be serializable and use com.pulumi.core.Output<T> types, rather than the language’s default types. This means complex or platform-specific types may not be supported. For details on type requirements and limitations, see Component arguments and type requirements.

    The @Import decorator marks this as a required input and allows use to give a name for the input that could be different from the implementation here.

    In YAML, rather than defining a separate args class, the inputs are declared under the inputs key:

    Example: PulumiPlugin.yaml the Component arguments implementation

        inputs:
          indexContent:
            type: string

    Inputs can be any basic type (e.g. boolean, integer, string) or an array of any of those types. You can provide a default value via the default property.

    Define the Component resource

    Now we can implement the component itself. Components should inherit from pulumi.ComponentResource, and should accept the arguments class we just defined in the constructor. All the work for our component happens in the constructor, and outputs are returned via class properties. At the end of the process a calling self.registerOutputs signals Pulumi that the process of creating the component resource has completed.

    Example: StaticPage.ts the Component implementation

    export class StaticPage extends pulumi.ComponentResource {
        public readonly endpoint: pulumi.Output<string>;
    
        constructor(name: string, args: StaticPageArgs, opts?: pulumi.ComponentResourceOptions) {
            super("sample-components:index:StaticPage", name, args, opts);
    
            const bucket = new aws.s3.Bucket(`${name}-bucket`, {}, { parent: this });
    
            const bucketWebsite = new aws.s3.BucketWebsiteConfiguration(
                `${name}-website`,
                {
                    bucket: bucket.bucket,
                    indexDocument: { suffix: "index.html" },
                },
                {
                    parent: this,
                },
            );
    
            new aws.s3.BucketObject(
                `${name}-index-object`,
                {
                    bucket: bucket.bucket,
                    key: "index.html",
                    content: args.indexContent,
                    contentType: "text/html",
                },
                {
                    parent: this,
                },
            );
    
            const publicAccessBlock = new aws.s3.BucketPublicAccessBlock(
                `${name}-public-access-block`,
                {
                    bucket: bucket.id,
                    blockPublicAcls: false,
                },
                {
                    parent: this,
                },
            );
    
            new aws.s3.BucketPolicy(
                `${name}-bucket-policy`,
                {
                    bucket: bucket.id,
                    policy: bucket.bucket.apply(this.allowGetObjectPolicy),
                },
                {
                    parent: this,
                    dependsOn: publicAccessBlock,
                },
            );
    
            this.endpoint = bucketWebsite.websiteEndpoint;
    
            this.registerOutputs({
                endpoint: bucketWebsite.websiteEndpoint,
            });

    Now we can implement the component itself. Components should inherit from pulumi.ComponentResource, and should accept the arguments class we just defined in the constructor. All the work for our component happens in the constructor, and outputs are returned via class properties. At the end of the process a calling self.register_outputs signals Pulumi that the process of creating the component resource has completed.

    Example: static_page.py the Component implementation

    class StaticPage(pulumi.ComponentResource):
        endpoint: pulumi.Output[str]
        """The URL of the static website."""
    
        def __init__(self,
                     name: str,
                     args: StaticPageArgs,
                     opts: Optional[ResourceOptions] = None) -> None:
    
            super().__init__("sample-components:index:StaticPage", name, {}, opts)
    
            bucket = s3.Bucket(
                f"{name}-bucket",
                opts=ResourceOptions(parent=self))
    
            bucket_website = s3.BucketWebsiteConfiguration(
                f"{name}-website",
                bucket=bucket.bucket,
                index_document={"suffix": "index.html"},
                opts=ResourceOptions(parent=self))
    
            s3.BucketObject(
                f"{name}-index-object",
                bucket=bucket.bucket,
                key="index.html",
                content=args.get("index_content"),
                content_type="text/html",
                opts=ResourceOptions(parent=self))
    
            bucket_public_access_block = s3.BucketPublicAccessBlock(
                f"{name}-public-access-block",
                bucket=bucket.id,
                block_public_acls=False,
                opts=ResourceOptions(parent=self))
    
            s3.BucketPolicy(
                f"{name}-bucket-policy",
                bucket=bucket.bucket,
                policy=bucket.bucket.apply(_allow_getobject_policy),
                opts=ResourceOptions(parent=self, depends_on=[bucket_public_access_block]))
    
            self.endpoint = bucket_website.website_endpoint
    
            self.register_outputs({
                "endpoint": bucket_website.website_endpoint
            })

    Now we can implement the component itself. Component structs should include pulumi.ResourceState and define the consumable outputs, which follow the same general rules as inputs. All the work for building our component happens in the NewStaticPage constructor.

    Example: static_page.go the Component implementation

    type StaticPage struct {
    	pulumi.ResourceState
    	Endpoint pulumi.StringOutput `pulumi:"endpoint"`
    }
    
    func NewStaticPage(ctx *pulumi.Context, name string, args *StaticPageArgs, opts ...pulumi.ResourceOption) (*StaticPage, error) {
    	comp := &StaticPage{}
    	err := ctx.RegisterComponentResource("sample-components:index:StaticPage", name, comp, opts...)
    	if err != nil {
    		return nil, err
    	}
    
    	bucket, err := s3.NewBucket(ctx, fmt.Sprintf("%s-bucket", name), &s3.BucketArgs{},
    		pulumi.Parent(comp))
    	if err != nil {
    		return nil, err
    	}
    
    	bucketWebsite, err := s3.NewBucketWebsiteConfiguration(ctx, fmt.Sprintf("%s-website", name),
    		&s3.BucketWebsiteConfigurationArgs{
    			Bucket: bucket.Bucket,
    			IndexDocument: s3.BucketWebsiteConfigurationIndexDocumentArgs{
    				Suffix: pulumi.String("index.html"),
    			},
    		},
    		pulumi.Parent(comp))
    	if err != nil {
    		return nil, err
    	}
    
    	_, err = s3.NewBucketObject(ctx, fmt.Sprintf("%s-index-object", name), &s3.BucketObjectArgs{
    		Bucket:      bucket.Bucket,
    		Key:         pulumi.String("index.html"),
    		Content:     args.IndexContent,
    		ContentType: pulumi.String("text/html"),
    	}, pulumi.Parent(comp))
    	if err != nil {
    		return nil, err
    	}
    
    	publicAccessBlock, err := s3.NewBucketPublicAccessBlock(ctx, fmt.Sprintf("%s-public-access-block", name),
    		&s3.BucketPublicAccessBlockArgs{
    			Bucket:          bucket.ID(),
    			BlockPublicAcls: pulumi.Bool(false),
    		}, pulumi.Parent(comp))
    	if err != nil {
    		return nil, err
    	}
    
    	allowGetObjectPolicy := func(bucketName string) (string, error) {
    		policy := map[string]interface{}{
    			"Version": "2012-10-17",
    			"Statement": []map[string]interface{}{
    				{
    					"Effect":    "Allow",
    					"Principal": "*",
    					"Action":    []string{"s3:GetObject"},
    					"Resource":  []string{fmt.Sprintf("arn:aws:s3:::%s/*", bucketName)},
    				},
    			},
    		}
    		policyJSON, err := json.Marshal(policy)
    		if err != nil {
    			return "", err
    		}
    		return string(policyJSON), nil
    	}
    
    	_, err = s3.NewBucketPolicy(ctx, fmt.Sprintf("%s-bucket-policy", name), &s3.BucketPolicyArgs{
    		Bucket: bucket.ID(),
    		Policy: bucket.Bucket.ApplyT(func(bucketName string) (string, error) {
    			return allowGetObjectPolicy(bucketName)
    		}).(pulumi.StringOutput),
    	}, pulumi.Parent(comp), pulumi.DependsOn([]pulumi.Resource{publicAccessBlock}))
    	if err != nil {
    		return nil, err
    	}
    
    	comp.Endpoint = bucketWebsite.WebsiteEndpoint
    
    	return comp, nil
    }

    Now we can implement the component itself. Components should inherit from Pulumi.ComponentResource, and should accept the arguments class we just defined in the constructor. All the work for our component happens in the constructor, and outputs are returned via class properties. At the end of the process a calling this.RegisterOutputs signals Pulumi that the process of creating the component resource has completed.

    Example: StaticPage.cs the Component implementation

    class StaticPage : ComponentResource {
        [Output("endpoint")]
        public Output<string> Endpoint { get; set; }
    
        public StaticPage(string name, StaticPageArgs args, ComponentResourceOptions? opts = null)
            : base("sample-components:index:StaticPage", name, args, opts)
        {
            var bucket = new Bucket($"{name}-bucket", new() { }, new() { Parent = this });
    
            var bucketWebsite = new BucketWebsiteConfiguration($"{name}-website", new() {
                Bucket = bucket.BucketName,
                IndexDocument = new BucketWebsiteConfigurationIndexDocumentArgs { Suffix = "index.html" },
            }, new() { Parent = this });
    
            var bucketObject = new BucketObject($"{name}-index-object", new BucketObjectArgs {
                Bucket = bucket.BucketName,
                Key = "index.html",
                Content = args.IndexContent,
                ContentType = "text/html",
            }, new() { Parent = this });
    
            var publicAccessBlock = new BucketPublicAccessBlock($"{name}-public-access-block", new() {
                Bucket = bucket.BucketName,
                BlockPublicAcls = false,
            }, new() { Parent = this });
    
            var bucketPolicy = new BucketPolicy($"{name}-bucket-policy", new() {
                Bucket = bucket.BucketName,
                Policy = bucket.Id.Apply(this.AllowGetObjectPolicy),
            }, new() { Parent = this, DependsOn = publicAccessBlock });
    
            this.Endpoint = bucketWebsite.WebsiteEndpoint;
    
            this.RegisterOutputs(new Dictionary<string, object?> {
                ["endpoint"] = bucketWebsite.WebsiteEndpoint
            });
        }
    
        private string AllowGetObjectPolicy(string bucketName) {
            return JsonConvert.SerializeObject(new {
                Version = "2012-10-17",
                Statement = new[] { new {
                    Effect = "Allow",
                    Principal = "*",
                    Action = new[] {
                        "s3:GetObject"
                    },
                    Resource = new[] {
                        $"arn:aws:s3:::{bucketName}/*"
                    }
                }}
            });
        }
    }

    Now we can implement the component itself. Components should inherit from Pulumi.ComponentResource, and should accept the arguments class we just defined in the constructor. All the work for our component happens in the constructor, and outputs are returned via class properties. At the end of the process a calling this.registerOutputs signals Pulumi that the process of creating the component resource has completed.

    Example: StaticPage.java the Component implementation

    class StaticPage extends ComponentResource {
        @Export(name = "endpoint", refs = { String.class }, tree = "[0]")
        public final Output<String> endpoint;
    
        public StaticPage(String name, StaticPageArgs args, ComponentResourceOptions opts) {
            super("sample-components:index:StaticPage", name, null, opts);
    
            var bucket = new Bucket(
                    String.format("%s-bucket", name),
                    null,
                    CustomResourceOptions.builder()
                            .parent(this)
                            .build());
    
            var bucketWebsite = new BucketWebsiteConfiguration(
                    String.format("%s-website", name),
                    BucketWebsiteConfigurationArgs.builder()
                            .bucket(bucket.id())
                            .indexDocument(
                                    BucketWebsiteConfigurationIndexDocumentArgs.builder()
                                            .suffix("index.html")
                                            .build())
                            .build(),
                    CustomResourceOptions.builder()
                            .parent(this)
                            .build());
    
            var bucketObject = new BucketObject(
                    String.format("%s-index-object", name),
                    BucketObjectArgs.builder()
                            .bucket(bucket.bucket())
                            .key("index.html")
                            .content(args.indexContent())
                            .contentType("text/html")
                            .build(),
                    CustomResourceOptions.builder()
                            .parent(this)
                            .build());
    
            var publicAccessBlock = new BucketPublicAccessBlock(
                    String.format("%s-public-access-block", name),
                    BucketPublicAccessBlockArgs.builder()
                            .bucket(bucket.id())
                            .blockPublicAcls(false)
                            .blockPublicPolicy(false)
                            .ignorePublicAcls(false)
                            .restrictPublicBuckets(false)
                            .build(),
                    CustomResourceOptions.builder()
                            .parent(this)
                            .build());
    
            var policyJson = bucket.arn().applyValue(bucketArn ->
                "{\n" +
                "  \"Version\": \"2012-10-17\",\n" +
                "  \"Statement\": [\n" +
                "    {\n" +
                "      \"Sid\": \"PublicReadGetObject\",\n" +
                "      \"Effect\": \"Allow\",\n" +
                "      \"Principal\": \"*\",\n" +
                "      \"Action\": \"s3:GetObject\",\n" +
                "      \"Resource\": \"" + bucketArn + "/*\"\n" +
                "    }\n" +
                "  ]\n" +
                "}"
            );
    
            var bucketPolicy = new BucketPolicy(
                    String.format("%s-bucket-policy", name),
                    BucketPolicyArgs.builder()
                            .bucket(bucket.id())
                            .policy(policyJson.applyValue(Either::ofLeft))
                            .build(),
                    CustomResourceOptions.builder()
                            .parent(this)
                            .dependsOn(publicAccessBlock)
                            .build());
    
            this.endpoint = bucketWebsite.websiteEndpoint();
    
            this.registerOutputs(Map.of(
                    "endpoint", bucketWebsite.websiteEndpoint()));
        }
    }

    Now we can implement the component itself. Under the components key, create one or more component definitions. A component in YAML follows the following structure:

    components:
      MyComponent:      # the component class name
        inputs:         # one or more input values
        resources:      # one or more sub-resource definitions
        variables:      # intermediate variable definitions
        outputs:        # one or more output values
    

    Here’s the full code for our StaticPage component:

    Example: PulumiPlugin.yaml the Component implementation

    components:
      StaticPage:
        inputs:
          indexContent:
            type: string
        resources:
          bucket:
            type: aws:s3:Bucket
            properties: {}
    
          bucketWebsite:
            type: aws:s3:BucketWebsiteConfiguration
            properties:
              bucket: ${bucket.bucket}
              indexDocument:
                suffix: index.html
    
          bucketObject:
            type: aws:s3/bucketObject:BucketObject
            properties:
              bucket: ${bucket.bucket}
              key: index.html
              content: ${indexContent}
              contentType: text/html
    
          publicAccessBlock:
            type: aws:s3/bucketPublicAccessBlock:BucketPublicAccessBlock
            properties:
              bucket: ${bucket.id}
              blockPublicAcls: false
              blockPublicPolicy: false
              ignorePublicAcls: false
              restrictPublicBuckets: false
    
          bucketPolicy:
            type: aws:s3/bucketPolicy:BucketPolicy
            properties:
              bucket: ${bucket.id}
              policy:
                fn::toJSON:
                  Version: "2012-10-17"
                  Statement:
                    - Effect: Allow
                      Principal: "*"
                      Action:
                        - s3:GetObject
                      Resource:
                        - arn:aws:s3:::${bucket.bucket}/*
            options:
              dependsOn:
                - ${publicAccessBlock}
    
        outputs:
          endpoint: ${bucketWebsite.websiteEndpoint}

    Detailed implementation breakdown

    Let’s dissect this component implementation piece-by-piece:

    Inheriting from the base class

    export class StaticPage extends pulumi.ComponentResource {

    Inheriting from pulumi.ComponentResource gives us some built-in behind-the-scenes behavior that allows the component state to be tracked and run within the Pulumi engine and within its host provider. It also allows the underlying library to find and infer the schema of the component.

    Outputs as class properties

        public readonly endpoint: pulumi.Output<string>;

    We use a class property to store the output value. Component outputs should always use pulumi.Output<T> rather than plain types. This ensures that outputs can be passed directly to other Pulumi resource inputs, which also accept pulumi.Output<T>, without any additional unwrapping or conversion.

    The Component constructor

        constructor(name: string, args: StaticPageArgs, opts?: pulumi.ComponentResourceOptions) {
            super("sample-components:index:StaticPage", name, args, opts);

    The constructor has a few standard arguments:

    • name: The name given to an instance of this component. When writing a Pulumi program, resources are named by the end-user. Later on in the implementation we will use this base component name to uniquely name the resources it contains.
    • args: This is an instance of the argument class we defined earlier, containing the inputs for our component.
    • opts: This is an optional set of common resource configuration values. The ComponentResourceOptions class is part of the basic API for all Pulumi resources, and will be passed to the constructors of our sub-resources later on.

    Since we’re inheriting, we also need to call the base class constructor super(...). The first parameter is the name of the resource type, which is very important to get right. The resource type name has the following format: <package-name>:index:<component-class-name>. It must match exactly. Keep this in mind if you refactor the name of your package or the component’s class name. The index portion of this type name is a required implementation detail. Otherwise, we pass the name, args, and opts values from our component constructor into the base constructor.

    Changing the resource type name after a component has been deployed will cause all existing resources of that type to be recreated. Change the type name with extreme caution.

    Creating and managing sub-resources, dependencies, and execution order

    Next we implement the Bucket, BucketWebsiteConfiguration, BucketObject, BucketPublicAccessBlock and BucketPolicy sub-resources.

            const bucket = new aws.s3.Bucket(`${name}-bucket`, {}, { parent: this });
    
            const bucketWebsite = new aws.s3.BucketWebsiteConfiguration(
                `${name}-website`,
                {
                    bucket: bucket.bucket,
                    indexDocument: { suffix: "index.html" },
                },
                {
                    parent: this,
                },
            );
    
            new aws.s3.BucketObject(
                `${name}-index-object`,
                {
                    bucket: bucket.bucket,
                    key: "index.html",
                    content: args.indexContent,
                    contentType: "text/html",
                },
                {
                    parent: this,
                },
            );
    
            const publicAccessBlock = new aws.s3.BucketPublicAccessBlock(
                `${name}-public-access-block`,
                {
                    bucket: bucket.id,
                    blockPublicAcls: false,
                },
    The Bucket sub-resource

    The Bucket resource represents an S3 bucket, which is similar to a directory. This is our public-facing entry point for hosting website content on the internet.

    Notice the use of the name parameter and format string to create a unique name for the bucket resource. Every resource must have a unique name. We will use the same pattern in all the sub-resources.

    Another important implementation detail here is the opts value being passed to the sub-resource constructor. We create a new instance of ResourceOptions and pass the component instance as the parent of the Bucket resource, via parent: this in the ResourceOptions class. This is an essential step to tie the sub-resources into the dependency graph.

    The BucketWebsiteConfiguration and BucketObject sub-resources

    The BucketWebsiteConfiguration represents the website configuration and the BucketObject represents the contents of the file we will host as index.html.

    Managing the dependency graph of your sub-resources is very important in a component!

    Another point of interest here is the use of args. In the BucketObject constructor, we pass the contents of the index.html page we want to host via the args class. It’s available as a strongly typed property accessor on the args class.

    The BucketPublicAccessBlock and BucketPolicy sub-resources

    By default the BucketObject we created is not accessible to the public, so we need to unlock that access with the BucketPublicAccessBlock and BucketPolicy resources.

    The BucketPolicy resource shows an important coding technique when implementing components: handling asynchronous output values. We use bucket.bucket.apply(...) to generate an S3 policy document using the allowGetObjectPolicy helper function. This respects the asynchronous workflow, materializing that value only after the bucket has been created. If we attempted to create a BucketPolicy before the Bucket existed, the operation would fail. That’s because the S3 policy document needs to use the bucket’s name within its definition, and we won’t know what that value is until the Bucket creation operation has completed. Using apply here will ensure that execution of the allowGetObjectPolicy function doesn’t happen until the bucket has been created successfully.

    Just like in a Pulumi program, it’s important to understand and respect the asynchronous flow of resource creation within our code. The apply function encodes the dependency and required order-of-operations.

    The BucketPolicy resource also shows another technique: resource dependencies. We use the dependsOn resource option to indicate that the BucketPolicy depends on the BucketPublicAccessBlock. This relationship is important to encode so that resource creation, modification, and deletion happens as expected.

    Handling outputs

    The last part of the constructor handles output values. First we set the endpoint class property to the website endpoint from the BucketWebsiteConfiguration class. Note that this is a pulumi.Output<string>, not a regular TypeScript string. Outputs must use pulumi.Output types.

    Finally, calling this.registerOutputs signals Pulumi that the component creation process has completed.

                    parent: this,
                },
            );
    
            new aws.s3.BucketPolicy(
                `${name}-bucket-policy`,

    Helper functions

    In addition to the constructor logic, we also have a helper function allowGetObjectPolicy:

    Example: StaticPage.ts a helper function

                    bucket: bucket.id,
                    policy: bucket.bucket.apply(this.allowGetObjectPolicy),
                },
                {
                    parent: this,
                    dependsOn: publicAccessBlock,
                },
            );
    
            this.endpoint = bucketWebsite.websiteEndpoint;
    
            this.registerOutputs({
                endpoint: bucketWebsite.websiteEndpoint,

    This function is used to create a S3 policy document, allowing public access to the objects in our bucket. It will be invoked within the context of apply(...). That means that the bucketName, which is normally a pulumi.Output<str> value, can be materialized as a normal TypeScript string, and is passed into this function that way. Note that you can’t modify the value of bucketName, but you can read the value and use it to construct the policy document. The JSON.stringify(...) function takes the object as input and returns it as a JSON formatted string.

    Inheriting from the base class

    class StaticPage(pulumi.ComponentResource):

    Inheriting from pulumi.ComponentResource gives us some built-in behind-the-scenes behavior that allows the component state to be tracked and run within the Pulumi engine. It also allows the underlying library to find and infer the schema of the component.

    Outputs as class properties

        endpoint: pulumi.Output[str]
        """The URL of the static website."""

    We use a class property to store the output value. Component outputs should always use pulumi.Output[T] rather than plain types. This ensures that outputs can be passed directly to other Pulumi resource inputs, which also accept pulumi.Output[T], without any additional unwrapping or conversion.

    The Component constructor

        def __init__(self,
                     name: str,
                     args: StaticPageArgs,
                     opts: Optional[ResourceOptions] = None) -> None:
    
            super().__init__("sample-components:index:StaticPage", name, {}, opts)

    The constructor has a few standard arguments:

    • name: The name given to an instance of this component. When writing a Pulumi program, resources are named by the end-user. Later on in the implementation we will use this base component name to uniquely name the resources it contains.
    • args: This is an instance of the argument class we defined earlier, containing the inputs for our component.
    • opts: This is an optional set of common resource configuration values. The ComponentResourceOptions class is part of the basic API for all Pulumi resources, and will be passed to the constructors of our sub-resources later on.

    Since we’re inheriting, we also need to call the base class constructor super().__init__. The first parameter is the name of the resource type, which is very important to get right. The resource type name has the following format: <package-name>:index:<component-class-name>. It must match exactly. Keep this in mind if you refactor the name of your package or the component’s class name. The index portion of this type name is a required implementation detail. Otherwise, we pass the name value into the base constructor, as well as the opts value, and an empty object for the args value.

    Changing the resource type name after a component has been deployed will cause all existing resources of that type to be recreated. Change the type name with extreme caution.

    Creating and managing sub-resources, dependencies, and execution order

    Next we implement the Bucket, BucketWebsiteConfiguration, BucketObject, BucketPublicAccessBlock and BucketPolicy sub-resources.

            bucket = s3.Bucket(
                f"{name}-bucket",
                opts=ResourceOptions(parent=self))
    
            bucket_website = s3.BucketWebsiteConfiguration(
                f"{name}-website",
                bucket=bucket.bucket,
                index_document={"suffix": "index.html"},
                opts=ResourceOptions(parent=self))
    
            s3.BucketObject(
                f"{name}-index-object",
                bucket=bucket.bucket,
                key="index.html",
                content=args.get("index_content"),
                content_type="text/html",
                opts=ResourceOptions(parent=self))
    
            bucket_public_access_block = s3.BucketPublicAccessBlock(
                f"{name}-public-access-block",
                bucket=bucket.id,
                block_public_acls=False,
                opts=ResourceOptions(parent=self))
    
            s3.BucketPolicy(
                f"{name}-bucket-policy",
                bucket=bucket.bucket,
                policy=bucket.bucket.apply(_allow_getobject_policy),
                opts=ResourceOptions(parent=self, depends_on=[bucket_public_access_block]))
    The Bucket sub-resource

    The Bucket resource represents an S3 bucket, which is similar to a directory. This is our public-facing entry point for hosting website content on the internet.

    Notice the use of the name parameter and format string to create a unique name for the bucket resource. Every resource must have a unique name. We will use the same pattern in all the sub-resources.

    Another important implementation detail here is the opts value being passed to the sub-resource constructor. We create a new instance of ResourceOptions and pass the component instance as the parent of the Bucket resource, via parent=self in the ResourceOptions class. This is an essential step to tie the sub-resources into the dependency graph.

    The BucketWebsiteConfiguration and BucketObject sub-resources

    The BucketWebsiteConfiguration represents the website configuration and the BucketObject represents the contents of the file we will host as index.html.

    Managing the dependency graph of your sub-resources is very important in a component!

    Another point of interest here is the use of args. In the BucketObject constructor, we pass the contents of the index.html page we want to host via the args class. Instead of using a property accessor on the args class, we use args.get(...) and pass the name of the value we want.

    The BucketPublicAccessBlock and BucketPolicy sub-resources

    By default the BucketObject we created is not accessible to the public, so we need to unlock that access with the BucketPublicAccessBlock and BucketPolicy resources.

    The BucketPolicy resource shows an important coding technique when implementing components: handling asynchronous output values. We use bucket.bucket.[apply](https://www.pulumi.com/docs/iac/concepts/inputs-outputs/apply/)(...) to generate an S3 policy document using the _allow_getobject_policy helper function. This respects the asynchronous workflow, materializing that value only after the bucket has been created. If we attempted to create a BucketPolicy before the Bucket existed, the operation would fail. That’s because the S3 Policy document needs to use the bucket’s name within S3, and we won’t know what that value is until the Bucket creation operation has completed. Using apply here will ensure that execution of the _allow_getobject_policy function doesn’t happen until the Bucket has been created successfully.

    Just like in a Pulumi program, it’s important to understand and respect the asynchronous flow of resource creation within our code. The apply function encodes the dependency and required order-of-operations.

    The BucketPolicy resource also shows another technique: resource dependencies. We use the depends_on resource option to indicate that the BucketPolicy depends on the BucketPublicAccessBlock. This relationship is important to encode so that resource creation, modification, and deletion happens as expected.

    Handling outputs

    The last part of the constructor handles output values. First we set the endpoint class property to the end-point URL from the BucketWebsiteConfiguration class. Note that this is a pulumi.Output[str], not a regular Python string. Outputs must use pulumi.Output types.

    Finally, calling self.register_outputs signals Pulumi that the component creation process has completed.

            self.endpoint = bucket_website.website_endpoint
    
            self.register_outputs({
                "endpoint": bucket_website.website_endpoint
            })

    Helper functions

    In addition to the constructor logic, we also have a helper function _allow_getobject_policy:

    Example: static_page.py a helper function

    def _allow_getobject_policy(bucket_name: str) -> str:
        return json.dumps({
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow",
                    "Principal": "*",
                    "Action": ["s3:GetObject"],
                    "Resource": [
                        f"arn:aws:s3:::{bucket_name}/*",
                    ],
                },
            ],
        })

    This function is used to create a S3 policy document, allowing public access to the objects in our bucket. It will be invoked within the context of apply(...). That means that the bucket_name, which is normally a pulumi.Output[str] value, can be materialized as a normal Python string, and is passed into this function that way. Note that you can’t modify the value of bucket_name, but you can read the value and use it to construct the policy document. The json.dumps(...) function takes the dictionary as input and returns it as a JSON formatted string.

    Defining the struct

    type StaticPage struct {
    	pulumi.ResourceState
    	Endpoint pulumi.StringOutput `pulumi:"endpoint"`
    }

    The struct must embed the pulumi.ResourceState struct. This gives us some built-in behind-the-scenes behavior that allows the component state to be tracked and run within the Pulumi engine and within its host provider. It also allows the underlying library to find and infer the schema of the component.

    Outputs as struct fields

    	Endpoint pulumi.StringOutput `pulumi:"endpoint"`

    We use a struct field to store the output value. Component outputs should always use a pulumi.*Output type (such as pulumi.StringOutput) rather than plain types. This ensures that outputs can be passed directly to other Pulumi resource inputs, which also accept these output types, without any additional unwrapping or conversion. The pulumi:"endpoint" tag defines the name of the property and allows for reflection to generate schema.

    The Component constructor

    func NewStaticPage(ctx *pulumi.Context, name string, args *StaticPageArgs, opts ...pulumi.ResourceOption) (*StaticPage, error) {
    	comp := &StaticPage{}
    	err := ctx.RegisterComponentResource("sample-components:index:StaticPage", name, comp, opts...)
    	if err != nil {
    		return nil, err
    	}

    The constructor has a few standard arguments:

    • ctx: The Pulumi context, which allows for interaction w/ the Pulumi engine
    • name: The name given to an instance of this component. When writing a Pulumi program, resources are named by the end-user. Later on in the implementation we will use this base component name to uniquely name the resources it contains.
    • args: This is an instance of the argument class we defined earlier, containing the inputs for our component.
    • opts: This is an optional set of common resource configuration values. The ComponentResourceOptions class is part of the basic API for all Pulumi resources, and will be passed to the constructors of our sub-resources later on.

    The next step is to register our new component instance with Pulumi via the ctx instance. The first parameter is the name of the resource type, which is very important to get right. The resource type name has the following format: <package-name>:index:<component-class-name>. It must match exactly. Keep this in mind if you refactor the name of your package or the component’s class name. The index portion of this type name is a required implementation detail. Otherwise, we pass the name value, our component instance, as well as the opts values.

    Changing the resource type name after a component has been deployed will cause all existing resources of that type to be recreated. Change the type name with extreme caution.

    Creating and managing sub-resources, dependencies, and execution order

    Next we implement the Bucket, BucketWebsiteConfiguration, BucketObject, BucketPublicAccessBlock and BucketPolicy sub-resources.

    	bucket, err := s3.NewBucket(ctx, fmt.Sprintf("%s-bucket", name), &s3.BucketArgs{},
    		pulumi.Parent(comp))
    	if err != nil {
    		return nil, err
    	}
    
    	bucketWebsite, err := s3.NewBucketWebsiteConfiguration(ctx, fmt.Sprintf("%s-website", name),
    		&s3.BucketWebsiteConfigurationArgs{
    			Bucket: bucket.Bucket,
    			IndexDocument: s3.BucketWebsiteConfigurationIndexDocumentArgs{
    				Suffix: pulumi.String("index.html"),
    			},
    		},
    		pulumi.Parent(comp))
    	if err != nil {
    		return nil, err
    	}
    
    	_, err = s3.NewBucketObject(ctx, fmt.Sprintf("%s-index-object", name), &s3.BucketObjectArgs{
    		Bucket:      bucket.Bucket,
    		Key:         pulumi.String("index.html"),
    		Content:     args.IndexContent,
    		ContentType: pulumi.String("text/html"),
    	}, pulumi.Parent(comp))
    	if err != nil {
    		return nil, err
    	}
    
    	publicAccessBlock, err := s3.NewBucketPublicAccessBlock(ctx, fmt.Sprintf("%s-public-access-block", name),
    		&s3.BucketPublicAccessBlockArgs{
    			Bucket:          bucket.ID(),
    			BlockPublicAcls: pulumi.Bool(false),
    		}, pulumi.Parent(comp))
    	if err != nil {
    		return nil, err
    	}
    
    	allowGetObjectPolicy := func(bucketName string) (string, error) {
    		policy := map[string]interface{}{
    			"Version": "2012-10-17",
    			"Statement": []map[string]interface{}{
    				{
    					"Effect":    "Allow",
    					"Principal": "*",
    					"Action":    []string{"s3:GetObject"},
    					"Resource":  []string{fmt.Sprintf("arn:aws:s3:::%s/*", bucketName)},
    				},
    			},
    		}
    		policyJSON, err := json.Marshal(policy)
    		if err != nil {
    			return "", err
    		}
    		return string(policyJSON), nil
    	}
    
    	_, err = s3.NewBucketPolicy(ctx, fmt.Sprintf("%s-bucket-policy", name), &s3.BucketPolicyArgs{
    		Bucket: bucket.ID(),
    		Policy: bucket.Bucket.ApplyT(func(bucketName string) (string, error) {
    			return allowGetObjectPolicy(bucketName)
    		}).(pulumi.StringOutput),
    	}, pulumi.Parent(comp), pulumi.DependsOn([]pulumi.Resource{publicAccessBlock}))
    	if err != nil {
    		return nil, err
    	}
    The Bucket sub-resource

    The Bucket resource represents an S3 bucket, which is similar to a directory. This is our public-facing entry point for hosting website content on the internet.

    Notice the use of the name parameter and format string to create a unique name for the bucket resource. Every resource must have a unique name. We will use the same pattern in all the sub-resources.

    Another important implementation detail here is the opts value being passed to the sub-resource constructor. We use pulumi.Parent(comp) to pass the component instance as the parent of the Bucket resource. This is an essential step to tie the sub-resources into the dependency graph.

    The BucketWebsiteConfiguration and BucketObject sub-resources

    The BucketWebsiteConfiguration represents the website configuration and the BucketObject represents the contents of the file we will host as index.html.

    Managing the dependency graph of your sub-resources is very important in a component!

    Another point of interest here is the use of args. In the BucketObject constructor, we pass the contents of the index.html page we want to host via the args.IndexContent field.

    The BucketPublicAccessBlock and BucketPolicy sub-resources

    By default the BucketObject we created is not accessible to the public, so we need to unlock that access with the BucketPublicAccessBlock and BucketPolicy resources.

    The BucketPolicy resource shows an important coding technique when implementing components: handling asynchronous output values. We use bucket.Bucket.[ApplyT](/docs/iac/concepts/inputs-outputs/apply/)(...) to generate an S3 policy document using the allowGetObjectPolicy helper function. This respects the asynchronous workflow, materializing that value only after the bucket has been created. If we attempted to create a BucketPolicy before the Bucket existed, the operation would fail. That’s because the S3 Policy document needs to use the bucket’s name within S3, and we won’t know what that value is until the Bucket creation operation has completed. Using ApplyT here will ensure that execution of the allowGetObjectPolicy function doesn’t happen until the Bucket has been created successfully.

    Just like in a Pulumi program, it’s important to understand and respect the asynchronous flow of resource creation within our code. The ApplyT function encodes the dependency and required order-of-operations.

    The BucketPolicy resource also shows another technique: resource dependencies. We use pulumi.DependsOn([]pulumi.Resource{publicAccessBlock}) to set the [dependsOn](/docs/iac/concepts/options/dependson/) resource option to indicate that the BucketPolicy depends on the BucketPublicAccessBlock. This relationship is important to encode so that resource creation, modification, and deletion happens as expected.

    Handling outputs

    The last part of the constructor handles output values. First we set the Endpoint struct field to the end-point URL from the BucketWebsiteConfiguration resource. Note that this is a pulumi.StringOutput, not a regular Go string. Outputs must use pulumi.Output types.

    Finally, return the component instance.

    	comp.Endpoint = bucketWebsite.WebsiteEndpoint
    
    	return comp, nil
    }

    Helper functions

    In addition to the resource constructor logic, we also had this inline helper function allowGetObjectPolicy:

    Example: static_page.go a helper function

    	allowGetObjectPolicy := func(bucketName string) (string, error) {
    		policy := map[string]interface{}{
    			"Version": "2012-10-17",
    			"Statement": []map[string]interface{}{
    				{
    					"Effect":    "Allow",
    					"Principal": "*",
    					"Action":    []string{"s3:GetObject"},
    					"Resource":  []string{fmt.Sprintf("arn:aws:s3:::%s/*", bucketName)},
    				},
    			},
    		}
    		policyJSON, err := json.Marshal(policy)
    		if err != nil {
    			return "", err
    		}
    		return string(policyJSON), nil
    	}

    This function is used to create a S3 policy document, allowing public access to the objects in our bucket. It will be invoked within the context of ApplyT(...). That means that the bucketName, which is normally an asynchronous pulumi.StringOutput value, can be materialized as a normal Go string, and is passed into this function that way. Note that you can’t modify the value of bucketName, but you can read the value and use it to construct the policy document. The json.Marshal(...) function takes the map as input and returns it as a JSON formatted string.

    Inheriting from the base class

    class StaticPage : ComponentResource {

    Inheriting from Pulumi.ComponentResource gives us some built-in behind-the-scenes behavior that allows the component state to be tracked and run within the Pulumi engine and within its host provider. It also allows the underlying library to find and infer the schema of the component.

    Outputs as class properties

        [Output("endpoint")]
        public Output<string> Endpoint { get; set; }

    We use a class property to store the output value. Component outputs should always use Pulumi.Output<T> rather than plain types. This ensures that outputs can be passed directly to other Pulumi resource inputs, which also accept Pulumi.Output<T>, without any additional unwrapping or conversion.

    The Component constructor

        public StaticPage(string name, StaticPageArgs args, ComponentResourceOptions? opts = null)
            : base("sample-components:index:StaticPage", name, args, opts)
        {

    The constructor has a few standard arguments:

    • name: The name given to an instance of this component. When writing a Pulumi program, resources are named by the end-user. Later on in the implementation we will use this base component name to uniquely name the resources it contains.
    • args: This is an instance of the argument class we defined earlier, containing the inputs for our component.
    • opts: This is an optional set of common resource configuration values. The ComponentResourceOptions class is part of the basic API for all Pulumi resources, and will be passed to the constructors of our sub-resources later on.

    Since we’re inheriting, we also need to call the base class constructor base(...). The first parameter is the name of the resource type, which is very important to get right. The resource type name has the following format: <package-name>:<module>:<component-class-name>. It must match exactly. Keep this in mind if you refactor the name of your package or the component’s class name. The module portion of this type name is always index and is a required implementation detail. Otherwise, we pass the name, args, and opts values from our component constructor into the base constructor.

    Changing the resource type name after a component has been deployed will cause all existing resources of that type to be recreated. Change the type name with extreme caution.

    Creating and managing sub-resources, dependencies, and execution order

    Next we implement the Bucket, BucketWebsiteConfiguration, BucketObject, BucketPublicAccessBlock and BucketPolicy sub-resources.

            var bucket = new Bucket($"{name}-bucket", new() { }, new() { Parent = this });
    
            var bucketWebsite = new BucketWebsiteConfiguration($"{name}-website", new() {
                Bucket = bucket.BucketName,
                IndexDocument = new BucketWebsiteConfigurationIndexDocumentArgs { Suffix = "index.html" },
            }, new() { Parent = this });
    
            var bucketObject = new BucketObject($"{name}-index-object", new BucketObjectArgs {
                Bucket = bucket.BucketName,
                Key = "index.html",
                Content = args.IndexContent,
                ContentType = "text/html",
            }, new() { Parent = this });
    
            var publicAccessBlock = new BucketPublicAccessBlock($"{name}-public-access-block", new() {
                Bucket = bucket.BucketName,
                BlockPublicAcls = false,
            }, new() { Parent = this });
    
            var bucketPolicy = new BucketPolicy($"{name}-bucket-policy", new() {
                Bucket = bucket.BucketName,
                Policy = bucket.Id.Apply(this.AllowGetObjectPolicy),
            }, new() { Parent = this, DependsOn = publicAccessBlock });
    The Bucket sub-resource

    The Bucket resource represents an S3 bucket, which is similar to a directory. This is our public-facing entry point for hosting website content on the internet.

    Notice the use of the name parameter and format string to create a unique name for the bucket resource. Every resource must have a unique name. We will use the same pattern in all the sub-resources.

    Another important implementation detail here is the opts value being passed to the sub-resource constructor. We create a new object and pass the component instance as the Parent of the Bucket resource, via Parent = this in the opts object. This is an essential step to tie the sub-resources into the dependency graph.

    The BucketWebsiteConfiguration and BucketObject sub-resources

    The BucketWebsiteConfiguration represents the website configuration and the BucketObject represents the contents of the file we will host as index.html.

    Managing the dependency graph of your sub-resources is very important in a component!

    Another point of interest here is the use of args. In the BucketObject constructor, we pass the contents of the index.html page we want to host via the args class. It’s available as a strongly typed property accessor on the StaticPageArgs class.

    The BucketPublicAccessBlock and BucketPolicy sub-resources

    By default the BucketObject we created is not accessible to the public, so we need to unlock that access with the BucketPublicAccessBlock and BucketPolicy resources.

    The BucketPolicy resource shows an important coding technique when implementing components: handling asynchronous output values. We use bucket.bucket.Apply(...) to generate an S3 policy document using the AllowGetObjectPolicy helper function. This respects the asynchronous workflow, materializing that value only after the bucket has been created. If we attempted to create a BucketPolicy before the Bucket existed, the operation would fail. That’s because the S3 policy document needs to use the bucket’s name within its definition, and we won’t know what that value is until the Bucket creation operation has completed. Using Apply here will ensure that execution of the AllowGetObjectPolicy function doesn’t happen until the bucket has been created successfully.

    Just like in a Pulumi program, it’s important to understand and respect the asynchronous flow of resource creation within our code. The Apply function encodes the dependency and required order-of-operations.

    The BucketPolicy resource also shows another technique: resource dependencies. We use the DependsOn resource option to indicate that the BucketPolicy depends on the BucketPublicAccessBlock. This relationship is important to encode so that resource creation, modification, and deletion happens as expected.

    Handling outputs

    The last part of the constructor handles output values. First we set the endpoint class property to the website endpoint from the BucketWebsiteConfiguration class. Note that this is a Pulumi.Output<string>, not a regular .NET string. Outputs must use Pulumi.Output types.

    Finally, calling this.RegisterOutputs signals Pulumi that the component creation process has completed.

            this.Endpoint = bucketWebsite.WebsiteEndpoint;
    
            this.RegisterOutputs(new Dictionary<string, object?> {
                ["endpoint"] = bucketWebsite.WebsiteEndpoint
            });
        }

    Helper functions

    In addition to the constructor logic, we also have a helper function AllowGetObjectPolicy:

    Example: StaticPage.cs a helper function

        private string AllowGetObjectPolicy(string bucketName) {
            return JsonConvert.SerializeObject(new {
                Version = "2012-10-17",
                Statement = new[] { new {
                    Effect = "Allow",
                    Principal = "*",
                    Action = new[] {
                        "s3:GetObject"
                    },
                    Resource = new[] {
                        $"arn:aws:s3:::{bucketName}/*"
                    }
                }}
            });
        }

    This function is used to create a S3 policy document, allowing public access to the objects in our bucket. It will be invoked within the context of Apply(...). That means that the bucketName, which is normally a Pulumi.Output<string> value, can be materialized as a regular .NET string, and is passed into this function that way. Note that you can’t modify the value of bucketName, but you can read the value and use it to construct the policy document. The JsonConvert.SerializeObject(...) function takes the object as input and returns it as a JSON formatted string.

    Inheriting from the base class

    class StaticPage extends ComponentResource {

    Inheriting from com.pulumi.resources.ComponentResource gives us some built-in behind-the-scenes behavior that allows the component state to be tracked and run within the Pulumi engine and within its host provider. It also allows the underlying library to find and infer the schema of the component.

    Outputs as class properties

        @Export(name = "endpoint", refs = { String.class }, tree = "[0]")
        public final Output<String> endpoint;

    We use a class property to store the output value. Component outputs should always use com.pulumi.core.Output<T> rather than plain types. This ensures that outputs can be passed directly to other Pulumi resource inputs, which also accept Output<T>, without any additional unwrapping or conversion.

    The @Export decorator marks this as an exported output, and allows us to set a specific name for the output value.

    The Component constructor

        public StaticPage(String name, StaticPageArgs args, ComponentResourceOptions opts) {
            super("sample-components:index:StaticPage", name, null, opts);

    The constructor has a few standard arguments:

    • name: The name given to an instance of this component. When writing a Pulumi program, resources are named by the end-user. Later on in the implementation we will use this base component name to uniquely name the resources it contains.
    • args: This is an instance of the argument class we defined earlier, containing the inputs for our component.
    • opts: This is an optional set of common resource configuration values. The ComponentResourceOptions class is part of the basic API for all Pulumi resources, and will be passed to the constructors of our sub-resources later on.

    Since we’re inheriting, we also need to call the base class constructor super(...). The first parameter is the name of the resource type, which is very important to get right. The resource type name has the following format: <package-name>:<module>:<component-class-name>. It must match exactly. Keep this in mind if you refactor the name of your package or the component’s class name. The module portion of this type name is always index and is a required implementation detail. Otherwise, we pass the name, args, and opts values from our component constructor into the base constructor.

    Changing the resource type name after a component has been deployed will cause all existing resources of that type to be recreated. Change the type name with extreme caution.

    Creating and managing sub-resources, dependencies, and execution order

    Next we implement the Bucket, BucketWebsiteConfiguration, BucketObject, BucketPublicAccessBlock and BucketPolicy sub-resources.

            var bucket = new Bucket(
                    String.format("%s-bucket", name),
                    null,
                    CustomResourceOptions.builder()
                            .parent(this)
                            .build());
    
            var bucketWebsite = new BucketWebsiteConfiguration(
                    String.format("%s-website", name),
                    BucketWebsiteConfigurationArgs.builder()
                            .bucket(bucket.id())
                            .indexDocument(
                                    BucketWebsiteConfigurationIndexDocumentArgs.builder()
                                            .suffix("index.html")
                                            .build())
                            .build(),
                    CustomResourceOptions.builder()
                            .parent(this)
                            .build());
    
            var bucketObject = new BucketObject(
                    String.format("%s-index-object", name),
                    BucketObjectArgs.builder()
                            .bucket(bucket.bucket())
                            .key("index.html")
                            .content(args.indexContent())
                            .contentType("text/html")
                            .build(),
                    CustomResourceOptions.builder()
                            .parent(this)
                            .build());
    
            var publicAccessBlock = new BucketPublicAccessBlock(
                    String.format("%s-public-access-block", name),
                    BucketPublicAccessBlockArgs.builder()
                            .bucket(bucket.id())
                            .blockPublicAcls(false)
                            .blockPublicPolicy(false)
                            .ignorePublicAcls(false)
                            .restrictPublicBuckets(false)
                            .build(),
                    CustomResourceOptions.builder()
                            .parent(this)
                            .build());
    
            var policyJson = bucket.arn().applyValue(bucketArn ->
                "{\n" +
                "  \"Version\": \"2012-10-17\",\n" +
                "  \"Statement\": [\n" +
                "    {\n" +
                "      \"Sid\": \"PublicReadGetObject\",\n" +
                "      \"Effect\": \"Allow\",\n" +
                "      \"Principal\": \"*\",\n" +
                "      \"Action\": \"s3:GetObject\",\n" +
                "      \"Resource\": \"" + bucketArn + "/*\"\n" +
                "    }\n" +
                "  ]\n" +
                "}"
            );
    
            var bucketPolicy = new BucketPolicy(
                    String.format("%s-bucket-policy", name),
                    BucketPolicyArgs.builder()
                            .bucket(bucket.id())
                            .policy(policyJson.applyValue(Either::ofLeft))
                            .build(),
                    CustomResourceOptions.builder()
                            .parent(this)
                            .dependsOn(publicAccessBlock)
                            .build());
    The Bucket sub-resource

    The Bucket resource represents an S3 bucket, which is similar to a directory. This is our public-facing entry point for hosting website content on the internet.

    Notice the use of the name parameter and format string to create a unique name for the bucket resource. Every resource must have a unique name. We will use the same pattern in all the sub-resources.

    Another important implementation detail here is the opts value being passed to the sub-resource constructor. We create a new object and pass the component instance as the parent of the Bucket resource, via parent(this) in the opts object. This is an essential step to tie the sub-resources into the dependency graph.

    The BucketWebsiteConfiguration and BucketObject sub-resources

    The BucketWebsiteConfiguration represents the website configuration and the BucketObject represents the contents of the file we will host as index.html.

    Managing the dependency graph of your sub-resources is very important in a component!

    Another point of interest here is the use of args. In the BucketObject constructor, we pass the contents of the index.html page we want to host via the args class. It’s available as a strongly typed property accessor on the StaticPageArgs class.

    The BucketPublicAccessBlock and BucketPolicy sub-resources

    By default the BucketObject we created is not accessible to the public, so we need to unlock that access with the BucketPublicAccessBlock and BucketPolicy resources.

    The BucketPolicy resource shows an important coding technique when implementing components: handling asynchronous output values. We use bucket.arn().applyValue(...) to generate an S3 policy document. This respects the asynchronous workflow, materializing that value only after the bucket has been created. If we attempted to create a BucketPolicy before the Bucket existed, the operation would fail. That’s because the S3 policy document needs to use the bucket’s ARN within its definition, and we won’t know what that value is until the Bucket creation operation has completed. Using applyValue here will ensure that the policy creation doesn’t happen until the bucket has been created successfully.

    Just like in a Pulumi program, it’s important to understand and respect the asynchronous flow of resource creation within our code. The applyValue function encodes the dependency and required order-of-operations.

    The BucketPolicy resource also shows another technique: resource dependencies. We use the DependsOn resource option to indicate that the BucketPolicy depends on the BucketPublicAccessBlock. This relationship is important to encode so that resource creation, modification, and deletion happens as expected.

    Handling outputs

    The last part of the constructor handles output values. First we set the endpoint class property to the website endpoint from the BucketWebsiteConfiguration class. Note that this is a com.pulumi.core.Output<String>, not a regular Java string. Outputs must use com.pulumi.core.Output<T> types.

    Finally, calling this.registerOutputs signals Pulumi that the component creation process has completed.

            this.endpoint = bucketWebsite.websiteEndpoint();
    
            this.registerOutputs(Map.of(
                    "endpoint", bucketWebsite.websiteEndpoint()));
        }

    Policy Creation

    The bucket policy is created using a JSON string that defines the access policy for the S3 bucket. With AWS Provider v7.x, the policy field expects either a JSON string or a PolicyDocumentArgs object, wrapped in an Either type.

    The policy JSON is created within the applyValue(...) context, where the bucketArn (normally a com.pulumi.core.Output<String> value) can be materialized as a regular Java string. We use Either::ofLeft to convert the JSON string into the expected Either<String, PolicyDocumentArgs> type that the AWS provider expects.

    Naming the component

    name: sample-components
    components:
      StaticPage:

    YAML components rely on built-in behind-the-scenes behavior that allows the component state to be tracked and run within a host provider. The Pulumi SDK will scan the definitions and infer the schema of the component. All we need to provide is a unique name for the resource type. Later consumers of the component can reference it by the package name and component type like this:

    name: sample-components
    components:
      StaticPage:

    It follows the schema of <package_name>:<component_resource_name>.

    Output properties

        outputs:
          endpoint: ${bucketWebsite.websiteEndpoint}

    The outputs section defines the outputs that will be shared to the consumer of the component. These will appear in consumer languages as pulumi.Output<T> equivalents, instead of just a regular string. This allows the end-user to access this in an asynchronous manner when writing their Pulumi program.

    Creating and managing sub-resources, dependencies, and execution order

    Next we implement the Bucket, BucketWebsiteConfiguration, BucketObject, BucketPublicAccessBlock and BucketPolicy sub-resources. Defining sub-resources in a YAML component works exactly the same as defining them in a YAML Pulumi program.

        resources:
          bucket:
            type: aws:s3:Bucket
            properties: {}
    
          bucketWebsite:
            type: aws:s3:BucketWebsiteConfiguration
            properties:
              bucket: ${bucket.bucket}
              indexDocument:
                suffix: index.html
    
          bucketObject:
            type: aws:s3/bucketObject:BucketObject
            properties:
              bucket: ${bucket.bucket}
              key: index.html
              content: ${indexContent}
              contentType: text/html
    
          publicAccessBlock:
            type: aws:s3/bucketPublicAccessBlock:BucketPublicAccessBlock
            properties:
              bucket: ${bucket.id}
              blockPublicAcls: false
              blockPublicPolicy: false
              ignorePublicAcls: false
              restrictPublicBuckets: false
    
          bucketPolicy:
            type: aws:s3/bucketPolicy:BucketPolicy
            properties:
              bucket: ${bucket.id}
              policy:
                fn::toJSON:
                  Version: "2012-10-17"
                  Statement:
                    - Effect: Allow
                      Principal: "*"
                      Action:
                        - s3:GetObject
                      Resource:
                        - arn:aws:s3:::${bucket.bucket}/*
            options:
              dependsOn:
                - ${publicAccessBlock}
    The Bucket sub-resource

    The Bucket resource represents an S3 bucket, which is similar to a directory. This is our public-facing entry point for hosting website content on the internet.

    Another important implementation detail here is the options section. By default the component instance will be set as the parent of the Bucket resource, so there’s no need to define that on this object.

    The BucketWebsiteConfiguration and BucketObject sub-resources

    The BucketWebsiteConfiguration represents the website configuration and the BucketObject represents the contents of the file we will host as index.html.

    Managing the dependency graph of your sub-resources is very important in a component!

    Another point of interest here is the use of the component input values. In the BucketObject definition, we pass the contents of the index.html page we want to host via ${indexContent} string interpolation. All input values are available for string interpolation.

    The BucketPublicAccessBlock and BucketPolicy sub-resources

    By default the BucketObject we created is not accessible to the public, so we need to unlock that access with the BucketPublicAccessBlock and BucketPolicy resources.

    The BucketPublicAccessBlock resource requires all four public access properties to be set to false to properly enable public access: blockPublicAcls, blockPublicPolicy, ignorePublicAcls, and restrictPublicBuckets. Setting only one or some of these properties will not allow the bucket policy to function correctly.

    The BucketPolicy resource shows an important coding technique when implementing components: handling asynchronous output values. We use the ${bucket.bucket} interpolation to generate an S3 policy document using the fn::toJSON: helper function. This respects the asynchronous workflow, waiting to create the BucketPolicy resource until after the bucket has been created. If the provider attempted to create a BucketPolicy before the Bucket existed, the operation would fail. That’s because the S3 policy document needs to use the bucket’s name within its definition, and we won’t know what that value is until the Bucket creation operation has completed. Pulumi’s YAML implementation handles that workflow automatically.

    The BucketPolicy resource also shows another technique: resource dependencies. We use the dependsOn resource option to indicate that the BucketPolicy depends on the BucketPublicAccessBlock. This relationship is important to encode so that resource creation, modification, and deletion happens as expected.

    Handling outputs

    The last part of the component definition handles output values. First we set the endpoint class property to the website endpoint from the BucketWebsiteConfiguration class. This uses standard string interopolation and automatically handles asynchronous value resolution, waiting to assign the endpoint output until bucketWebsite.websiteEndpoint has completed completion and the value is available.

        outputs:
          endpoint: ${bucketWebsite.websiteEndpoint}

    Helper functions

    In addition to the component definitions, we are also using a helper function fn::toJSON::

    Example: YAML helper function

              policy:
                fn::toJSON:
                  Version: "2012-10-17"
                  Statement:
                    - Effect: Allow
                      Principal: "*"
                      Action:
                        - s3:GetObject
                      Resource:
                        - arn:aws:s3:::${bucket.bucket}/*
            options:
              dependsOn:
                - ${publicAccessBlock}

    This function is used to create a S3 policy document, allowing public access to the objects in our bucket. It will be invoked only when the interpolated value ${bucket.bucket} is available as a standard string. We construct a YAML object which is then serialized to a JSON formatted string and assigned to the policy property.

    Use the Component in a Pulumi Program

    Let’s try it out in a Pulumi program.

    Setup the Pulumi Program

    First, let’s create a simple Pulumi program project. Create a new directory and a Pulumi.yaml file.

    $ cd ..
    $ mkdir use-static-page-component
    

    Example: A Pulumi.yaml file for a Pulumi project written in TypeScript

    name: sample-components-typescript
    description: A Pulumi TypeScript program that builds and uses a static page component
    runtime: nodejs
    config:
      pulumi:tags:
        value:
          pulumi:template: aws-typescript
    

    Generate the SDK

    In order to use our Pulumi component from source, we’ll need to generate a TypeScript-specific SDK to use in a Pulumi program. From the root directory of your use-static-page-component Pulumi project, run the following command:

    pulumi package add ../sample-components
    

    This creates a new subdirectory called sdks/ that contains the generated SDK.

    Add the NodeJS and TypeScript project files

    Now lets create our package.json and tsconfig.json. We’ll need the standard pulumi SDK and our custom component. To use that, just add the path to the generated TypeScript SDK using the format file:<path> instead of a package version spec.

    Example: package.json

    {
        "name": "sample-components-typescript",
        "main": "index.ts",
        "devDependencies": {
            "@types/node": "^18"
        },
        "dependencies": {
            "@pulumi/pulumi": "^3.0.0",
            "@pulumi/aws": "^7.0.0"
        }
    }
    

    Note that we don’t need to add the Pulumi AWS provider library here, because that dependency is handled by the component project, in whatever language you implemented it in. We just need to carry a reference to the component SDK which provides us access to the component via RPC to its provider host. This creates a clean separation of concerns between the component implementation and the end users of the component.

    The TypeScript config is the same as any standard Pulumi program.

    Example: tsconfig.json for a Pulumi program

    {
        "compilerOptions": {
            "strict": true,
            "outDir": "bin",
            "target": "es2016",
            "module": "commonjs",
            "moduleResolution": "node",
            "sourceMap": true,
            "experimentalDecorators": true,
            "pretty": true,
            "noFallthroughCasesInSwitch": true,
            "noImplicitReturns": true,
            "forceConsistentCasingInFileNames": true
        },
        "files": [
            "index.ts",
            "StaticPage.ts"
        ]
    }
    

    Install dependencies

    Next, install the pulumi dependencies:

    pulumi install
    

    Create the Pulumi program

    Example: index.ts that uses the Static Page component

    import * as pulumi from "@pulumi/pulumi";
    import { StaticPage } from "./StaticPage";
    
    const pageHTML = "<h1>I love Pulumi!</h1>";
    
    const page = new StaticPage("my-static-page", {
        indexContent: pageHTML,
    });
    
    export const websiteURL = pulumi.interpolate`http://${page.endpoint}`;
    

    Example: A Pulumi.yaml file for a Pulumi project written in Python

    name: sample-components-python
    description: A Pulumi Python program that builds and uses a static page component
    runtime:
      name: python
      options:
        virtualenv: venv
    config:
      pulumi:tags:
        value:
          pulumi:template: aws-python
    

    Generate the SDK

    In order to use our Pulumi component from source, we’ll need to generate a Python-specific SDK to use in a Pulumi program. From the root directory of your use-static-page-component Pulumi project, run the following command:

    pulumi package add ../sample-components
    

    This creates a new subdirectory called sdks/ that contains the generated SDK.

    Update Pulumi.yaml with packages reference

    You need to manually add the packages section to your Pulumi.yaml:

    name: sample-components-python
    description: A Pulumi Python program that builds and uses a static page component
    runtime:
      name: python
      options:
        virtualenv: venv
    config:
      pulumi:tags:
        value:
          pulumi:template: aws-python
    

    Add the package dependencies

    Now lets create our requirements.txt. We’ll need the standard pulumi SDK:

    pulumi>=3.0.0,<4.0
    pulumi_aws>=7.0.0,<8.0
    

    Note that we don’t need to add the Pulumi AWS provider library here, because that dependency is handled by the component project, in whatever language you implemented it in. We just need to carry a reference to the component SDK which provides us access to the component via RPC to its provider host. This creates a clean separation of concerns between the component implementation and the end users of the component.

    Setup the virtual environment

    Next, set up your virtual environment:

    $ python -m venv venv
    $ source venv/bin/activate
    $ pulumi install
    

    Create the Pulumi program

    Example: __main__.py that uses the Static Page component

    import pulumi
    from static_page import StaticPage, StaticPageArgs
    
    page_html = "<h1>I love Pulumi!</h1>"
    page = StaticPage(
        "my-static-page",
        StaticPageArgs(index_content=page_html)
    )
    
    website_url = page.endpoint.apply(lambda v: f"http://{v}")
    pulumi.export("websiteURL", website_url)
    

    Example: A Pulumi.yaml file for a Pulumi project written in Go

    name: sample-components-go
    description: A Pulumi Go program that builds and uses a static page component
    runtime: go
    config:
      pulumi:tags:
        value:
          pulumi:template: aws-go
    

    Add the Go project file

    Now lets create our go.mod. We’ll need the standard pulumi SDK and our custom component. Since our component is structured as a standard Go package, we can import it directly using a replace directive for local development.

    Example: go.mod for our Pulumi project

    module sample-components-go
    
    go 1.23.0
    
    require (
    	github.com/pulumi/pulumi-aws/sdk/v7 v7.6.0
    	github.com/pulumi/pulumi/sdk/v3 v3.185.0
    )
    

    Note that we don’t need to add the Pulumi AWS provider library here, because that dependency is handled by the component project. The replace directive tells Go to use our local component instead of trying to fetch it from a remote repository.

    Install dependencies

    Next, install the pulumi dependencies:

    go mod tidy
    

    Create the Pulumi program

    Example: main.go that uses the Static Page component

    package main
    
    import (
    	"sample-components-go/staticpagecomponent"
    
    	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
    )
    
    func main() {
    	pulumi.Run(func(ctx *pulumi.Context) error {
    		page, err := staticpagecomponent.NewStaticPage(ctx, "my-static-page", &staticpagecomponent.StaticPageArgs{
    			IndexContent: pulumi.String("<h1>I love Pulumi!</h1>"),
    		})
    		if err != nil {
    			return err
    		}
    
    		url := page.Endpoint.ApplyT(func(endpoint string) string {
    			return "http://" + endpoint
    		}).(pulumi.StringOutput)
    
    		ctx.Export("websiteURL", url)
    		return nil
    	})
    }
    

    Example: A Pulumi.yaml file for a Pulumi project written in C#

    name: sample-components-csharp
    description: A Pulumi C# program that builds and uses a static page component
    runtime:
      name: dotnet
    config:
      pulumi:tags:
        value:
          pulumi:template: aws-csharp
    

    Generate the SDK

    In order to use our Pulumi component from source, we’ll need to generate a .NET-specific SDK to use in a Pulumi program. From the root directory of your use-static-page-component Pulumi project, run the following command:

    pulumi package add ../sample-components
    

    This creates a new subdirectory called sdks/ that contains the generated SDK.

    Update Pulumi.yaml with packages reference

    You need to manually add the packages section to your Pulumi.yaml:

    name: sample-components-csharp
    description: A Pulumi C# program that builds and uses a static page component
    runtime:
      name: dotnet
    config:
      pulumi:tags:
        value:
          pulumi:template: aws-csharp
    

    Add the .NET project file

    Now lets create our .csproj. We’ll need the standard pulumi SDK and our custom component. To use the generated .NET SDK, just add the relative path to the project file in the Include attribute instead of the name of the library.

    Example: use-static-page-component.csproj

    <Project Sdk="Microsoft.NET.Sdk">
    
      <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net8.0</TargetFramework>
        <Nullable>enable</Nullable>
      </PropertyGroup>
    
      <ItemGroup>
        <PackageReference Include="Pulumi" Version="3.*" />
        <PackageReference Include="Pulumi.Aws" Version="7.*" />
        <PackageReference Include="Newtonsoft.Json" Version="13.*" />
      </ItemGroup>
    
    </Project>
    

    Note that we don’t need to add the Pulumi AWS provider library here, because that dependency is handled by the component project, in whatever language you implemented it in. We just need to carry a reference to the component SDK which provides us access to the component via RPC to its provider host. This creates a clean separation of concerns between the component implementation and the end users of the component.

    Install dependencies

    Next, install the pulumi dependencies:

    pulumi install
    

    Create the Pulumi program

    Example: Program.cs that uses the Static Page component

    using System.Collections.Generic;
    using System.Threading.Tasks;
    
    using Pulumi;
    
    return await Deployment.RunAsync(() =>
    {
        var pageHTML = "<h1>I love Pulumi!</h1>";
    
        var page = new StaticPage("my-static-page", new StaticPageArgs {
            IndexContent = pageHTML,
        });
    
        return new Dictionary<string, object?> {
            ["websiteURL"] = Output.Format($"http://{page.Endpoint}"),
        };
    });
    

    Example: A Pulumi.yaml file for a Pulumi project written in Java

    name: sample-components-java
    description: A Pulumi Java program that builds and uses a static page component
    runtime:
      name: java
    config:
      pulumi:tags:
        value:
          pulumi:template: aws-java
    

    Generate the SDK

    In order to use our Pulumi component from source, we’ll need to generate a Java-specific SDK to use in a Pulumi program. From the root directory of your use-static-page-component Pulumi project, run the following command:

    pulumi package add ../sample-components
    

    This creates a new subdirectory called sdks/ that contains the generated SDK.

    Add the Maven project file

    Now lets create our pom.xml. We’ll need the standard pulumi SDK and our custom component.

    We’ll need to add the sources from the generated SDK output into the build sources Maven looks for, and also add the dependencies. The output of the pulumi package add command should have given instructions on the necessary dependencies to add, in XML format. It will also suggest copying the source files into your src/main folder. Instead, we’ll leave the SDK files in place, and modify our build configuration to look in that directory as well as our normal source directory.

    Example: pom.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.pulumi</groupId>
        <artifactId>sample-components-java</artifactId>
        <version>1.0-SNAPSHOT</version>
    
        <properties>
            <encoding>UTF-8</encoding>
            <maven.compiler.source>11</maven.compiler.source>
            <maven.compiler.target>11</maven.compiler.target>
            <maven.compiler.release>11</maven.compiler.release>
            <mainClass>myproject.App</mainClass>
            <mainArgs/>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>com.pulumi</groupId>
                <artifactId>pulumi</artifactId>
                <version>(,1.0]</version>
            </dependency>
            <dependency>
                <groupId>com.pulumi</groupId>
                <artifactId>aws</artifactId>
                <version>(7.0.0,7.99]</version>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-jar-plugin</artifactId>
                    <version>3.2.2</version>
                    <configuration>
                        <archive>
                            <manifest>
                                <addClasspath>true</addClasspath>
                                <mainClass>${mainClass}</mainClass>
                            </manifest>
                        </archive>
                    </configuration>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-assembly-plugin</artifactId>
                    <version>3.3.0</version>
                    <configuration>
                        <archive>
                            <manifest>
                                <addClasspath>true</addClasspath>
                                <mainClass>${mainClass}</mainClass>
                            </manifest>
                        </archive>
                        <descriptorRefs>
                            <descriptorRef>jar-with-dependencies</descriptorRef>
                        </descriptorRefs>
                    </configuration>
                    <executions>
                        <execution>
                            <id>make-my-jar-with-dependencies</id>
                            <phase>package</phase>
                            <goals>
                                <goal>single</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
                <plugin>
                    <groupId>org.codehaus.mojo</groupId>
                    <artifactId>exec-maven-plugin</artifactId>
                    <version>3.0.0</version>
                    <configuration>
                        <mainClass>${mainClass}</mainClass>
                        <commandlineArgs>${mainArgs}</commandlineArgs>
                    </configuration>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-wrapper-plugin</artifactId>
                    <version>3.1.0</version>
                    <configuration>
                        <mavenVersion>3.8.5</mavenVersion>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    </project>
    

    Note that we don’t need to add the Pulumi AWS provider library here, because that dependency is handled by the component project, in whatever language you implemented it in. We just need to carry a reference to the component SDK, and add its dependencies, which will provide us access to the component via RPC, which is running inside of its provider host. This creates a clean separation of concerns between the component implementation and the end users of the component.

    Install dependencies

    Next, install the pulumi dependencies:

    pulumi install
    

    Create the Pulumi program

    Make a new subdirectory called src/main/java/myproject and in it, create a file called App.java.

    Example: App.java that uses the Static Page component

    package myproject;
    
    import com.pulumi.Pulumi;
    import com.pulumi.core.Output;
    
    public class App {
        public static void main(String[] args) {
            Pulumi.run(ctx -> {
                final var pageHTML = "<h1>I love Pulumi!</h1>";
    
                var page = new StaticPage("my-static-page", new StaticPageArgs(
                    Output.of(pageHTML)
                ), null);
    
                ctx.export("websiteURL", page.endpoint.applyValue(endpoint -> "http://" + endpoint));
            });
        }
    }
    

    Example: A Pulumi.yaml file for a Pulumi project written in YAML

    name: sample-components-yaml
    description: A Pulumi YAML program that builds and uses a static page component
    runtime:
      name: yaml

    Add the component reference in Pulumi.yaml

    Now, in your Pulumi.yaml file add the following section to load the component from source:

    packages:
      sample-components: ./sample-components

    The aws: "7.6.0" entry ensures we use the latest AWS provider v7.x with all the most recent features and improvements.

    Create the Pulumi program

    Now we can define our Static Page component resource in the Pulumi.yaml file. Add the following section:

    Example: Pulumi.yaml that uses the Static Page component

    resources:
      my-static-page:
        type: sample-components:StaticPage
        properties:
          indexContent: "<h1>I love Pulumi!</h1>"
    outputs:
      websiteURL: http://${my-static-page.endpoint}

    Finally, run the Pulumi update and we will see our component resource creates, as well as its sub-resources.

    $ pulumi up
    ...
         Type                                          Name                                Status
     +   pulumi:pulumi:Stack                           use-static-page-component-dev       created (8s)
     +   └─ sample-components:index:StaticPage     my-static-page                      created (5s)
     +      └─ aws:s3:Bucket                         my-static-page-bucket               created (2s)
     +         ├─ aws:s3:BucketPublicAccessBlock       my-static-page-public-access-block  created (0.84s)
     +         ├─ aws:s3:BucketWebsiteConfiguration  my-static-page-website              created (0.91s)
     +         ├─ aws:s3:BucketObject                  my-static-page-index-object         created (0.84s)
     +         └─ aws:s3:BucketPolicy                  my-static-page-bucket-policy        created (1s)
    
    Policies:
        ✅ pulumi-internal-policies@v0.0.6
    
    Outputs:
        websiteURL: "http://my-static-page-bucket-abcd123.s3-website-us-west-2.amazonaws.com"
    
    Resources:
        + 7 created
    
    Duration: 10s
    

    Success! We were able to use our component as just a single resource within our Pulumi program and it managed five other resources under the hood for us. This greatly reduces the amount of code an end user has to write to be able to host an HTML file in S3.

    Defining a component resource

    The walkthrough above demonstrated a complete component implementation. The following sections cover the rules and requirements in detail as a reference.

    To author a new component, either in a program or for a reusable library, create a subclass of ComponentResource. Inside of its constructor, chain to the base constructor, passing its type string, name, arguments, and options. Also inside of its constructor, allocate any child resources, passing the component resource itself as the parent option. Setting parent correctly ensures that child resources appear under the component in the resource graph, inherit provider configuration, and are tracked as dependencies of the component.

    Here’s a basic component example:

    class MyComponent extends pulumi.ComponentResource {
        constructor(name: string, myComponentArgs: MyComponentArgs, opts: pulumi.ComponentResourceOptions) {
            super("pkg:index:MyComponent", name, {}, opts);
        }
    }
    
    class MyComponent(pulumi.ComponentResource):
        def __init__(self, name, my_component_args, opts = None):
            super().__init__('pkg:index:MyComponent', name, None, opts)
    
    type MyComponent struct {
        pulumi.ResourceState
    }
    
    func NewMyComponent(ctx *pulumi.Context, name string, myComponentArgs MyComponentArgs, opts ...pulumi.ResourceOption) (*MyComponent, error) {
        myComponent := &MyComponent{}
        err := ctx.RegisterComponentResource("pkg:index:MyComponent", name, myComponent, opts...)
        if err != nil {
            return nil, err
        }
        return myComponent, nil
    }
    
    using System.Collections.Generic;
    using Pulumi;
    
    class MyComponent : ComponentResource
    {
        public MyComponent(string name, MyComponentArgs myComponentArgs, ComponentResourceOptions opts)
            : base("pkg:index:MyComponent", name, opts)
        {
        }
    }
    
    import com.pulumi.resources.ComponentResource;
    import com.pulumi.resources.ComponentResourceOptions;
    
    class MyComponent extends ComponentResource {
        public MyComponent(String name, MyComponentArgs myComponentArgs, ComponentResourceOptions opts) {
            super("pkg:index:MyComponent", name, null, opts);
        }
    }
    

    Upon creating a new instance of MyComponent, the call to the base constructor (using super/base) registers the component resource instance with the Pulumi engine. This records the resource’s state and tracks it across program deployments so that you see diffs during updates just like with a regular resource (even though component resources have no provider logic associated with them). Since all resources must have a name, a component resource constructor should accept a name and pass it to super.

    If you wish to have full control over one of the custom resource’s lifecycle in your component resource—including running specific code when a resource has been updated or deleted—you should look into dynamic providers. These let you create full-blown resource abstractions in your language of choice.

    A component resource must register a unique type name with the base constructor. In the example, the registration is pkg:index:MyComponent. To reduce the potential of other type name conflicts, this name contains the package and module name, in addition to the type: <package>:<module>:<type>. These names are namespaced alongside non-component resources, such as aws:lambda:Function.

    Component arguments and type requirements

    When authoring components that will be consumed across different languages (multi-language components), the arguments class has specific requirements and limitations due to the need for serialization. These constraints ensure that component arguments can be transmitted to the Pulumi engine and reconstructed across language boundaries.

    Serialization requirements

    Component arguments must be serializable, meaning you must convert them to a format that the engine can transmit and reconstruct. This is necessary because:

    1. The Pulumi engine needs to understand and validate the inputs
    2. Multi-language components need to translate arguments between languages
    3. The state needs to be stored and retrieved across deployments

    Supported types

    The following types are supported in component arguments:

    • Primitive types: string, number/int, boolean.

    • Arrays/lists: Arrays of any supported type.

    • Objects/maps: Objects with properties of supported types.

    • Input wrappers: Language-specific input types that wrap values:

      • TypeScript/JavaScript: pulumi.Input<T>
      • Python: pulumi.Input[T]
      • Go: pulumi.StringInput, pulumi.IntInput, etc.
      • .NET: Input<T>
      • Java: Output<T>
    • Enums: Enum types are supported in TypeScript and Python:

      TypeScript: Both the enum keyword and the “as const object pattern” are supported:

      // Using the enum keyword
      enum MyEnum { Value1 = "Value1", Value2 = "Value2" }
      
      // or alternatively using a const object
      const MyEnum = { Value1: "Value1", Value2: "Value2" } as const;
      type MyEnum = typeof MyEnum[keyof typeof MyEnum];
      

      Python: The standard library enum module is supported:

      from enum import Enum
      
      class MyEnum(Enum):
          VALUE1 = "Value1"
          VALUE2 = "Value2"
      
    • Utility types (TypeScript): The Required<T> and Partial<T> utility types are supported.

    Unsupported types

    The following types are not supported in component arguments:

    • Union types: TypeScript and Python union types like string | number are not supported due to limitations in schema inference. One exception: unions of undefined (TypeScript) or None (Python) with exactly one other type are supported to represent optional values (e.g., string | undefined or str | None).
    • Functions/callbacks: Functions cannot be used in component arguments as they cannot be represented in the schema.
    • Platform-specific types: Types that exist only in one language and cannot be translated.

    Design recommendations

    For better usability and maintainability:

    • Avoid deeply nested types: While complex generic types can be serialized, deeply nested structures make components harder to use and understand. Keep argument structures simple and flat when possible.

    Example of unsupported TypeScript types:

    // ❌ This will NOT work - union types are not supported
    export interface MyComponentArgs {
        value: string | number;  // Union type - unsupported
        callback: () => void;    // Function - unsupported
    }
    
    // ✅ This WILL work - use primitives or Input types
    export interface MyComponentArgs {
        value: pulumi.Input<string>;
        count: pulumi.Input<number>;
    }
    

    Constructor requirements by language

    Each language has specific requirements for component constructors to ensure proper schema generation:

    Requirements:

    • The constructor must have an argument named exactly args
    • The args parameter must have a type declaration (e.g., args: MyComponentArgs)
    class MyComponent extends pulumi.ComponentResource {
        constructor(name: string, args: MyComponentArgs, opts?: pulumi.ComponentResourceOptions) {
            super("pkg:index:MyComponent", name, {}, opts);
        }
    }
    

    Requirements:

    • The __init__ method must have an argument named exactly args
    • The args parameter must have a type annotation (e.g., args: MyComponentArgs)
    class MyComponent(pulumi.ComponentResource):
        def __init__(self, name: str, args: MyComponentArgs, opts: Optional[pulumi.ResourceOptions] = None):
            super().__init__('pkg:index:MyComponent', name, None, opts)
    

    Requirements:

    • The constructor function should accept a context, name, args struct, and variadic resource options
    • The args should be a struct type
    func NewMyComponent(ctx *pulumi.Context, name string, args *MyComponentArgs, opts ...pulumi.ResourceOption) (*MyComponent, error) {
        myComponent := &MyComponent{}
        err := ctx.RegisterComponentResource("pkg:index:MyComponent", name, myComponent, opts...)
        if err != nil {
            return nil, err
        }
        return myComponent, nil
    }
    

    Requirements:

    • The constructor must have exactly 3 arguments:
      1. A string for the name (or any unspecified first argument)
      2. An argument that is assignable from ResourceArgs (must extend ResourceArgs)
      3. An argument that is assignable from ComponentResourceOptions
    public class MyComponent : ComponentResource
    {
        public MyComponent(string name, MyComponentArgs args, ComponentResourceOptions? opts = null)
            : base("pkg:index:MyComponent", name, opts)
        {
        }
    }
    
    public sealed class MyComponentArgs : ResourceArgs
    {
        [Input("value")]
        public Input<string>? Value { get; set; }
    }
    

    Requirements:

    • The constructor must have exactly one argument that extends ResourceArgs
    • Other arguments (name, options) are not restricted but typically follow the standard pattern
    public class MyComponent extends ComponentResource {
        public MyComponent(String name, MyComponentArgs args, ComponentResourceOptions opts) {
            super("pkg:index:MyComponent", name, null, opts);
        }
    }
    
    class MyComponentArgs extends ResourceArgs {
        @Import(name = "value")
        private Output<String> value;
    
        public Output<String> getValue() {
            return this.value;
        }
    }
    

    Best practices

    When designing component arguments:

    1. Wrap all scalar members in Input types: Every scalar argument should be wrapped in the language’s input type (e.g., pulumi.Input<string>). This allows users to pass both plain values and outputs from other resources, avoiding the need to use apply for resource composition.
    2. Use basic types: Stick to primitive types, arrays, and basic objects.
    3. Avoid union types: Instead of a single value with multiple types, consider multiple, mutually exclusive argument members and validate that only one of them has a value in your component constructor.
    4. Document required vs. optional: Clearly document which arguments are required and which have defaults.
    5. Follow language conventions: Use camelCase for schema properties but follow language-specific naming in implementation (snake_case in Python, PascalCase in .NET).

    Creating child resources

    Component resources contain child resources. Child resource names must include the component resource’s name as part of their names (e.g., as a prefix). This ensures uniqueness across multiple instances of the component and ensures that if the component is renamed, the child resources are renamed as well. Also, when constructing a resource, children must be registered as such. To do this, pass the component resource itself as the parent option.

    This example demonstrates both the naming convention and how to designate the component resource as the parent:

    class MyComponent extends pulumi.ComponentResource {
        bucket: aws.s3.Bucket;
    
        constructor(name: string, myComponentArgs: MyComponentArgs, opts: pulumi.ComponentResourceOptions) {
            super("pkg:index:MyComponent", name, {}, opts);
    
            // Create Child Resource
            this.bucket = new aws.s3.Bucket(`${name}-bucket`,
                {}, { parent: this });
        }
    }
    
    class MyComponent(pulumi.ComponentResource):
        def __init__(self, name, my_component_args, opts = None):
            super().__init__('pkg:index:MyComponent', name, None, opts)
    
            # Create Child Resource
            self.bucket = s3.Bucket(f"{name}-bucket",
                opts=pulumi.ResourceOptions(parent=self))
    
    type MyComponent struct {
        pulumi.ResourceState
        Bucket *s3.Bucket
    }
    
    func NewMyComponent(ctx *pulumi.Context, name string, myComponentArgs MyComponentArgs, opts ...pulumi.ResourceOption) (*MyComponent, error) {
        myComponent := &MyComponent{}
        err := ctx.RegisterComponentResource("pkg:index:MyComponent", name, myComponent, opts...)
        if err != nil {
            return nil, err
        }
    
        // Create Child Resource
        bucket, err := s3.NewBucket(ctx, fmt.Sprintf("%s-bucket", name),
            &s3.BucketArgs{}, pulumi.Parent(myComponent))
        if err != nil {
            return nil, err
        }
        myComponent.Bucket = bucket
    
        return myComponent, nil
    }
    
    using System.Collections.Generic;
    using Pulumi;
    using Pulumi.Aws.S3;
    
    class MyComponent : ComponentResource
    {
        public Bucket Bucket { get; private set; }
    
        public MyComponent(string name, MyComponentArgs myComponentArgs, ComponentResourceOptions opts)
            : base("pkg:index:MyComponent", name, opts)
        {
            // Create Child Resource
            this.Bucket = new Bucket($"{name}-bucket",
                new BucketArgs(), new CustomResourceOptions { Parent = this });
        }
    }
    
    import com.pulumi.resources.ComponentResource;
    import com.pulumi.resources.ComponentResourceOptions;
    import com.pulumi.aws.s3.Bucket;
    import com.pulumi.aws.s3.BucketArgs;
    import com.pulumi.resources.CustomResourceOptions;
    
    class MyComponent extends ComponentResource {
        private final Bucket bucket;
    
        public MyComponent(String name, MyComponentArgs myComponentArgs, ComponentResourceOptions opts) {
            super("pkg:index:MyComponent", name, null, opts);
    
            // Create Child Resource
            this.bucket = new Bucket(String.format("%s-bucket", name),
                BucketArgs.builder().build(),
                CustomResourceOptions.builder()
                    .parent(this)
                    .build());
        }
    
        public Bucket bucket() {
            return this.bucket;
        }
    }
    

    Always set parent when creating resources inside a component. Omitting it causes the following problems:

    • Resource graph: Child resources won’t appear under the component in pulumi preview and the Pulumi Console, making the resource hierarchy unclear.
    • Provider inheritance: Child resources won’t automatically inherit provider configuration (such as region or credentials) passed to the component via the providers option.
    • Dependency tracking: Other resources that depend on the component won’t automatically wait for un-parented resources to finish creating, which can cause race conditions or incomplete deployments.

    Registering component outputs

    Component resources can define their own output properties. Outputs in a component must be registered with the Pulumi IaC engine by calling registerOutputs. The Pulumi engine uses this information to display the logical outputs of the component resource and any changes to those outputs will be shown during an update.

    For example, this code registers an S3 bucket’s computed domain name, which won’t be known until the bucket is created:

    class MyComponent extends pulumi.ComponentResource {
        bucket: aws.s3.Bucket;
    
        constructor(name: string, myComponentArgs: MyComponentArgs, opts: pulumi.ComponentResourceOptions) {
            super("pkg:index:MyComponent", name, {}, opts);
    
            this.bucket = new aws.s3.Bucket(`${name}-bucket`,
                {}, { parent: this });
    
            // Registering Component Outputs
            this.registerOutputs({
                bucketDnsName: this.bucket.bucketDomainName
            });
        }
    }
    
    class MyComponent(pulumi.ComponentResource):
        bucket_dns_name: pulumi.Output[str]
        """The DNS name of the bucket"""
    
        def __init__(self, name, my_component_args, opts = None):
            super().__init__('pkg:index:MyComponent', name, None, opts)
    
            self.bucket = s3.Bucket(f"{name}-bucket",
                opts=pulumi.ResourceOptions(parent=self))
    
            # Registering Component Outputs
            self.register_outputs({
                "bucket_dns_name": self.bucket.bucket_domain_name
            })
    
    type MyComponent struct {
        pulumi.ResourceState
        Bucket *s3.Bucket
    }
    
    func NewMyComponent(ctx *pulumi.Context, name string, myComponentArgs MyComponentArgs, opts ...pulumi.ResourceOption) (*MyComponent, error) {
        myComponent := &MyComponent{}
        err := ctx.RegisterComponentResource("pkg:index:MyComponent", name, myComponent, opts...)
        if err != nil {
            return nil, err
        }
    
        bucket, err := s3.NewBucket(ctx, fmt.Sprintf("%s-bucket", name),
            &s3.BucketArgs{}, pulumi.Parent(myComponent))
        if err != nil {
            return nil, err
        }
        myComponent.Bucket = bucket
    
        //Registering Component Outputs
        ctx.RegisterResourceOutputs(myComponent, pulumi.Map{
            "bucketDnsName": bucket.BucketDomainName,
        })
    
        return myComponent, nil
    }
    
    using System.Collections.Generic;
    using Pulumi;
    using Pulumi.Aws.S3;
    
    class MyComponent : ComponentResource
    {
        public Bucket Bucket { get; private set; }
    
        public MyComponent(string name, MyComponentArgs myComponentArgs, ComponentResourceOptions opts)
            : base("pkg:index:MyComponent", name, opts)
        {
    
            this.Bucket = new Bucket($"{name}-bucket",
                new BucketArgs(), new CustomResourceOptions { Parent = this });
    
            // Registering Component Outputs
            this.RegisterOutputs(new Dictionary<string, object?>
            {
                { "bucketDnsName", Bucket.BucketDomainName }
            });
        }
    }
    
    import com.pulumi.resources.ComponentResource;
    import com.pulumi.resources.ComponentResourceOptions;
    import com.pulumi.aws.s3.Bucket;
    import com.pulumi.aws.s3.BucketArgs;
    import com.pulumi.resources.CustomResourceOptions;
    
    class MyComponent extends ComponentResource {
        private final Bucket bucket;
    
        public MyComponent(String name, MyComponentArgs myComponentArgs, ComponentResourceOptions opts) {
            super("pkg:index:MyComponent", name, null, opts);
    
            this.bucket = new Bucket(String.format("%s-bucket", name),
                BucketArgs.builder().build(),
                CustomResourceOptions.builder()
                    .parent(this)
                    .build());
    
            // Registering Component Outputs
            this.registerOutputs(Map.of(
                "bucketDnsName", bucket.bucketDomainName()
            ));
        }
    
        public Bucket bucket() {
            return this.bucket;
        }
    }
    

    The call to registerOutputs typically happens at the very end of the component resource’s constructor.

    What registerOutputs does

    The registerOutputs call serves two purposes:

    1. Marks the component as fully constructed: It signals to the Pulumi engine that the component resource has finished registering all its child resources and should be considered complete.
    2. Saves outputs to state: It saves the component’s output properties to the state file so they appear in the Pulumi Console and CLI.

    Calling registerOutputs is strongly recommended, even when you have no outputs to register (pass an empty object). Without this call:

    • The component will appear as “creating…” in the CLI and Pulumi Console until the entire deployment completes, rather than when the component itself finishes.
    • The component’s output properties will not be saved to the state file. (Child resource state is unaffected.)
    • Resource hooks such as afterCreate will not fire on the component.
    .NET: Since pulumi-dotnet 3.59.0, calling RegisterOutputs() without arguments automatically registers all properties decorated with [Output].

    Inheriting resource providers

    One option all resources have is the ability to pass an explicit resource provider to supply explicit configuration settings. For instance, you may want to ensure that all AWS resources are created in a different region than the globally configured region. In the case of component resources, the challenge is that these providers must flow from parent to children.

    To allow this, component resources accept a providers option that custom resources don’t have. This value contains a map from the provider name to the explicit provider instance to use for the component resource. The map is used by a component resource to fetch the proper provider object to use for any child resources. This example overrides the globally configured AWS region and sets it to us-east-1. Note that myk8s is the name of the Kubernetes provider.

    let component = new MyComponent("...", {
        providers: {
            aws: useast1,
            kubernetes: myk8s,
        },
    });
    
    component = MyComponent('...', ResourceOptions(providers={
        'aws': useast1,
        'kubernetes': myk8s,
    }))
    
    component, err := NewMyResource(ctx, "...", nil, pulumi.ProviderMap(
        map[string]pulumi.ProviderResource{
            "aws":        awsUsEast1,
            "kubernetes": myk8s,
        },
    ))
    
    var component = new MyResource("...", new ComponentResourceOptions {
        Providers = {
            { "aws", awsUsEast1 },
            { "kubernetes", myk8s }
        }
    });
    
    var component = new MyResource("...",
        ComponentResourceOptions.builder()
            .providers(awsUsEast1, myk8s)
            .build());
    

    If a component resource is itself a child of another component resource, its set of providers is inherited from its parent by default.

    Next steps

    Now that you’ve built a component, you can package and distribute it for reuse. See Packaging Components to learn about single-language, cross-language, and provider-based packaging approaches.

    For testing strategies, see Testing Components.