Using Pulumi IaC to deploy NextJs static website with AWS S3 and EC2
This is a submission for the Pulumi Deploy and Document Challenge: Fast Static Website Deployment Reference: https://www.pulumi.com/templates/static-website/aws/ What I Built Static Website Deployment Next JS Framework publish to s3 and ec2using Pulumi IaC Live Demo Link NA Before After Project Repo NA My Journey I had face issue related to my hardening envt and restriction based sandbox due to security reasons normal production replica:- 1.aws linux 2 flavor with no option to migrate to latest(legacy servers) 2.not using latest nextjs,ts,npm,node version(legacy codebase) 3.unable to create new ec2 4.having s3 public access blocked 5.IAM limited scope and putpolicy for s3 not given Using Pulumi install pulumi verify pulumi verify pulumi specific version configure Pulumi to use a local backend for state before launching pulumi check node and npm version Amazon Linux EC2 /RHEL Based curl -fsSL https://rpm.nodesource.com/setup_16.x | sudo bash - sudo yum install -y nodejs now check node -version (i came across this while troubleshooting) create new directory only in empty directory pulumi project can be created pulumi new aws-typescript now enter values one by one project name: webserver project description: Yashvi Deploying on AWS using Pulumi IaC stack name: Press enter to accept the default dev passphrase to protect config/secrets: Enter Secret123 (you will be prompted to enter this two times) aws:region: us-west-2 Pulumi installed dependencies and Now project ready Now Pulumi to use the IAM role attached to your EC2 instance for AWS authentication pulumi config set aws:skipMetadataApiCheck false You can Refer to the [AWS Classic Pulumi package documentation ] Now to avoid repeated passphrase prompts in Pulumi, set a Bash environment variable: Now to retrieve the public IP address of EC2 instance, execute curl https://checkip.amazonaws.com Now, Open a new browser tab and paste the following URL into the address bar, replacing with the IP address 54.218.250.5 http://54.218.250.5:3000 Trying to add website content and IaC for creating s3 Now by default As we intent Static Website Deployment as per our project submission so i can have nextjs demo app live Replace this part in index.ts import * as pulumi from "@pulumi/pulumi"; import * as aws from "@pulumi/aws"; import * as awsx from "@pulumi/awsx"; // Create an AWS resource (S3 Bucket) const bucket = new aws.s3.BucketV2("my-bucket"); // Export the name of the bucket export const bucketName = bucket.id; with import * as pulumi from "@pulumi/pulumi"; import * as aws from "@pulumi/aws"; import * as fs from "fs"; import * as path from "path"; // 1. Build Next.js app (generates 'out' folder) const nextjsBuildDir = path.join(process.cwd(), "../nextjs-app"); if (!fs.existsSync(nextjsBuildDir)) { throw new Error("Next.js app not found at ../nextjs-app"); } // Simulate build process (run this manually first) // pulumi.log.info("Run 'npm run build' in your Next.js app first!"); // 2. Create S3 bucket with website hosting const siteBucket = new aws.s3.Bucket("nextjs-site", { website: { indexDocument: "index.html", errorDocument: "404.html", }, acl: "public-read", }); // 3. Upload static export files from Next.js const outDir = path.join(nextjsBuildDir, "out"); if (fs.existsSync(outDir)) { const uploadFile = (filePath: string) => { const relativePath = path.relative(outDir, filePath); new aws.s3.BucketObject(relativePath, { bucket: siteBucket.id, source: new pulumi.asset.FileAsset(filePath), contentType: getContentType(filePath), acl: "public-read", }); }; // Recursively upload files const walkDir = (dir: string) => { fs.readdirSync(dir).forEach(file => { const fullPath = path.join(dir, file); if (fs.statSync(fullPath).isDirectory()) { walkDir(fullPath); } else { uploadFile(fullPath); } }); }; walkDir(outDir); } else { // Fallback demo content if Next.js app isn't built new aws.s3.BucketObject("index.html", { bucket: siteBucket.id, content: "Next.js Static Site (Demo)Run 'npm run build' in your Next.js app!", contentType: "text/html", acl: "public-read", }); } // Helper to detect MIME types function getContentType(filePath: string): string { const ext = path.extname(filePath); switch (ext) { case ".html": return "text/html"; case ".css": return "text/css"; case ".js": return "application/javascript"; case ".json": return "application/json"; case ".png": return "image/png"; case ".jpg": case ".jpeg": return "image/jpeg"; default: return "application/octet-stream"; } } // 4. Export URLs export c

