PulumiAWS
AWS S3 + CloudFront (Pulumi TypeScript)
Pulumi TypeScript program deploying an S3 static site with CloudFront CDN and origin access control.
pulumitypescripts3cloudfrontstatic-site
Prerequisites
- •
Pulumi CLI installed: https://www.pulumi.com/docs/install/ - •
AWS credentials configured - •
Node.js >= 18 - •
Pulumi account (or use local backend)
Template Code
// ─────────────────────────────────────────────────────────────────────────────
// Pulumi TypeScript — AWS S3 Static Site + CloudFront CDN
// File: index.ts
// ─────────────────────────────────────────────────────────────────────────────
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const config = new pulumi.Config();
const environment = config.get("environment") ?? "production";
const domainName = config.get("domainName"); // Optional custom domain
// ── S3 Bucket ────────────────────────────────────────────────────────────────
const siteBucket = new aws.s3.BucketV2("site-bucket", {
bucket: `${environment}-static-site-${pulumi.getStack()}`,
tags: { Environment: environment },
});
// Block all public access
new aws.s3.BucketPublicAccessBlock("site-public-access-block", {
bucket: siteBucket.id,
blockPublicAcls: true,
blockPublicPolicy: true,
ignorePublicAcls: true,
restrictPublicBuckets: true,
});
// Enable versioning
new aws.s3.BucketVersioningV2("site-versioning", {
bucket: siteBucket.id,
versioningConfiguration: { status: "Enabled" },
});
// ── CloudFront Origin Access Control ─────────────────────────────────────────
const oac = new aws.cloudfront.OriginAccessControl("site-oac", {
originAccessControlOriginType: "s3",
signingBehavior: "always",
signingProtocol: "sigv4",
});
// ── CloudFront Distribution ───────────────────────────────────────────────────
const distribution = new aws.cloudfront.Distribution("site-distribution", {
enabled: true,
isIpv6Enabled: true,
defaultRootObject: "index.html",
aliases: domainName ? [domainName] : undefined,
priceClass: "PriceClass_100",
origins: [{
domainName: siteBucket.bucketRegionalDomainName,
originId: siteBucket.id.apply(id => `S3-${id}`),
originAccessControlId: oac.id,
}],
defaultCacheBehavior: {
allowedMethods: ["GET", "HEAD"],
cachedMethods: ["GET", "HEAD"],
targetOriginId: siteBucket.id.apply(id => `S3-${id}`),
viewerProtocolPolicy: "redirect-to-https",
compress: true,
forwardedValues: {
queryString: false,
cookies: { forward: "none" },
},
minTtl: 0,
defaultTtl: 86400,
maxTtl: 604800,
},
customErrorResponses: [{
errorCode: 404,
responseCode: 200,
responsePagePath: "/index.html", // SPA fallback
}],
restrictions: {
geoRestriction: { restrictionType: "none" },
},
viewerCertificate: domainName
? {
// Provide your ACM certificate ARN when using a custom domain
acmCertificateArn: config.require("certificateArn"),
sslSupportMethod: "sni-only",
minimumProtocolVersion: "TLSv1.2_2021",
}
: { cloudfrontDefaultCertificate: true },
tags: { Environment: environment },
});
// ── S3 Bucket Policy — allow CloudFront only ──────────────────────────────────
const bucketPolicy = new aws.s3.BucketPolicy("site-bucket-policy", {
bucket: siteBucket.id,
policy: pulumi.all([siteBucket.arn, distribution.arn]).apply(
([bucketArn, distArn]) => JSON.stringify({
Version: "2012-10-17",
Statement: [{
Effect: "Allow",
Principal: { Service: "cloudfront.amazonaws.com" },
Action: "s3:GetObject",
Resource: `${bucketArn}/*`,
Condition: {
StringEquals: { "AWS:SourceArn": distArn },
},
}],
})
),
});
// ── Exports ───────────────────────────────────────────────────────────────────
export const bucketName = siteBucket.bucket;
export const cloudfrontUrl = pulumi.interpolate`https://${distribution.domainName}`;
export const distributionId = distribution.id;
Usage
npm install pulumi up