Reusable Static Sites with Pulumi Component Resources on AWS
Wrap an S3 bucket and CloudFront distribution into a Pulumi component resource so you can deploy static blogs repeatedly without copying infrastructure code.
16 Jun 2026, 04:46 UTC

The problem: the same bucket and distribution, again and again
If your team hosts more than one static site on AWS, you probably have the same CloudFront and S3 boilerplate in every stack. One person copies the bucket configuration, another adjusts the origin path, and over time the environments drift. A missing logging block or a mismatched error response in one stack becomes an incident you only notice after a deploy.
The useful takeaway: define the combination once as a Pulumi component resource, then instantiate it with different inputs for each site or environment. The stack state tracks the component as one logical unit while it manages the bucket, distribution, and access controls underneath.
What a component resource is
A component resource is a Pulumi resource that groups child resources without representing a single AWS API object itself. You register it with a type token such as custom:blog:StaticBlog. Pulumi records the component in state, and the child resources—buckets, distributions, policies—are created beneath it. Changing an input updates the children through the same component.
A stack is an isolated deployment environment, such as dev or production. State is Pulumi's record of what has been deployed.
Worked example: a StaticBlog component
The following TypeScript example defines a component that provisions an S3 bucket configured for static hosting and a CloudFront distribution in front of it. It is written for the Pulumi AWS provider and Node.js. Treat it as a structural template: adjust the provider version, origin access control, and bucket policy to match your security requirements before running it.
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
interface StaticBlogArgs {
siteName: string;
indexDocument?: string;
}
class StaticBlog extends pulumi.ComponentResource {
public readonly bucket: aws.s3.BucketV2;
public readonly distribution: aws.cloudfront.Distribution;
public readonly websiteUrl: pulumi.Output;
constructor(name: string, args: StaticBlogArgs, opts?: pulumi.ComponentResourceOptions) {
super("custom:blog:StaticBlog", name, args, opts);
const bucket = new aws.s3.BucketV2(`${name}-bucket`, {
bucket: args.siteName,
website: {
indexDocument: args.indexDocument ?? "index.html",
},
}, { parent: this });
// Public read policy shown for illustration only. Production sites should
// restrict access through CloudFront origin access control and block public
// bucket access.
const bucketPolicy = new aws.s3.BucketPolicy(`${name}-policy`, {
bucket: bucket.id,
policy: pulumi.output({
Version: "2012-10-17",
Statement: [{
Effect: "Allow",
Principal: "*",
Action: ["s3:GetObject"],
Resource: [bucket.arn.apply(arn => `${arn}/*`)],
}],
}).apply(JSON.stringify),
}, { parent: this });
const distribution = new aws.cloudfront.Distribution(`${name}-distribution`, {
origins: [{
domainName: bucket.websiteEndpoint,
originId: "s3-origin",
customOriginConfig: {
originProtocolPolicy: "http-only",
httpPort: 80,
httpsPort: 443,
},
}],
enabled: true,
defaultRootObject: args.indexDocument ?? "index.html",
defaultCacheBehavior: {
targetOriginId: "s3-origin",
viewerProtocolPolicy: "redirect-to-https",
},
}, { parent: this, dependsOn: [bucketPolicy] });
this.bucket = bucket;
this.distribution = distribution;
this.websiteUrl = distribution.domainName.apply(domain => `https://${domain}`);
this.registerOutputs({ websiteUrl: this.websiteUrl });
}
}
// Usage in a stack entry point:
// const blog = new StaticBlog("my-blog", { siteName: "my-blog.example.com" });
// export const websiteUrl = blog.websiteUrl;
Each parent: this attachment tells Pulumi that the child belongs to the component. The component exposes websiteUrl so other stack code can reference the distribution domain without knowing how it was built.
Run it and verify the result
- Open a terminal in your Pulumi project directory.
- Ensure AWS credentials are available to the Pulumi process, typically through environment variables or ~/.aws/credentials. The IAM principal must be allowed to create S3 buckets, bucket policies, and CloudFront distributions.
- Create or select a stack: pulumi stack init <stack-name>.
- Run pulumi up. Review the planned changes, then confirm. Expect Pulumi to print the component outputs, including the CloudFront domain name.
- After the update completes, check the distribution directly: aws cloudfront get-distribution --id <distribution-id>, replacing <distribution-id> with the ID from Pulumi's output or the CloudFront console.
- Test the endpoint: curl -I https://<domain>/. Look for a 200 OK or 301 response from CloudFront.
Risk: pulumi up creates and modifies live AWS resources. It can incur charges and expose data if the bucket policy is misconfigured. Review the plan before confirming.
Trade-offs and limitations
- Public read access: The example allows public s3:GetObject so CloudFront can reach the origin. For a private blog, replace this with origin access control and signed URLs or an identity provider.
- IAM complexity: The Pulumi role or user needs permissions across S3 and CloudFront. Misconfigured policies produce deployment failures before any site traffic is affected.
- Cost: CloudFront requests and data transfer are billed separately from the S3 storage. High traffic or large media files can raise the bill unexpectedly.
- Propagation delay: CloudFront distribution updates can take several minutes to propagate globally, so a successful pulumi up does not mean the new configuration is visible everywhere immediately.
Closing
Start with one component for your most repeated site pattern. Parameterize the domain name, index document, and any custom error responses, then instantiate it per stack. Verify with pulumi up and a direct CloudFront request before pointing production DNS at the distribution.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.