This is a submission for the Pulumi Deploy and Document Challenge: Fast Static Website Deployment
Reference: https://www.pulumi.com/templates/static-website/aws/
What I Built
Static Website Deployment Next JS Framework publish to s3 and ec2using Pulumi IaC
Live Demo Link
NA
After
Project Repo
NA
My Journey
I had face issue related to my hardening envt and restriction based sandbox due to security reasons normal production replica:-
1.aws linux 2 flavor with no option to migrate to latest(legacy servers)
2.not using latest nextjs,ts,npm,node version(legacy codebase)
3.unable to create new ec2
4.having s3 public access blocked
5.IAM limited scope and putpolicy for s3 not given
Using Pulumi
install pulumi
verify pulumi
verify pulumi specific version
configure Pulumi to use a local backend for state
before launching pulumi check node and npm version
Amazon Linux EC2 /RHEL Based
curl -fsSL https://rpm.nodesource.com/setup_16.x | sudo bash -
sudo yum install -y nodejs
now check node -version (i came across this while troubleshooting)
create new directory
only in empty directory pulumi project can be created
pulumi new aws-typescript
now enter values one by one
project name: webserver
project description: Yashvi Deploying on AWS using Pulumi IaC
stack name: Press enter to accept the default dev
passphrase to protect config/secrets: Enter Secret123 (you will be prompted to enter this two times)
aws:region: us-west-2
Pulumi installed dependencies and Now project ready
Now Pulumi to use the IAM role attached to your EC2 instance for AWS authentication
pulumi config set aws:skipMetadataApiCheck false
You can Refer to the [AWS Classic Pulumi package documentation ]
Now to avoid repeated passphrase prompts in Pulumi, set a Bash environment variable:
Now to retrieve the public IP address of EC2 instance, execute
curl https://checkip.amazonaws.com
Now, Open a new browser tab and paste the following URL into the address bar, replacing with the IP address 54.218.250.5
Trying to add website content and IaC for creating s3
Now by default
As we intent Static Website Deployment as per our project submission so i can have nextjs demo app live
Replace this part in index.ts
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
import * as awsx from "@pulumi/awsx";
// Create an AWS resource (S3 Bucket)
const bucket = new aws.s3.BucketV2("my-bucket");
// Export the name of the bucket
export const bucketName = bucket.id;
with
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
import * as fs from "fs";
import * as path from "path";
// 1. Build Next.js app (generates 'out' folder)
const nextjsBuildDir = path.join(process.cwd(), "../nextjs-app");
if (!fs.existsSync(nextjsBuildDir)) {
throw new Error("Next.js app not found at ../nextjs-app");
}
// Simulate build process (run this manually first)
// pulumi.log.info("Run 'npm run build' in your Next.js app first!");
// 2. Create S3 bucket with website hosting
const siteBucket = new aws.s3.Bucket("nextjs-site", {
website: {
indexDocument: "index.html",
errorDocument: "404.html",
},
acl: "public-read",
});
// 3. Upload static export files from Next.js
const outDir = path.join(nextjsBuildDir, "out");
if (fs.existsSync(outDir)) {
const uploadFile = (filePath: string) => {
const relativePath = path.relative(outDir, filePath);
new aws.s3.BucketObject(relativePath, {
bucket: siteBucket.id,
source: new pulumi.asset.FileAsset(filePath),
contentType: getContentType(filePath),
acl: "public-read",
});
};
// Recursively upload files
const walkDir = (dir: string) => {
fs.readdirSync(dir).forEach(file => {
const fullPath = path.join(dir, file);
if (fs.statSync(fullPath).isDirectory()) {
walkDir(fullPath);
} else {
uploadFile(fullPath);
}
});
};
walkDir(outDir);
} else {
// Fallback demo content if Next.js app isn't built
new aws.s3.BucketObject("index.html", {
bucket: siteBucket.id,
content: "Next.js Static Site (Demo)
Run 'npm run build' in your Next.js app!",
contentType: "text/html",
acl: "public-read",
});
}
// Helper to detect MIME types
function getContentType(filePath: string): string {
const ext = path.extname(filePath);
switch (ext) {
case ".html": return "text/html";
case ".css": return "text/css";
case ".js": return "application/javascript";
case ".json": return "application/json";
case ".png": return "image/png";
case ".jpg": case ".jpeg": return "image/jpeg";
default: return "application/octet-stream";
}
}
// 4. Export URLs
export const websiteUrl = siteBucket.websiteEndpoint;
Structure (making nexjs app folder sibling one)
Pulumi code in webserver/index.ts should correctly reference the sibling ../nextjs-app folder is must
I tried for Pulumi IaC for S3 and for EC2 (legacy system and troubleshooting :) )
pulumi up
keep doing yes by y
Now bucket website URL is final output of website deployment