AWS Infrastructure with Pulumi

Medium 28 min read

AWS Provider Setup

Why AWS with Pulumi?

The Problem: AWS has 200+ services and the console makes it hard to reproduce infrastructure reliably across environments.

The Solution: Pulumi lets you define AWS infrastructure using real programming languages with type safety, IDE support, and full abstraction capabilities.

Real Impact: Teams using Pulumi with AWS reduce infrastructure provisioning time by up to 80% and eliminate configuration drift entirely.

Real-World Analogy

Think of Pulumi with AWS as an architect's blueprint system:

  • AWS Provider = The construction company that builds from blueprints
  • VPC = The land and fencing around your building complex
  • Subnets = Different zones within your property (public lobby, private offices)
  • EC2 Instances = Individual buildings on your property
  • S3 Buckets = Storage warehouses for your documents

Key AWS Concepts in Pulumi

Provider Configuration

Configure AWS credentials, region, and profile settings to authenticate Pulumi with your AWS account.

Resource Naming

Pulumi auto-generates unique names for AWS resources, preventing naming conflicts across stacks.

Cross-Region Deploy

Use multiple provider instances to deploy resources across different AWS regions from one program.

Tagging Strategy

Apply consistent tags to all resources using transformations for cost tracking and organization.

Installing the AWS Provider

setup.sh
# Create a new Pulumi project for AWS
pulumi new aws-typescript

# Install the AWS provider package
npm install @pulumi/aws

# Configure the AWS region
pulumi config set aws:region us-east-1

# Optionally set an AWS profile
pulumi config set aws:profile my-profile

Provider Configuration in Code

index.ts
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

// Default provider uses pulumi config values
const defaultVpc = new aws.ec2.Vpc("default-vpc", {
    cidrBlock: "10.0.0.0/16",
});

// Explicit provider for a different region
const westProvider = new aws.Provider("west", {
    region: "us-west-2",
});

const westVpc = new aws.ec2.Vpc("west-vpc", {
    cidrBlock: "10.1.0.0/16",
}, { provider: westProvider });

VPC & Networking

AWS VPC Architecture with Pulumi
VPC (10.0.0.0/16) Public Subnet (AZ-a) 10.0.1.0/24 EC2 Instance Web Server NAT Gateway S3 Bucket Private Subnet (AZ-a) 10.0.2.0/24 Lambda Function DynamoDB Table Public Subnet (AZ-b) 10.0.3.0/24 Application Load Balancer Private Subnet (AZ-b) 10.0.4.0/24 RDS Database Internet Gateway

Creating a VPC with Subnets

vpc.ts
import * as aws from "@pulumi/aws";

// Create a VPC
const vpc = new aws.ec2.Vpc("main-vpc", {
    cidrBlock: "10.0.0.0/16",
    enableDnsHostnames: true,
    enableDnsSupport: true,
    tags: { Name: "main-vpc", Environment: "production" },
});

// Create public subnets
const publicSubnet = new aws.ec2.Subnet("public-subnet", {
    vpcId: vpc.id,
    cidrBlock: "10.0.1.0/24",
    availabilityZone: "us-east-1a",
    mapPublicIpOnLaunch: true,
    tags: { Name: "public-subnet" },
});

// Create private subnets
const privateSubnet = new aws.ec2.Subnet("private-subnet", {
    vpcId: vpc.id,
    cidrBlock: "10.0.2.0/24",
    availabilityZone: "us-east-1a",
    tags: { Name: "private-subnet" },
});

// Internet Gateway for public access
const igw = new aws.ec2.InternetGateway("igw", {
    vpcId: vpc.id,
    tags: { Name: "main-igw" },
});

// Route table for public subnet
const publicRouteTable = new aws.ec2.RouteTable("public-rt", {
    vpcId: vpc.id,
    routes: [{
        cidrBlock: "0.0.0.0/0",
        gatewayId: igw.id,
    }],
});

new aws.ec2.RouteTableAssociation("public-rta", {
    subnetId: publicSubnet.id,
    routeTableId: publicRouteTable.id,
});

export const vpcId = vpc.id;
export const publicSubnetId = publicSubnet.id;

Compute (EC2 & Lambda)

Launching an EC2 Instance

ec2.ts
// Security group for web traffic
const webSg = new aws.ec2.SecurityGroup("web-sg", {
    vpcId: vpc.id,
    description: "Allow HTTP and SSH",
    ingress: [
        { protocol: "tcp", fromPort: 80, toPort: 80, cidrBlocks: ["0.0.0.0/0"] },
        { protocol: "tcp", fromPort: 443, toPort: 443, cidrBlocks: ["0.0.0.0/0"] },
        { protocol: "tcp", fromPort: 22, toPort: 22, cidrBlocks: ["10.0.0.0/16"] },
    ],
    egress: [
        { protocol: "-1", fromPort: 0, toPort: 0, cidrBlocks: ["0.0.0.0/0"] },
    ],
});

