docs
reference
services
tessl install tessl/maven-com-pulumi--aws@7.16.0Pulumi Java SDK for AWS providing strongly-typed Infrastructure-as-Code for 227 AWS service packages including compute, storage, databases, networking, security, analytics, machine learning, and more.
This guide demonstrates how to deploy a complete, production-ready web application on AWS using Pulumi. The architecture includes high availability, security best practices, and scalability.
┌─────────────┐
│ Route 53 │
│ DNS │
└──────┬──────┘
│
┌──────▼──────┐
│ CloudFront │
│ CDN │
└──────┬──────┘
│
┌───────────────────┼───────────────────┐
│ │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ ALB │ │ S3 │ │ S3 │
│ us-west-2a │ │ Static │ │ Logs │
└──────┬──────┘ │ Assets │ └─────────────┘
│ └─────────────┘
┌──────────┴──────────┐
│ Public Subnet │
│ ┌───────────────┐ │
│ │ NAT Gateway │ │
└───┴───────┬───────┴──┘
│
┌───────────┴──────────┐
│ Private Subnet │
│ ┌─────────────────┐ │
│ │ ECS Fargate │ │
│ │ Application │ │
│ └────────┬────────┘ │
└───────────┼──────────┘
│
┌───────────▼──────────┐
│ Private Subnet │
│ ┌─────────────────┐ │
│ │ RDS Database │ │
│ │ PostgreSQL │ │
│ └─────────────────┘ │
└──────────────────────┘import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
// Configuration
const config = new pulumi.Config();
const appName = "webapp";
const environment = pulumi.getStack();
const dbPassword = config.requireSecret("dbPassword");
// Tags for all resources
const tags = {
Environment: environment,
Application: appName,
ManagedBy: "pulumi",
};
// ============================================================================
// 1. NETWORKING - VPC with Public and Private Subnets
// ============================================================================
// Create VPC
const vpc = new aws.ec2.Vpc("vpc", {
cidrBlock: "10.0.0.0/16",
enableDnsHostnames: true,
enableDnsSupport: true,
tags: { ...tags, Name: `${appName}-vpc` },
});
// Get availability zones
const azs = aws.getAvailabilityZones({
state: "available",
});
// Create Internet Gateway
const igw = new aws.ec2.InternetGateway("igw", {
vpcId: vpc.id,
tags: { ...tags, Name: `${appName}-igw` },
});
// Create public subnets in two AZs for high availability
const publicSubnets = ["10.0.1.0/24", "10.0.2.0/24"].map((cidr, i) => {
return new aws.ec2.Subnet(`public-subnet-${i}`, {
vpcId: vpc.id,
cidrBlock: cidr,
availabilityZone: azs.then(az => az.names[i]),
mapPublicIpOnLaunch: true,
tags: { ...tags, Name: `${appName}-public-${i}`, Tier: "public" },
});
});
// Create private subnets for application tier
const privateAppSubnets = ["10.0.11.0/24", "10.0.12.0/24"].map((cidr, i) => {
return new aws.ec2.Subnet(`private-app-subnet-${i}`, {
vpcId: vpc.id,
cidrBlock: cidr,
availabilityZone: azs.then(az => az.names[i]),
tags: { ...tags, Name: `${appName}-private-app-${i}`, Tier: "application" },
});
});
// Create private subnets for database tier
const privateDbSubnets = ["10.0.21.0/24", "10.0.22.0/24"].map((cidr, i) => {
return new aws.ec2.Subnet(`private-db-subnet-${i}`, {
vpcId: vpc.id,
cidrBlock: cidr,
availabilityZone: azs.then(az => az.names[i]),
tags: { ...tags, Name: `${appName}-private-db-${i}`, Tier: "database" },
});
});
// Create Elastic IP for NAT Gateway
const natEip = new aws.ec2.Eip("nat-eip", {
domain: "vpc",
tags: { ...tags, Name: `${appName}-nat-eip` },
});
// Create NAT Gateway in first public subnet
const natGw = new aws.ec2.NatGateway("nat-gw", {
allocationId: natEip.id,
subnetId: publicSubnets[0].id,
tags: { ...tags, Name: `${appName}-nat-gw` },
});
// Public route table
const publicRt = new aws.ec2.RouteTable("public-rt", {
vpcId: vpc.id,
routes: [{
cidrBlock: "0.0.0.0/0",
gatewayId: igw.id,
}],
tags: { ...tags, Name: `${appName}-public-rt` },
});
// Associate public subnets with public route table
publicSubnets.forEach((subnet, i) => {
new aws.ec2.RouteTableAssociation(`public-rta-${i}`, {
subnetId: subnet.id,
routeTableId: publicRt.id,
});
});
// Private route table
const privateRt = new aws.ec2.RouteTable("private-rt", {
vpcId: vpc.id,
routes: [{
cidrBlock: "0.0.0.0/0",
natGatewayId: natGw.id,
}],
tags: { ...tags, Name: `${appName}-private-rt` },
});
// Associate private subnets with private route table
[...privateAppSubnets, ...privateDbSubnets].forEach((subnet, i) => {
new aws.ec2.RouteTableAssociation(`private-rta-${i}`, {
subnetId: subnet.id,
routeTableId: privateRt.id,
});
});
// ============================================================================
// 2. SECURITY GROUPS
// ============================================================================
// ALB Security Group - allows HTTP/HTTPS from internet
const albSg = new aws.ec2.SecurityGroup("alb-sg", {
vpcId: vpc.id,
description: "Security group for Application Load Balancer",
ingress: [
{
protocol: "tcp",
fromPort: 80,
toPort: 80,
cidrBlocks: ["0.0.0.0/0"],
description: "Allow HTTP from internet",
},
{
protocol: "tcp",
fromPort: 443,
toPort: 443,
cidrBlocks: ["0.0.0.0/0"],
description: "Allow HTTPS from internet",
},
],
egress: [{
protocol: "-1",
fromPort: 0,
toPort: 0,
cidrBlocks: ["0.0.0.0/0"],
description: "Allow all outbound",
}],
tags: { ...tags, Name: `${appName}-alb-sg` },
});
// Application Security Group - allows traffic from ALB
const appSg = new aws.ec2.SecurityGroup("app-sg", {
vpcId: vpc.id,
description: "Security group for application containers",
egress: [{
protocol: "-1",
fromPort: 0,
toPort: 0,
cidrBlocks: ["0.0.0.0/0"],
description: "Allow all outbound",
}],
tags: { ...tags, Name: `${appName}-app-sg` },
});
// Allow app to receive traffic from ALB
new aws.ec2.SecurityGroupRule("app-from-alb", {
type: "ingress",
securityGroupId: appSg.id,
sourceSecurityGroupId: albSg.id,
protocol: "tcp",
fromPort: 8080,
toPort: 8080,
description: "Allow traffic from ALB",
});
// Database Security Group - allows traffic from app
const dbSg = new aws.ec2.SecurityGroup("db-sg", {
vpcId: vpc.id,
description: "Security group for RDS database",
egress: [{
protocol: "-1",
fromPort: 0,
toPort: 0,
cidrBlocks: ["0.0.0.0/0"],
description: "Allow all outbound",
}],
tags: { ...tags, Name: `${appName}-db-sg` },
});
// Allow database to receive traffic from app
new aws.ec2.SecurityGroupRule("db-from-app", {
type: "ingress",
securityGroupId: dbSg.id,
sourceSecurityGroupId: appSg.id,
protocol: "tcp",
fromPort: 5432,
toPort: 5432,
description: "Allow PostgreSQL from application",
});
// ============================================================================
// 3. APPLICATION LOAD BALANCER
// ============================================================================
// Create ALB
const alb = new aws.lb.LoadBalancer("alb", {
loadBalancerType: "application",
internal: false,
subnets: publicSubnets.map(s => s.id),
securityGroups: [albSg.id],
enableDeletionProtection: environment === "production",
tags: { ...tags, Name: `${appName}-alb` },
});
// Create Target Group
const targetGroup = new aws.lb.TargetGroup("app-tg", {
port: 8080,
protocol: "HTTP",
vpcId: vpc.id,
targetType: "ip",
healthCheck: {
enabled: true,
path: "/health",
port: "8080",
protocol: "HTTP",
healthyThreshold: 2,
unhealthyThreshold: 3,
timeout: 5,
interval: 30,
matcher: "200",
},
deregistrationDelay: 30,
tags: { ...tags, Name: `${appName}-tg` },
});
// Create HTTP Listener (redirects to HTTPS in production)
const httpListener = new aws.lb.Listener("http-listener", {
loadBalancerArn: alb.arn,
port: 80,
protocol: "HTTP",
defaultActions: [{
type: "redirect",
redirect: {
port: "443",
protocol: "HTTPS",
statusCode: "HTTP_301",
},
}],
});
// ============================================================================
// 4. ECS CLUSTER AND SERVICE
// ============================================================================
// Create ECS Cluster
const cluster = new aws.ecs.Cluster("cluster", {
name: `${appName}-cluster`,
settings: [{
name: "containerInsights",
value: "enabled",
}],
tags,
});
// Create IAM Role for ECS Task Execution
const taskExecutionRole = new aws.iam.Role("task-execution-role", {
assumeRolePolicy: JSON.stringify({
Version: "2012-10-17",
Statement: [{
Action: "sts:AssumeRole",
Effect: "Allow",
Principal: {
Service: "ecs-tasks.amazonaws.com",
},
}],
}),
tags,
});
// Attach AWS managed policy for ECS task execution
new aws.iam.RolePolicyAttachment("task-execution-policy", {
role: taskExecutionRole.name,
policyArn: "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy",
});
// Create IAM Role for ECS Task (application role)
const taskRole = new aws.iam.Role("task-role", {
assumeRolePolicy: JSON.stringify({
Version: "2012-10-17",
Statement: [{
Action: "sts:AssumeRole",
Effect: "Allow",
Principal: {
Service: "ecs-tasks.amazonaws.com",
},
}],
}),
tags,
});
// Create CloudWatch Log Group for application logs
const logGroup = new aws.cloudwatch.LogGroup("app-logs", {
name: `/ecs/${appName}`,
retentionInDays: 30,
tags,
});
// Create Task Definition
const taskDefinition = new aws.ecs.TaskDefinition("app-task", {
family: `${appName}-task`,
networkMode: "awsvpc",
requiresCompatibilities: ["FARGATE"],
cpu: "512",
memory: "1024",
executionRoleArn: taskExecutionRole.arn,
taskRoleArn: taskRole.arn,
containerDefinitions: pulumi.all([logGroup.name, dbPassword]).apply(([logGroupName, password]) =>
JSON.stringify([{
name: "app",
image: "nginx:latest", // Replace with your application image
essential: true,
portMappings: [{
containerPort: 8080,
protocol: "tcp",
}],
environment: [
{
name: "ENVIRONMENT",
value: environment,
},
{
name: "DB_HOST",
value: "", // Will be set below
},
{
name: "DB_NAME",
value: "webapp",
},
],
secrets: [
{
name: "DB_PASSWORD",
valueFrom: "", // Will be set from Secrets Manager
},
],
logConfiguration: {
logDriver: "awslogs",
options: {
"awslogs-group": logGroupName,
"awslogs-region": aws.getRegionOutput().name,
"awslogs-stream-prefix": "app",
},
},
healthCheck: {
command: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"],
interval: 30,
timeout: 5,
retries: 3,
startPeriod: 60,
},
}])
),
tags,
});
// Create ECS Service
const service = new aws.ecs.Service("app-service", {
name: `${appName}-service`,
cluster: cluster.id,
taskDefinition: taskDefinition.arn,
desiredCount: 2,
launchType: "FARGATE",
platformVersion: "LATEST",
networkConfiguration: {
assignPublicIp: false,
subnets: privateAppSubnets.map(s => s.id),
securityGroups: [appSg.id],
},
loadBalancers: [{
targetGroupArn: targetGroup.arn,
containerName: "app",
containerPort: 8080,
}],
healthCheckGracePeriodSeconds: 60,
deploymentConfiguration: {
maximumPercent: 200,
minimumHealthyPercent: 100,
},
tags,
}, { dependsOn: [httpListener] });
// Configure Auto Scaling for ECS Service
const scalingTarget = new aws.appautoscaling.Target("app-scaling-target", {
serviceNamespace: "ecs",
resourceId: pulumi.interpolate`service/${cluster.name}/${service.name}`,
scalableDimension: "ecs:service:DesiredCount",
minCapacity: 2,
maxCapacity: 10,
});
// CPU-based auto scaling
new aws.appautoscaling.Policy("cpu-scaling-policy", {
name: `${appName}-cpu-scaling`,
serviceNamespace: scalingTarget.serviceNamespace,
resourceId: scalingTarget.resourceId,
scalableDimension: scalingTarget.scalableDimension,
policyType: "TargetTrackingScaling",
targetTrackingScalingPolicyConfiguration: {
targetValue: 70.0,
predefinedMetricSpecification: {
predefinedMetricType: "ECSServiceAverageCPUUtilization",
},
scaleInCooldown: 300,
scaleOutCooldown: 60,
},
});
// ============================================================================
// 5. RDS DATABASE
// ============================================================================
// Create DB Subnet Group
const dbSubnetGroup = new aws.rds.SubnetGroup("db-subnet-group", {
subnetIds: privateDbSubnets.map(s => s.id),
tags: { ...tags, Name: `${appName}-db-subnet-group` },
});
// Create RDS Instance
const db = new aws.rds.Instance("db", {
identifier: `${appName}-db`,
engine: "postgres",
engineVersion: "15.5",
instanceClass: "db.t3.micro",
allocatedStorage: 20,
maxAllocatedStorage: 100, // Enable storage autoscaling
storageType: "gp3",
storageEncrypted: true,
dbName: "webapp",
username: "dbadmin",
password: dbPassword,
dbSubnetGroupName: dbSubnetGroup.name,
vpcSecurityGroupIds: [dbSg.id],
publiclyAccessible: false,
multiAz: environment === "production",
backupRetentionPeriod: 7,
backupWindow: "03:00-04:00",
maintenanceWindow: "mon:04:00-mon:05:00",
enabledCloudwatchLogsExports: ["postgresql", "upgrade"],
performanceInsightsEnabled: true,
performanceInsightsRetentionPeriod: 7,
deletionProtection: environment === "production",
skipFinalSnapshot: environment !== "production",
finalSnapshotIdentifier: environment === "production" ? `${appName}-final-snapshot` : undefined,
tags,
});
// ============================================================================
// 6. S3 BUCKET FOR STATIC ASSETS
// ============================================================================
// Create S3 bucket for static assets
const assetsBucket = new aws.s3.BucketV2("assets", {
bucket: `${appName}-assets-${aws.getCallerIdentityOutput().accountId}`,
tags,
});
// Enable versioning
new aws.s3.BucketVersioningV2("assets-versioning", {
bucket: assetsBucket.id,
versioningConfiguration: {
status: "Enabled",
},
});
// Block public access
new aws.s3.BucketPublicAccessBlock("assets-public-access-block", {
bucket: assetsBucket.id,
blockPublicAcls: true,
blockPublicPolicy: true,
ignorePublicAcls: true,
restrictPublicBuckets: true,
});
// Enable encryption
new aws.s3.BucketServerSideEncryptionConfigurationV2("assets-encryption", {
bucket: assetsBucket.id,
rules: [{
applyServerSideEncryptionByDefault: {
sseAlgorithm: "AES256",
},
bucketKeyEnabled: true,
}],
});
// Create Origin Access Identity for CloudFront
const oai = new aws.cloudfront.OriginAccessIdentity("oai", {
comment: `OAI for ${appName}`,
});
// Bucket policy to allow CloudFront access
const assetsBucketPolicy = new aws.s3.BucketPolicy("assets-policy", {
bucket: assetsBucket.id,
policy: pulumi.all([assetsBucket.arn, oai.iamArn]).apply(([bucketArn, oaiArn]) =>
JSON.stringify({
Version: "2012-10-17",
Statement: [{
Effect: "Allow",
Principal: {
AWS: oaiArn,
},
Action: "s3:GetObject",
Resource: `${bucketArn}/*`,
}],
})
),
});
// ============================================================================
// 7. CLOUDFRONT CDN
// ============================================================================
// Create CloudFront distribution
const cdn = new aws.cloudfront.Distribution("cdn", {
enabled: true,
comment: `CDN for ${appName}`,
origins: [
{
originId: "alb",
domainName: alb.dnsName,
customOriginConfig: {
httpPort: 80,
httpsPort: 443,
originProtocolPolicy: "http-only",
originSslProtocols: ["TLSv1.2"],
},
},
{
originId: "s3",
domainName: assetsBucket.bucketRegionalDomainName,
s3OriginConfig: {
originAccessIdentity: oai.cloudfrontAccessIdentityPath,
},
},
],
defaultCacheBehavior: {
targetOriginId: "alb",
viewerProtocolPolicy: "redirect-to-https",
allowedMethods: ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"],
cachedMethods: ["GET", "HEAD"],
compress: true,
defaultTtl: 0,
maxTtl: 0,
minTtl: 0,
forwardedValues: {
queryString: true,
cookies: { forward: "all" },
headers: ["Host", "Origin", "Authorization"],
},
},
orderedCacheBehaviors: [{
pathPattern: "/static/*",
targetOriginId: "s3",
viewerProtocolPolicy: "redirect-to-https",
allowedMethods: ["GET", "HEAD", "OPTIONS"],
cachedMethods: ["GET", "HEAD"],
compress: true,
defaultTtl: 86400,
maxTtl: 31536000,
minTtl: 0,
forwardedValues: {
queryString: false,
cookies: { forward: "none" },
},
}],
priceClass: "PriceClass_100",
restrictions: {
geoRestriction: {
restrictionType: "none",
},
},
viewerCertificate: {
cloudfrontDefaultCertificate: true,
// For custom domain, use:
// acmCertificateArn: certificate.arn,
// sslSupportMethod: "sni-only",
// minimumProtocolVersion: "TLSv1.2_2021",
},
tags,
});
// ============================================================================
// 8. ROUTE 53 DNS (Optional - for custom domain)
// ============================================================================
// Uncomment to use custom domain:
/*
const zone = aws.route53.getZone({
name: "example.com",
});
const certificate = new aws.acm.Certificate("cert", {
domainName: "app.example.com",
validationMethod: "DNS",
tags,
}, { provider: aws.usEast1Provider }); // ACM cert must be in us-east-1 for CloudFront
const certValidationRecord = new aws.route53.Record("cert-validation", {
name: certificate.domainValidationOptions[0].resourceRecordName,
type: certificate.domainValidationOptions[0].resourceRecordType,
zoneId: zone.then(z => z.zoneId),
records: [certificate.domainValidationOptions[0].resourceRecordValue],
ttl: 60,
});
const certValidation = new aws.acm.CertificateValidation("cert-validation", {
certificateArn: certificate.arn,
validationRecordFqdns: [certValidationRecord.fqdn],
}, { provider: aws.usEast1Provider });
const dnsRecord = new aws.route53.Record("dns", {
name: "app",
type: "A",
zoneId: zone.then(z => z.zoneId),
aliases: [{
name: cdn.domainName,
zoneId: cdn.hostedZoneId,
evaluateTargetHealth: false,
}],
});
*/
// ============================================================================
// 9. CLOUDWATCH ALARMS AND MONITORING
// ============================================================================
// ALB Unhealthy Target alarm
new aws.cloudwatch.MetricAlarm("alb-unhealthy-targets", {
name: `${appName}-unhealthy-targets`,
comparisonOperator: "GreaterThanThreshold",
evaluationPeriods: 2,
metricName: "UnHealthyHostCount",
namespace: "AWS/ApplicationELB",
period: 300,
statistic: "Average",
threshold: 0,
dimensions: {
LoadBalancer: alb.arnSuffix,
TargetGroup: targetGroup.arnSuffix,
},
alarmDescription: "Alert when targets are unhealthy",
tags,
});
// ECS Service CPU alarm
new aws.cloudwatch.MetricAlarm("ecs-cpu-high", {
name: `${appName}-ecs-cpu-high`,
comparisonOperator: "GreaterThanThreshold",
evaluationPeriods: 2,
metricName: "CPUUtilization",
namespace: "AWS/ECS",
period: 300,
statistic: "Average",
threshold: 80,
dimensions: {
ClusterName: cluster.name,
ServiceName: service.name,
},
alarmDescription: "Alert when CPU is high",
tags,
});
// RDS CPU alarm
new aws.cloudwatch.MetricAlarm("rds-cpu-high", {
name: `${appName}-rds-cpu-high`,
comparisonOperator: "GreaterThanThreshold",
evaluationPeriods: 2,
metricName: "CPUUtilization",
namespace: "AWS/RDS",
period: 300,
statistic: "Average",
threshold: 80,
dimensions: {
DBInstanceIdentifier: db.identifier,
},
alarmDescription: "Alert when database CPU is high",
tags,
});
// ============================================================================
// OUTPUTS
// ============================================================================
export const vpcId = vpc.id;
export const albDns = alb.dnsName;
export const cdnDomain = cdn.domainName;
export const cdnUrl = pulumi.interpolate`https://${cdn.domainName}`;
export const dbEndpoint = db.endpoint;
export const dbAddress = db.address;
export const clusterName = cluster.name;
export const serviceName = service.name;
export const assetsBucketName = assetsBucket.bucket;Create a Pulumi.dev.yaml configuration file:
config:
aws:region: us-west-2
webapp:dbPassword:
secure: your-encrypted-passwordSet the database password:
pulumi config set --secret dbPassword YourSecurePassword123!# Install dependencies
npm install
# Preview changes
pulumi preview
# Deploy infrastructure
pulumi up
# Get outputs
pulumi stack output cdnUrl
pulumi stack output albDns# Destroy all resources
pulumi destroy
# Remove stack
pulumi stack rm dev