1. Docs
  2. Pulumi IaC
  3. Using Pulumi
  4. 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.

    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.

    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: static-page-yaml description: A minimal Pulumi YAML program runtime: yaml packages:  static-page-component: ../ resources:  mystaticpage:  type: static-page-component:StaticPage  properties:  indexContent: "<h1>Hello, World!</h1>" 

    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 seperate project from your Pulumi program. So, let’s create a new directory for it, and create some project files:

    $ mkdir static-page-component $ cd static-page-component 

    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.

    Authoring sharable components in JavaScript is not currently supported. Considering writing in TypeScript instead!

    Example: PulumiPlugin.yaml for YAML

    runtime: yaml 

    Because we’re also authoring the component in YAML, the PulumiPlugin.yaml file will be used to define all aspects of the the component. In most other languages, a package/project configuration file would name the component module. In YAML, all you need to do is set the name property:

    runtime: yaml name: static-page-component 

    This name will be important later on in the component implementation, so make sure it’s something unique and descriptive!

    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": "static-page-component",  "description": "Static Page Component",  "dependencies": {  "@pulumi/aws": "6.73.0",  "@pulumi/pulumi": "^3.159.0"  },  "devDependencies": {  "@types/node": "^18.0.0",  "typescript": "^4.6.0"  } } 

    The @pulumi/pulumi SDK contains everything we need for making a component. It should be version 3.159.0 or newer. The @pulumi/aws package is the AWS provider that we are building on top of.

    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 

    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.159.0,<4.0 pulumi_aws>=6.0.0 

    The pulumi SDK contains everything we need for making a component. It should be version 3.159.0 or newer. 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/static-page-component  go 1.24  toolchain go1.24.1  require ( github.com/pulumi/pulumi-aws/sdk/v6 v6.74.0 github.com/pulumi/pulumi-go-provider v1.0.0 github.com/pulumi/pulumi/sdk/v3 v3.159.0 ) 

    The pulumi SDK contains everything we need for making a component. It should be version 3.159.0 or newer. 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>static-page-component</AssemblyName>  </PropertyGroup>   <ItemGroup>  <PackageReference Include="Pulumi" Version="3.77.0" />  <PackageReference Include="Pulumi.Aws" Version="6.*" />  <PackageReference Include="Newtonsoft.Json" Version="13.*" />  </ItemGroup> </Project> 

    The Pulumi SDK contains everything we need for making a component. It should be version 3.77.0 or newer. 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>static-page-component</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>1.7.36</version>  </dependency>  <dependency>  <groupId>com.google.code.gson</groupId>  <artifactId>gson</artifactId>  <version>2.8.9</version>  </dependency>  <dependency>  <groupId>com.pulumi</groupId>  <artifactId>pulumi</artifactId>  <version>[1.8,)</version>  </dependency>  <dependency>  <groupId>com.pulumi</groupId>  <artifactId>aws</artifactId>  <version>(6.0.2,6.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. It should be version 1.8 or newer. 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

    Authoring sharable components in JavaScript is not currently supported. Considering writing in TypeScript instead!

    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="static-page-component", components=[StaticPage]) 

    Here, the component_provider_host call invokes a Pulumi provider implmentation 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(), "static-page-component", "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{ "static-page-component": "index", }). Build() } 

    Here, the infer.NewProviderBuilder()..Build() call builds a Pulumi provider implmentation 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 (static-page-component) 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 implmentation 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.Metadata; import com.pulumi.provider.internal.ComponentProviderHost;  public class App {  public static void main(String[] args) throws IOException, InterruptedException {  new ComponentProviderHost("static-page-component", App.class.getPackage()).start(args);  } } 

    Here, the ComponentProviderHost.start(...) call invokes a Pulumi provider implmentation 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"; 

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

    Example: staticpage.py required dependencies

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

    First create a file called staticpage.go, and add the imports we will need:

    Example: staticpage.go required dependencies

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

    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; 

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

    Example: StaticPage.java required imports

    package staticpagecomponent;  import java.util.Map;  import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonObject;  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.BucketV2; import com.pulumi.aws.s3.BucketWebsiteConfigurationV2; import com.pulumi.aws.s3.BucketWebsiteConfigurationV2Args; import com.pulumi.aws.s3.inputs.BucketWebsiteConfigurationV2IndexDocumentArgs;  import com.pulumi.core.Output; 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; 

    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 implmentation

    export interface StaticPageArgs {  // The HTML content for index.html  indexContent: pulumi.Input<string>; } 

    Note that argument classes must be serializable and use pulumi.Input types, rather than the language’s default types.

    Example: staticpage.py the Component arguments implmentation

    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.

    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: staticpage.go the Component arguments implmentation

    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.

    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 implmentation

    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.

    Example: StaticPage.java the Component arguments implmentation

    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.

    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 implmentation

    runtime: yaml name: static-page-component components:  StaticPage:  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

    Authoring sharable components in JavaScript is not currently supported. Considering writing in TypeScript instead!

    Now we can implement the component itself. Components should inherit from pulumi.ComponentResource, and should accept the required 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 implmentation

    export class StaticPage extends pulumi.ComponentResource {  // The URL of the static website.  public readonly endpoint: pulumi.Output<string>;   constructor(name: string, args: StaticPageArgs, opts?: pulumi.ComponentResourceOptions) {  super("static-page-component:index:StaticPage", name, args, opts);   // Create a bucket  const bucket = new aws.s3.BucketV2(`${name}-bucket`, {}, { parent: this });   // Configure the bucket website  const bucketWebsite = new aws.s3.BucketWebsiteConfigurationV2(`${name}-website`, {  bucket: bucket.bucket,  indexDocument: { suffix: "index.html" },  }, { parent: bucket });   // Create a bucket object for the index document.  const bucketObject = new aws.s3.BucketObject(`${name}-index-object`, {  bucket: bucket.bucket,  key: 'index.html',  content: args.indexContent,  contentType: 'text/html',  }, { parent: bucket });   // Create a public access block for the bucket  const publicAccessBlock = new aws.s3.BucketPublicAccessBlock(`${name}-public-access-block`, {  bucket: bucket.id,  blockPublicAcls: false,  }, { parent: bucket });   // Set the access policy for the bucket so all objects are readable  const bucketPolicy = new aws.s3.BucketPolicy(`${name}-bucket-policy`, {  bucket: bucket.id, // refer to the bucket created earlier  policy: bucket.bucket.apply(this.allowGetObjectPolicy),  }, { parent: bucket, dependsOn: publicAccessBlock });   this.endpoint = bucketWebsite.websiteEndpoint;   // By registering the outputs on which the component depends, we ensure  // that the Pulumi CLI will wait for all the outputs to be created before  // considering the component itself to have been created.  this.registerOutputs({  endpoint: bucketWebsite.websiteEndpoint,  });  }   allowGetObjectPolicy(bucketName: string) {  return JSON.stringify({  Version: "2012-10-17",  Statement: [{  Effect: "Allow",  Principal: "*",  Action: [  "s3:GetObject"  ],  Resource: [  `arn:aws:s3:::${bucketName}/*`  ]  }]  });  } } 

    Now we can implement the component itself. Components should inherit from pulumi.ComponentResource, and should accept the required 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: staticpage.py the Component implmentation

    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__('static-page-component:index:StaticPage', name, {}, opts)   # Create a bucket  bucket = s3.BucketV2(  f'{name}-bucket',  opts=ResourceOptions(parent=self))   # Configure the bucket website  bucket_website = s3.BucketWebsiteConfigurationV2(  f'{name}-website',  bucket=bucket.bucket,  index_document={"suffix": "index.html"},  opts=ResourceOptions(parent=bucket))   # Create a bucket object for the index document  s3.BucketObject(  f'{name}-index-object',  bucket=bucket.bucket,  key='index.html',  content=args.get("index_content"),  content_type='text/html',  opts=ResourceOptions(parent=bucket))   # Create a public access block for the bucket  bucket_public_access_block = s3.BucketPublicAccessBlock(  f'{name}-public-access-block',  bucket=bucket.id,  block_public_acls=False,  opts=ResourceOptions(parent=bucket))   # Set the access policy for the bucket so all objects are readable.  s3.BucketPolicy(  f'{name}-bucket-policy',  bucket=bucket.bucket,  policy=bucket.bucket.apply(_allow_getobject_policy),  opts=ResourceOptions(parent=bucket, depends_on=[bucket_public_access_block]))   self.endpoint = bucket_website.website_endpoint   # By registering the outputs on which the component depends, we ensure  # that the Pulumi CLI will wait for all the outputs to be created before  # considering the component itself to have been created.  self.register_outputs({  'endpoint': bucket_website.website_endpoint  })   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}/*', # policy refers to bucket name explicitly  ],  },  ],  }) 

    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: staticpage.go the Component implmentation

    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("static-page-component:index:StaticPage", name, comp, opts...) if err != nil { return nil, err }  // Create a bucket bucket, err := s3.NewBucketV2(ctx, fmt.Sprintf("%s-bucket", name), &s3.BucketV2Args{}, pulumi.Parent(comp)) if err != nil { return nil, err }  // Configure bucket website bucketWebsite, err := s3.NewBucketWebsiteConfigurationV2(ctx, fmt.Sprintf("%s-website", name), &s3.BucketWebsiteConfigurationV2Args{ Bucket: bucket.Bucket, IndexDocument: s3.BucketWebsiteConfigurationV2IndexDocumentArgs{ Suffix: pulumi.String("index.html"), }, }, pulumi.Parent(bucket)) if err != nil { return nil, err }  // Create bucket object for index document _, 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(bucket)) if err != nil { return nil, err }  // Create public access block publicAccessBlock, err := s3.NewBucketPublicAccessBlock(ctx, fmt.Sprintf("%s-public-access-block", name), &s3.BucketPublicAccessBlockArgs{ Bucket: bucket.ID(), BlockPublicAcls: pulumi.Bool(false), }, pulumi.Parent(bucket)) if err != nil { return nil, err }  // Create bucket policy 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(bucket), pulumi.DependsOn([]pulumi.Resource{publicAccessBlock})) if err != nil { return nil, err }  // Set outputs comp.Endpoint = bucketWebsite.WebsiteEndpoint  return comp, nil } 

    Now we can implement the component itself. Components should inherit from Pulumi.ComponentResource, and should accept the required 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 implmentation

    class StaticPage : ComponentResource {  [Output("endpoint")]  public Output<string> endpoint { get; set; }   public StaticPage(string name, StaticPageArgs args, ComponentResourceOptions? opts = null)  : base("static-page-component:index:StaticPage", name, args, opts)  {  // Create a bucket  var bucket = new BucketV2($"{name}-bucket", new() { }, new() { Parent = this });   // Configure the bucket website  var bucketWebsite = new BucketWebsiteConfigurationV2($"{name}-website", new() {  Bucket = bucket.Id,  IndexDocument = new BucketWebsiteConfigurationV2IndexDocumentArgs { Suffix = "index.html" },  }, new() { Parent = bucket });   // Create a bucket object for the index document  var bucketObject = new BucketObject($"{name}-index-object", new BucketObjectArgs {  Bucket = bucket.Bucket,  Key = "index.html",  Content = args.IndexContent,  ContentType = "text/html",  }, new() { Parent = bucket });   // Create a public access block for the bucket  var publicAccessBlock = new BucketPublicAccessBlock($"{name}-public-access-block", new() {  Bucket = bucket.Id,  BlockPublicAcls = false,  }, new() { Parent = bucket });   // Set the access policy for the bucket so all objects are readable  var bucketPolicy = new BucketPolicy($"{name}-bucket-policy", new() {  Bucket = bucket.Id,  Policy = bucket.Bucket.Apply(this.AllowGetObjectPolicy),  }, new() { Parent = bucket, DependsOn = publicAccessBlock });   this.endpoint = bucketWebsite.WebsiteEndpoint;   // By registering the outputs on which the component depends, we ensure  // that the Pulumi CLI will wait for all the outputs to be created before  // considering the component itself to have been created.  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 required 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 implmentation

    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("static-page-component:index:StaticPage", name, null, opts);   // Create a bucket  var bucket = new BucketV2(  String.format("%s-bucket", name),  null,  CustomResourceOptions.builder()  .parent(this)  .build());   // Configure the bucket website  var bucketWebsite = new BucketWebsiteConfigurationV2(  String.format("%s-website", name),  BucketWebsiteConfigurationV2Args.builder()  .bucket(bucket.id())  .indexDocument(  BucketWebsiteConfigurationV2IndexDocumentArgs.builder()  .suffix("index.html")  .build())  .build(),  CustomResourceOptions.builder()  .parent(bucket)  .build());   // Create a bucket object for the index document  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(bucket)  .build());   // Create a public access block for the bucket  var publicAccessBlock = new BucketPublicAccessBlock(  String.format("%s-public-access-block", name),  BucketPublicAccessBlockArgs.builder()  .bucket(bucket.id())  .blockPublicAcls(false)  .build(),  CustomResourceOptions.builder()  .parent(bucket)  .build());   // Set the access policy for the bucket so all objects are readable  var bucketPolicy = new BucketPolicy(  String.format("%s-bucket-policy", name),  BucketPolicyArgs.builder()  .bucket(bucket.id())  .policy(bucket.bucket().applyValue(  bucketName -> this.allowGetObjectPolicy(bucketName)))  .build(),  CustomResourceOptions.builder()  .parent(bucket)  .dependsOn(publicAccessBlock)  .build());   this.endpoint = bucketWebsite.websiteEndpoint();   // By registering the outputs on which the component depends, we ensure  // that the Pulumi CLI will wait for all the outputs to be created before  // considering the component itself to have been created.  this.registerOutputs(Map.of(  "endpoint", bucketWebsite.websiteEndpoint()));  }   private String allowGetObjectPolicy(String bucketName) {  var policyDoc = new JsonObject();  var statementArray = new JsonArray();  var statement = new JsonObject();  var actionArray = new JsonArray();  var resourceArray = new JsonArray();   policyDoc.addProperty("Version", "2012-10-17");  policyDoc.add("Statement", statementArray);  statementArray.add(statement);  statement.addProperty("Effect", "Allow");  statement.addProperty("Principal", "*");  statement.add("Action", actionArray);  actionArray.add("s3:GetObject");  statement.add("Resource", resourceArray);  resourceArray.add(String.format("arn:aws:s3:::%s/*", bucketName));   return new Gson().toJson(policyDoc);  } } 

    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 implmentation

    runtime: yaml name: static-page-component components:  StaticPage:  inputs:  indexContent:  type: string  resources:  bucket:  type: aws:s3/bucketV2:BucketV2  properties: {}   bucketWebsite:  type: aws:s3/bucketWebsiteConfigurationV2:BucketWebsiteConfigurationV2  properties:  bucket: ${bucket.bucket}  indexDocument:  suffix: index.html  options:  parent: ${bucket}   bucketObject:  type: aws:s3/bucketObject:BucketObject  properties:  bucket: ${bucket.bucket}  key: index.html  content: ${indexContent}  contentType: text/html  options:  parent: ${bucket}   publicAccessBlock:  type: aws:s3/bucketPublicAccessBlock:BucketPublicAccessBlock  properties:  bucket: ${bucket.id}  blockPublicAcls: false  options:  parent: ${bucket}   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:  parent: ${bucket}  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

    export class StaticPage extends pulumi.ComponentResource {  // The URL of the static website  public readonly endpoint: pulumi.Output<string>; // ... } 

    We use a class property to store the output value. Note that it’s using pulumi.Output<string> instead of just a regular string. This allows the end-user to access this in an asynchronous manner when writing their Pulumi program.

    The Component constructor

    // ...  constructor(name: string, args: StaticPageArgs, opts?: pulumi.ComponentResourceOptions) {  super("static-page-component: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 required inputs for our component.
    • opts: This is an optional set of common resource configuration values. The ResourceOptions 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 implmentation detail. Otherwise, we pass the name, args, and opts values from our component constructor into the base constructor.

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

    Next we implement the BucketV2, BucketWebsiteConfigurationV2, BucketObject, BucketPublicAccessBlock and BucketPolicy sub-resources.

    // ...  // Create a bucket  const bucket = new aws.s3.BucketV2(`${name}-bucket`, {}, { parent: this });   // Configure the bucket website  const bucketWebsite = new aws.s3.BucketWebsiteConfigurationV2(`${name}-website`, {  bucket: bucket.bucket,  indexDocument: { suffix: "index.html" },  }, { parent: bucket });   // Create a bucket object for the index document.  const bucketObject = new aws.s3.BucketObject(`${name}-index-object`, {  bucket: bucket.bucket,  key: 'index.html',  content: args.indexContent,  contentType: 'text/html',  }, { parent: bucket });   // Create a public access block for the bucket  const publicAccessBlock = new aws.s3.BucketPublicAccessBlock(`${name}-public-access-block`, {  bucket: bucket.id,  blockPublicAcls: false,  }, { parent: bucket });   // Set the access policy for the bucket so all objects are readable  const bucketPolicy = new aws.s3.BucketPolicy(`${name}-bucket-policy`, {  bucket: bucket.id, // refer to the bucket created earlier  policy: bucket.bucket.apply(this.allowGetObjectPolicy),  }, { parent: bucket, dependsOn: publicAccessBlock }); // ... 
    The Bucket sub-resource

    The BucketV2 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 BucketV2 resource, via parent: this in the ResourceOptions class. This is an essential step to tie the sub-resources into the dependency graph.

    The BucketWebsiteConfigurationV2 and BucketObject sub-resources

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

    Notice that this time we pass the BucketV2 instance in as the parent to the ResourceOptions instances for these sub-resources, as opposed to this (e.g. the component). That creates a resource relationship graph like: StaticPage -> BucketV2 -> BucketObject. We do the same thing in the BucketPublicAccessBlock and BucketPolicy resource.

    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 BucketWebsiteConfigurationV2 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.

    // ...  this.endpoint = bucketWebsite.websiteEndpoint;   // By registering the outputs on which the component depends, we ensure  // that the Pulumi CLI will wait for all the outputs to be created before  // considering the component itself to have been created.  this.registerOutputs({  endpoint: bucketWebsite.websiteEndpoint,  }); // ... 

    Helper functions

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

    Example: staticpage.ts a helper function

    // ...  allowGetObjectPolicy(bucketName: string) {  return JSON.stringify({  Version: "2012-10-17",  Statement: [{  Effect: "Allow",  Principal: "*",  Action: [  "s3:GetObject"  ],  Resource: [  `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<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

    class StaticPage(pulumi.ComponentResource):  endpoint: pulumi.Output[str] # ... 

    We use a class property to store the output value. Note that it’s using pulumi.Output[str] instead of just a regular string. This allows the end-user to access this in an asynchronous manner when writing their Pulumi program.

    The Component constructor

    # ...  def __init__(self,  name: str,  args: StaticPageArgs,  opts: Optional[ResourceOptions] = None) -> None:   super().__init__('static-page-component: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 required inputs for our component.
    • opts: This is an optional set of common resource configuration values. The ResourceOptions 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 implmentation 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.

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

    Next we implement the BucketV2, BucketWebsiteConfigurationV2, BucketObject, BucketPublicAccessBlock and BucketPolicy sub-resources.

    # ...  # Create a bucket  bucket = s3.BucketV2(  f'{name}-bucket',  opts=ResourceOptions(parent=self))   # Configure the bucket website  bucket_website = s3.BucketWebsiteConfigurationV2(  f'{name}-website',  bucket=bucket.bucket,  index_document={"suffix": "index.html"},  opts=ResourceOptions(parent=bucket))   # Create a bucket object for the index document  s3.BucketObject(  f'{name}-index-object',  bucket=bucket.bucket,  key='index.html',  content=args.get("index_content"),  content_type='text/html',  opts=ResourceOptions(parent=bucket))   # Create a public access block for the bucket  bucket_public_access_block = s3.BucketPublicAccessBlock(  f'{name}-public-access-block',  bucket=bucket.id,  block_public_acls=False,  opts=ResourceOptions(parent=bucket))   # Set the access policy for the bucket so all objects are readable.  s3.BucketPolicy(  f'{name}-bucket-policy',  bucket=bucket.bucket,  policy=bucket.bucket.apply(_allow_getobject_policy),  opts=ResourceOptions(parent=bucket, depends_on=[bucket_public_access_block])) # ... 
    The Bucket sub-resource

    The BucketV2 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 BucketV2 resource, via parent=self in the ResourceOptions class. This is an essential step to tie the sub-resources into the dependency graph.

    The BucketWebsiteConfigurationV2 and BucketObject sub-resources

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

    Notice that this time we pass the BucketV2 instance in as the parent to the ResourceOptions instances for these sub-resources, as opposed to self (e.g. the component). That creates a resource relationship graph like: StaticPage -> BucketV2 -> BucketObject. We do the same thing in the BucketPublicAccessBlock and BucketPolicy resource.

    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 BucketWebsiteConfigurationV2 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   # By registering the outputs on which the component depends, we ensure  # that the Pulumi CLI will wait for all the outputs to be created before  # considering the component itself to have been created.  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: staticpage.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}/*', # policy refers to bucket name explicitly  ],  },  ],  }) # ... 

    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  // ... } 

    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

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

    We use a struct field to store the output value. Note that it’s using pulumi.StringOutput instead of just a regular string. This allows the end-user to access this in an asynchronous manner when writing their Pulumi program. 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("static-page-component: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 required inputs for our component.
    • opts: This is an optional set of common resource configuration values. The ResourceOptions 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 implmentation detail. Otherwise, we pass the name value, our component instance, as well as the opts values.

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

    Next we implement the BucketV2, BucketWebsiteConfigurationV2, BucketObject, BucketPublicAccessBlock and BucketPolicy sub-resources.

    // ... // Create a bucket bucket, err := s3.NewBucketV2(ctx, fmt.Sprintf("%s-bucket", name), &s3.BucketV2Args{}, pulumi.Parent(comp)) if err != nil { return nil, err }  // Configure bucket website bucketWebsite, err := s3.NewBucketWebsiteConfigurationV2(ctx, fmt.Sprintf("%s-website", name), &s3.BucketWebsiteConfigurationV2Args{ Bucket: bucket.Bucket, IndexDocument: s3.BucketWebsiteConfigurationV2IndexDocumentArgs{ Suffix: pulumi.String("index.html"), }, }, pulumi.Parent(bucket)) if err != nil { return nil, err }  // Create bucket object for index document _, 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(bucket)) if err != nil { return nil, err }  // Create public access block publicAccessBlock, err := s3.NewBucketPublicAccessBlock(ctx, fmt.Sprintf("%s-public-access-block", name), &s3.BucketPublicAccessBlockArgs{ Bucket: bucket.ID(), BlockPublicAcls: pulumi.Bool(false), }, pulumi.Parent(bucket)) if err != nil { return nil, err }  // Create bucket policy 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(bucket), pulumi.DependsOn([]pulumi.Resource{publicAccessBlock})) if err != nil { return nil, err } // ... 
    The Bucket sub-resource

    The BucketV2 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 BucketV2 resource. This is an essential step to tie the sub-resources into the dependency graph.

    The BucketWebsiteConfigurationV2 and BucketObject sub-resources

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

    Notice that this time we pass the BucketV2 instance as the parent of these sub-resources, via pulumi.Parent(bucket), as opposed to comp (e.g. the component). That creates a resource relationship graph like: StaticPage -> BucketV2 -> BucketObject. We do the same thing in the BucketPublicAccessBlock and BucketPolicy resource.

    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 BucketWebsiteConfigurationV2 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: staticpage.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 asychronous 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

    class StaticPage : ComponentResource {  // The URL of the static website  [Output("endpoint")]  public Output<string> endpoint { get; set; }  // ... } 

    We use a class property to store the output value. Note that it’s using Pulumi.Output<string> instead of just a regular string. This allows the end-user to access this in an asynchronous manner when writing their Pulumi program.

    The Component constructor

    // ...  public StaticPage(string name, StaticPageArgs args, ComponentResourceOptions? opts = null)  : base("static-page-component: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 required 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 implmentation detail. Otherwise, we pass the name, args, and opts values from our component constructor into the base constructor.

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

    Next we implement the BucketV2, BucketWebsiteConfigurationV2, BucketObject, BucketPublicAccessBlock and BucketPolicy sub-resources.

    // ...  // Create a bucket  var bucket = new BucketV2($"{name}-bucket", new() { }, new() { Parent = this });   // Configure the bucket website  var bucketWebsite = new BucketWebsiteConfigurationV2($"{name}-website", new() {  Bucket = bucket.Id,  IndexDocument = new BucketWebsiteConfigurationV2IndexDocumentArgs { Suffix = "index.html" },  }, new() { Parent = bucket });   // Create a bucket object for the index document  var bucketObject = new BucketObject($"{name}-index-object", new BucketObjectArgs {  Bucket = bucket.Bucket,  Key = "index.html",  Content = args.IndexContent,  ContentType = "text/html",  }, new() { Parent = bucket });   // Create a public access block for the bucket  var publicAccessBlock = new BucketPublicAccessBlock($"{name}-public-access-block", new() {  Bucket = bucket.Id,  BlockPublicAcls = false,  }, new() { Parent = bucket });   // Set the access policy for the bucket so all objects are readable  var bucketPolicy = new BucketPolicy($"{name}-bucket-policy", new() {  Bucket = bucket.Id,  Policy = bucket.Bucket.Apply(this.AllowGetObjectPolicy),  }, new() { Parent = bucket, DependsOn = publicAccessBlock }); // ... 
    The Bucket sub-resource

    The BucketV2 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 BucketV2 resource, via Parent = this in the opts object. This is an essential step to tie the sub-resources into the dependency graph.

    The BucketWebsiteConfigurationV2 and BucketObject sub-resources

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

    Notice that this time we pass the BucketV2 instance in as the Parent in the opts for these sub-resources, as opposed to this (e.g. the component). That creates a resource relationship graph like: StaticPage -> BucketV2 -> BucketObject. We do the same thing in the BucketPublicAccessBlock and BucketPolicy resource.

    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 BucketWebsiteConfigurationV2 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;   // By registering the outputs on which the component depends, we ensure  // that the Pulumi CLI will wait for all the outputs to be created before  // considering the component itself to have been created.  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

    class StaticPage extends ComponentResource {  // The URL of the static website  @Export(name = "endpoint", refs = { String.class }, tree = "[0]")  public final Output<String> endpoint;  // ... } 

    We use a class property to store the output value. Note that it’s using com.pulumi.core.Output<String> instead of just a regular string. This allows the end-user to access this in an asynchronous manner when writing their Pulumi program.

    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("static-page-component: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 required 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 implmentation detail. Otherwise, we pass the name, args, and opts values from our component constructor into the base constructor.

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

    Next we implement the BucketV2, BucketWebsiteConfigurationV2, BucketObject, BucketPublicAccessBlock and BucketPolicy sub-resources.

    // ...  // Create a bucket  var bucket = new BucketV2(  String.format("%s-bucket", name),  null,  CustomResourceOptions.builder()  .parent(this)  .build());   // Configure the bucket website  var bucketWebsite = new BucketWebsiteConfigurationV2(  String.format("%s-website", name),  BucketWebsiteConfigurationV2Args.builder()  .bucket(bucket.id())  .indexDocument(  BucketWebsiteConfigurationV2IndexDocumentArgs.builder()  .suffix("index.html")  .build())  .build(),  CustomResourceOptions.builder()  .parent(bucket)  .build());   // Create a bucket object for the index document  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(bucket)  .build());   // Create a public access block for the bucket  var publicAccessBlock = new BucketPublicAccessBlock(  String.format("%s-public-access-block", name),  BucketPublicAccessBlockArgs.builder()  .bucket(bucket.id())  .blockPublicAcls(false)  .build(),  CustomResourceOptions.builder()  .parent(bucket)  .build());   // Set the access policy for the bucket so all objects are readable  var bucketPolicy = new BucketPolicy(  String.format("%s-bucket-policy", name),  BucketPolicyArgs.builder()  .bucket(bucket.id())  .policy(bucket.bucket().applyValue(  bucketName -> this.allowGetObjectPolicy(bucketName)))  .build(),  CustomResourceOptions.builder()  .parent(bucket)  .dependsOn(publicAccessBlock)  .build()); // ... 
    The Bucket sub-resource

    The BucketV2 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 BucketV2 resource, via parent(this) in the opts object. This is an essential step to tie the sub-resources into the dependency graph.

    The BucketWebsiteConfigurationV2 and BucketObject sub-resources

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

    Notice that this time we pass the BucketV2 instance in as the parent in the opts for these sub-resources, as opposed to this (e.g. the component). That creates a resource relationship graph like: StaticPage -> BucketV2 -> BucketObject. We do the same thing in the BucketPublicAccessBlock and BucketPolicy resource.

    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.applyValue(...) 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 applyValue 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 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 BucketWebsiteConfigurationV2 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();   // By registering the outputs on which the component depends, we ensure  // that the Pulumi CLI will wait for all the outputs to be created before  // considering the component itself to have been created.  this.registerOutputs(Map.of(  "endpoint", bucketWebsite.websiteEndpoint())); // ... 

    Helper functions

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

    Example: StaticPage.java a helper function

    // ...  private String allowGetObjectPolicy(String bucketName) {  var policyDoc = new JsonObject();  var statementArray = new JsonArray();  var statement = new JsonObject();  var actionArray = new JsonArray();  var resourceArray = new JsonArray();   policyDoc.addProperty("Version", "2012-10-17");  policyDoc.add("Statement", statementArray);  statementArray.add(statement);  statement.addProperty("Effect", "Allow");  statement.addProperty("Principal", "*");  statement.add("Action", actionArray);  actionArray.add("s3:GetObject");  statement.add("Resource", resourceArray);  resourceArray.add(String.format("arn:aws:s3:::%s/*", bucketName));   return new Gson().toJson(policyDoc);  } // ... 

    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 applyValue(...). That means that the bucketName, which is normally a com.pulumi.core.Output<String> value, can be materialized as a normal Java 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. We use JsonObject and JsonArray to construct the necessary JSON object then pass those to the Gson.toJson(...) function which returns it as a JSON formatted string.

    Naming the component

    runtime: yaml name: static-page-component 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:

    resources:  my-static-page:  type: static-page-component:StaticPage #... 

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

    Output properties

    runtime: yaml name: static-page-component components:  StaticPage:  # ...  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 BucketV2, BucketWebsiteConfigurationV2, 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/bucketV2:BucketV2  properties: {}   bucketWebsite:  type: aws:s3/bucketWebsiteConfigurationV2:BucketWebsiteConfigurationV2  properties:  bucket: ${bucket.bucket}  indexDocument:  suffix: index.html  options:  parent: ${bucket}   bucketObject:  type: aws:s3/bucketObject:BucketObject  properties:  bucket: ${bucket.bucket}  key: index.html  content: ${indexContent}  contentType: text/html  options:  parent: ${bucket}   publicAccessBlock:  type: aws:s3/bucketPublicAccessBlock:BucketPublicAccessBlock  properties:  bucket: ${bucket.id}  blockPublicAcls: false  options:  parent: ${bucket}   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:  parent: ${bucket}  dependsOn:  - ${publicAccessBlock} # ... 
    The Bucket sub-resource

    The BucketV2 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 BucketV2 resource, so there’s no need to define that on this object.

    The BucketWebsiteConfigurationV2 and BucketObject sub-resources

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

    Notice that this time we pass the BucketV2 instance in as the parent in the options for these sub-resources. This is an essential step to tie the sub-resources into the dependency graph. That creates a resource relationship graph like: StaticPage -> BucketV2 -> BucketObject. We do the same thing in the BucketPublicAccessBlock and BucketPolicy resource.

    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 ${indexDocument} 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 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 implmentation 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 BucketWebsiteConfigurationV2 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: StaticPage.java a helper function

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

    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} theis 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 Pulumi program. For fun, try using it in a different languages than you wrote it in, like YAML!

    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 JavaScript

    name: use-static-page-component description: A minimal JavaScript Pulumi program that uses the custom Static Page component runtime:  name: nodejs  options:  typescript: false 

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

    name: use-static-page-component description: A minimal TypeScript Pulumi program that uses the custom Static Page component runtime:  name: nodejs 

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

    name: use-static-page-component description: A minimal Python Pulumi program that uses the custom Static Page component runtime:  name: python  options:  toolchain: pip  virtualenv: venv 

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

    name: use-static-page-component description: A minimal Go Pulumi program that uses the custom Static Page component runtime:  name: go 

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

    name: use-static-page-component description: A minimal C# Pulumi program that uses the custom Static Page component runtime:  name: dotnet 

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

    name: use-static-page-component description: A minimal Java Pulumi program that uses the custom Static Page component runtime:  name: java 

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

    name: use-static-page-component description: A minimal YAML Pulumi program that uses the custom Static Page component runtime:  name: yaml 

    Generate the SDK

    In order to use our Pulumi component from source, we will need to generate a language-specific SDK, which will allow end users to use it in a Pulumi program. From the root directory of your use-static-page-component Pulumi project, run the following command:

    pulumi package add ../static-page-component 

    This will create a new subdirectory called sdks/, and in it, there will be a directory for your language SDK. One of: dotnet, go, java, nodejs, and python.

    If you’re authoring your Pulumi project in YAML, it is not necessary to generate a SDK! Skip ahead to the next step.

    Add the component reference in Pulumi.YAML

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

    packages:  static-page-component: ../static-page-component 

    Add the NodeJS project file

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

    Example: package.json

    {  "name": "use-static-page-component",  "dependencies": {  "@pulumi/pulumi": "3.157.0",  "@pulumi/static-page-component": "file:sdks/static-page-component"  } } 

    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 langauge 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 implmentation and the end users of the component.

    Install dependencies

    Next, install the pulumi dependencies:

    pulumi install 

    Create the Pulumi program

    Example: index.js that uses the Static Page component

    "use strict";  const pulumi = require("@pulumi/pulumi"); const staticpagecomponent = require("@pulumi/static-page-component");  const pageHTML = "<h1>I love Pulumi!</h1>";  const page = new staticpagecomponent.StaticPage("my-static-page", {  indexContent: pageHTML })  // Export the URL to the index page exports.websiteURL = pulumi.interpolate `http://${page.endpoint}`; 

    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": "use-static-page-component",  "devDependencies": {  "@types/node": "22.13.5"  },  "dependencies": {  "@pulumi/pulumi": "3.157.0",  "@pulumi/static-page-component": "file:sdks/static-page-component"  } } 

    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 langauge 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 implmentation 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": {  "outDir": "bin",  "target": "es2016",  "module": "commonjs",  "moduleResolution": "node",  "sourceMap": true,  "experimentalDecorators": true,  "pretty": true,  "noFallthroughCasesInSwitch": true,  "noImplicitAny": true,  "noImplicitReturns": true,  "forceConsistentCasingInFileNames": true,  "strictNullChecks": true  },  "files": [  "index.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 "@pulumi/static-page-component";  const pageHTML = "<h1>I love Pulumi!</h1>";  const page = new StaticPage('my-static-page', {  indexContent: pageHTML });  // Export the URL to the index page export const websiteURL = pulumi.interpolate`http://${page.endpoint}`; 

    Add the package dependencies

    Now lets create our requirements.txt. We’ll need the standard pulumi SDK and our custom component. To use that, just add the path to the generated Python SDK:

    sdk/python pulumi>=3.153.0,<4.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 langauge 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 implmentation 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 $ pip install -r requirements.txt 

    Create the Pulumi program

    Example: __main__.py that uses the Static Page component

    import pulumi from pulumi_static_page_component import StaticPage  page_html = "<h1>I love Pulumi!</h1>" page = StaticPage('my-static-page', index_content=page_html)  # Export the URL to the index page website_url = page.endpoint.apply(lambda v: f"http://{v}") pulumi.export('websiteURL', website_url) 

    Add the Go project file

    Now lets create our go.mod. We’ll need the standard pulumi SDK and our custom component. To use the generated Go SDK, we’ll use a replace directive to map the package name to the SDK source directory.

    Example: go.mod for our Pulumi project

    module use-static-page-component  go 1.20  require github.com/pulumi/pulumi/sdk/v3 v3.157.0  replace example.com/pulumi-static-page-component/sdk/go/static-page-component => ./sdks/static-page-component/staticpagecomponent 

    The pulumi package add command may have added a replace directive into your go.mod already. If so, remove it and replace with the above example. There’s a known bug w/ Go SDK generation which causes this.

    The same bug also causes the Go SDK to be generated without its necessary go.mod. Let’s create that file in the sdks/static-page-component/staticpagecomponent directory with the following contents:

    Example: go.mod patch for our generated SDK

    module example.com/pulumi-static-page-component/sdk/go/static-page-component 

    go 1.22

    toolchain go1.23.5

    require github.com/pulumi/pulumi/sdk/v3 v3.147.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 langauge 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 implmentation and the end users of the component.

    Install dependencies

    Next, install the pulumi dependencies:

    pulumi install 

    Create the Pulumi program

    Example: main.go that uses the Static Page component

    package main  import ( staticpagecomponent "example.com/pulumi-static-page-component/sdk/go/static-page-component" "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 }) } 

    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>  <AssemblyName>use-static-page-component</AssemblyName>  <DefaultItemExcludes>$(DefaultItemExcludes);sdks/**/*.cs</DefaultItemExcludes>  </PropertyGroup>   <ItemGroup>  <PackageReference Include="Pulumi" Version="3.*" />  </ItemGroup>   <ItemGroup>  <ProjectReference Include="sdks\static-page-component\Pulumi.StaticPageComponent.csproj" />  </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 langauge 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 implmentation 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 Pulumi; using Pulumi.StaticPageComponent; using System.Collections.Generic;  return await Deployment.RunAsync(() => {  var pageHTML = "<h1>I love Pulumi!</h1>";   var page = new StaticPage("my-static-page", new() {  IndexContent = pageHTML  });   // Export the URL of the page  return new Dictionary<string, object?>  {  ["websiteURL"] = Output.Format($"http://{page.Endpoint}")  }; }); 

    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>use-static-page-component</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.3,2.0)</version>  </dependency>  <!-- Add the SDK's dependencies, based on the output from the `pulumi package add` command -->  <dependency>  <groupId>com.google.code.findbugs</groupId>  <artifactId>jsr305</artifactId>  <version>3.0.2</version>  </dependency>  <dependency>  <groupId>com.google.code.gson</groupId>  <artifactId>gson</artifactId>  <version>2.8.9</version>  </dependency>  </dependencies>   <build>  <!-- Change the root directory that Maven uses to look for sources -->  <sourceDirectory>.</sourceDirectory>  <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>  <plugin>  <groupId>org.apache.maven.plugins</groupId>  <artifactId>maven-compiler-plugin</artifactId>  <version>3.8.1</version>  <configuration>  <source>11</source>  <target>11</target>   <!-- Add path glob specs for our two source locations -->  <includes>  <include>src/main/**/*.java</include>  <include>sdks/static-page-component/src/main/**/*.java</include>  </includes>  </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 langauge 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 implmentation 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.staticpagecomponent.StaticPage; import com.pulumi.staticpagecomponent.StaticPageArgs;  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-bucket", StaticPageArgs.builder()  .indexContent(pageHTML).build()  );   ctx.export("websiteURL", page.endpoint().applyValue(v->String.format("http://%s", v)));  });  } } 

    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: static-page-component: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)  + └─ static-page-component:index:StaticPage my-static-page created (5s)  + └─ aws:s3:BucketV2 my-static-page-bucket created (2s)  + ├─ aws:s3:BucketPublicAccessBlock my-static-page-public-access-block created (0.84s)  + ├─ aws:s3:BucketWebsiteConfigurationV2 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.

    Sharing and Reuse

    In the above examples, the component was referenced from a nearby directory, local to the machine. In order to share a component, it needs to be accessed outside of your local machine. There are two main ways to do that; sharing via a Git repo, or publishing as a Pulumi Package.

    Sharing via Git

    Storing a component in a Git repository allows for version control, collaboration, and easier integration into multiple projects. Developers can add the component to their Pulumi projects using the command:

    $ pulumi package add <repo_url>@<release-version> 

    The only steps necessary to enable this are to push your component project to a git repo, and create a release tag for the versioning. Pulumi supports referencing both GitHub and GitLab releases. You can also target a standard internally hosted git service, just by providing the repo URL without the <release-version> portion.

    Pulumi will automatically generate the needed language-specific end user SDK for your project. For example, if the Pulumi project was written in Python, the pulumi package add command would detect this and generate the Python SDK on-the-fly, as well as adding the dependency to your requirements.txt and running pip install -r requirements.txt for you. The output will also give you an example of the correct import statement to use the component.

    $ pulumi package add https://github.com/pulumi/staticpagecomponent@v0.1.0 Downloading provider: github.com_pulumi_staticpagecomponent.git Successfully generated a Python SDK for the staticpagecomponent package at /example/use-static-page-component/sdks/staticpagecomponent [...] You can then import the SDK in your Python code with: import pulumi_static_page_component as static_page_component 
    Pulumi also supports private repos in GitHub and GitLab. Pulumi will read standard environment variables like GITHUB_TOKEN and GITLAB_TOKEN if available in order to authenticate access to a private repo during pulumi package add.

    Generating Local SDKs with pulumi install

    Once you’ve added an entry to the packages section of your Pulumi.yaml file, you can run pulumi install to generate a local SDK in your project. This command will process all packages listed in your Pulumi.yaml and create the necessary SDK files. Check in these files if you want fully reproducible builds, or add them to .gitignore if you prefer to regenerate them on each checkout. When using .gitignore, team members will need to run pulumi install after checkout to regenerate the SDK.

    Sharing via Pulumi Package

    Publishing a component as a Pulumi package makes it easier to distribute and integrate into Pulumi workflows. This method enables community contributions and ensures that infrastructure components remain modular and maintainable. By packaging the component, it becomes easier to reuse across teams and projects, improving consistency and efficiency in infrastructure management. It also makes your component available for use within Pulumi Cloud Deployments.

    Learn more in the Publishing packages guide.

      IDP Builder Course. Register Now.