// Look up the latest Amazon Linux 2 AMI
const ami = aws.ec2.getAmi({
    mostRecent: true,
    owners: ["amazon"],
    filters: [{ name: "name", values: ["amzn2-ami-hvm-*-x86_64-gp2"] }],
});

// Launch an EC2 instance
const server = new aws.ec2.Instance("web-server", {
    instanceType: "t3.micro",
    ami: ami.then(a => a.id),
    subnetId: publicSubnet.id,
    vpcSecurityGroupIds: [webSg.id],
    userData: `#!/bin/bash
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
echo "Hello from Pulumi!" > /var/www/html/index.html`,
    tags: { Name: "web-server" },
});

export const publicIp = server.publicIp;

Creating a Lambda Function

lambda.ts
// IAM role for Lambda
const lambdaRole = new aws.iam.Role("lambda-role", {
    assumeRolePolicy: JSON.stringify({
        Version: "2012-10-17",
        Statement: [{
            Action: "sts:AssumeRole",
            Principal: { Service: "lambda.amazonaws.com" },
            Effect: "Allow",
        }],
    }),
});

new aws.iam.RolePolicyAttachment("lambda-basic", {
    role: lambdaRole.name,
    policyArn: aws.iam.ManagedPolicies.AWSLambdaBasicExecutionRole,
});

// Create the Lambda function
const fn = new aws.lambda.Function("api-handler", {
    runtime: "nodejs18.x",
    handler: "index.handler",
    role: lambdaRole.arn,
    code: new pulumi.asset.AssetArchive({
        "index.js": new pulumi.asset.StringAsset(
            `exports.handler = async (event) => {
    return {
        statusCode: 200,
        body: JSON.stringify({ message: "Hello from Pulumi Lambda!" }),
    };
};`
        ),
    }),
    environment: {
        variables: { STAGE: pulumi.getStack() },
    },
});

export const functionName = fn.name;

Storage (S3 & DynamoDB)

S3 Bucket with Configuration

storage.ts
// S3 bucket with versioning and encryption
const bucket = new aws.s3.BucketV2("data-bucket", {
    tags: { Environment: pulumi.getStack() },
});

new aws.s3.BucketVersioningV2("data-versioning", {
    bucket: bucket.id,
    versioningConfiguration: { status: "Enabled" },
});

new aws.s3.BucketServerSideEncryptionConfigurationV2("data-encryption", {
    bucket: bucket.id,
    rules: [{
        applyServerSideEncryptionByDefault: {
            sseAlgorithm: "aws:kms",
        },
    }],
});

// Block all public access
new aws.s3.BucketPublicAccessBlock("data-public-block", {
    bucket: bucket.id,
    blockPublicAcls: true,
    blockPublicPolicy: true,
    ignorePublicAcls: true,
    restrictPublicBuckets: true,
});

// DynamoDB table
const table = new aws.dynamodb.Table("app-table", {
    attributes: [
        { name: "pk", type: "S" },
        { name: "sk", type: "S" },
    ],
    hashKey: "pk",
    rangeKey: "sk",
    billingMode: "PAY_PER_REQUEST",
    tags: { Environment: pulumi.getStack() },
});

export const bucketName = bucket.bucket;
export const tableName = table.name;

IAM & Security

IAM Roles and Policies

iam.ts
// Create an IAM role for an application
const appRole = new aws.iam.Role("app-role", {
    assumeRolePolicy: JSON.stringify({
        Version: "2012-10-17",
        Statement: [{
            Action: "sts:AssumeRole",
            Principal: { Service: "ec2.amazonaws.com" },
            Effect: "Allow",
        }],
    }),
});

// Custom inline policy for S3 access
const s3Policy = new aws.iam.RolePolicy("s3-access", {
    role: appRole.id,
    policy: bucket.arn.apply(arn => JSON.stringify({
        Version: "2012-10-17",
        Statement: [{
            Effect: "Allow",
            Action: ["s3:GetObject", "s3:PutObject"],
            Resource: `${arn}/*`,
        }],
    })),
});

// Instance profile to attach role to EC2
const instanceProfile = new aws.iam.InstanceProfile("app-profile", {
    role: appRole.name,
});

Quick Reference

AWS Resource Cheat Sheet

Resource Pulumi Class Key Properties
VPC aws.ec2.Vpc cidrBlock, enableDnsHostnames, tags
Subnet aws.ec2.Subnet vpcId, cidrBlock, availabilityZone
EC2 Instance aws.ec2.Instance instanceType, ami, subnetId, userData
Lambda aws.lambda.Function runtime, handler, role, code
S3 Bucket aws.s3.BucketV2 tags, forceDestroy
DynamoDB aws.dynamodb.Table attributes, hashKey, billingMode
IAM Role aws.iam.Role assumeRolePolicy, tags

Essential CLI Commands

Common Workflows

  • pulumi up - Preview and deploy changes to AWS
  • pulumi preview - See what changes will be made without deploying
  • pulumi stack output - View exported values like IPs and ARNs
  • pulumi destroy - Tear down all AWS resources in the stack
  • pulumi refresh - Sync state with actual AWS resources