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