CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-pulumi--aws

A Pulumi package for creating and managing Amazon Web Services (AWS) cloud resources with infrastructure-as-code.

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

concepts.mddocs/core/

Core Concepts

This guide explains the fundamental concepts you need to understand when working with Pulumi and AWS.

Pulumi Fundamentals

Resources

Resources are the fundamental building blocks of Pulumi infrastructure. Each resource represents a cloud infrastructure object managed by Pulumi.

import * as aws from "@pulumi/aws";

// Resource declaration
const bucket = new aws.s3.Bucket("my-bucket", {
    bucketName: "my-unique-bucket-name",
    acl: "private"
});

Key characteristics:

  • Resources have a logical name (first argument) and properties (second argument)
  • Logical names must be unique within a stack
  • Resources track state and detect changes on updates
  • Resources can be referenced by other resources

Inputs and Outputs

Pulumi uses a special type system to handle asynchronous resource creation:

import * as aws from "@pulumi/aws";
import * as pulumi from "@pulumi/core";

const bucket = new aws.s3.Bucket("my-bucket");

// bucket.arn is an Output<string>
const bucketArn: pulumi.Output<string> = bucket.arn;

// Use .apply() to work with output values
bucket.arn.apply(arn => {
    console.log(`Bucket ARN: ${arn}`);
});

// Use outputs as inputs to other resources
const bucketPolicy = new aws.s3.BucketPolicy("my-policy", {
    bucket: bucket.id, // Output<string> automatically unwrapped
    policy: bucket.arn.apply(arn => JSON.stringify({
        Version: "2012-10-17",
        Statement: [{
            Effect: "Allow",
            Principal: "*",
            Action: "s3:GetObject",
            Resource: `${arn}/*`
        }]
    }))
});

Output types:

  • Output<T>: Represents a value that will be available after resource creation
  • Input<T>: Accepts either a plain value or an Output<T>
  • Use .apply() to transform output values
  • Outputs are automatically unwrapped when passed as inputs

State Management

Pulumi maintains state to track the current state of your infrastructure:

State storage:

  • State is stored in a backend (Pulumi Service, S3, local file, etc.)
  • Contains resource IDs, properties, and dependencies
  • Used to compute diffs during updates
  • Encrypted at rest
// State is automatically managed, but you can inspect it
import * as pulumi from "@pulumi/core";

// Export values to make them visible in state
export const bucketName = bucket.id;
export const bucketUrl = pulumi.interpolate`https://${bucket.bucketDomainName}`;

State operations:

  • pulumi up: Creates/updates resources and updates state
  • pulumi refresh: Syncs state with actual infrastructure
  • pulumi destroy: Removes resources and cleans up state
  • pulumi stack export: View/backup state

Stack Configuration

Stacks represent different deployment environments (dev, staging, prod):

import * as pulumi from "@pulumi/core";

// Read configuration values
const config = new pulumi.Config();
const instanceType = config.get("instanceType") || "t3.micro";
const region = config.require("region"); // Throws if not set
const tags = config.requireObject<Record<string, string>>("tags");

// Use configuration in resources
const instance = new aws.ec2.Instance("web-server", {
    instanceType: instanceType,
    tags: tags
});

Configuration management:

  • Set values: pulumi config set instanceType t3.small
  • Set secrets: pulumi config set --secret dbPassword hunter2
  • View config: pulumi config
  • Config is stack-specific

AWS Provider Architecture

Provider Configuration

The AWS provider manages authentication and default settings:

import * as aws from "@pulumi/aws";
import * as pulumi from "@pulumi/core";

// Default provider (uses AWS credentials from environment)
const bucket = new aws.s3.Bucket("my-bucket");

// Explicit provider with custom configuration
const usWest2Provider = new aws.Provider("us-west-2", {
    region: "us-west-2",
    profile: "my-aws-profile",
    defaultTags: {
        tags: {
            Environment: "production",
            ManagedBy: "pulumi"
        }
    }
});

// Use explicit provider
const westBucket = new aws.s3.Bucket("west-bucket", {
    bucketName: "my-west-bucket"
}, { provider: usWest2Provider });

Provider options:

  • region: AWS region for resources
  • profile: AWS CLI profile to use
  • defaultTags: Tags applied to all resources
  • allowedAccountIds: Restrict to specific AWS accounts
  • assumeRole: Assume an IAM role

Resource Naming

AWS resources follow specific naming patterns:

// Logical name vs physical name
const bucket = new aws.s3.Bucket("my-logical-name", {
    // Physical name (optional) - must be globally unique
    bucketName: "my-company-prod-data-2024"
    // If not specified, Pulumi generates: my-logical-name-a1b2c3d
});

// Auto-naming with prefix
const autoNamedBucket = new aws.s3.Bucket("data-bucket", {
    // Pulumi will generate: data-bucket-a1b2c3d
});

// Access the actual name
export const actualBucketName = bucket.id;

Naming best practices:

  • Use logical names for Pulumi tracking
  • Omit physical names for auto-generated names (recommended)
  • Specify physical names when required (domain names, certain integrations)
  • Use name prefixes for organization

Resource vs Data Source

Resources

Resources are infrastructure objects that Pulumi creates and manages:

// Resources are declared with "new"
const vpc = new aws.ec2.Vpc("my-vpc", {
    cidrBlock: "10.0.0.0/16",
    enableDnsHostnames: true,
    tags: { Name: "my-vpc" }
});

// Pulumi will:
// - Create the VPC on first deployment
// - Update it when properties change
// - Delete it when removed from code

Resource characteristics:

  • Created and managed by Pulumi
  • Have full lifecycle (create, update, delete)
  • Changes tracked in state
  • Can have dependencies

Data Sources

Data sources query existing infrastructure without managing it:

// Data sources use get* functions
const defaultVpc = aws.ec2.getVpcOutput({
    default: true
});

const ami = aws.ec2.getAmiOutput({
    filters: [{
        name: "name",
        values: ["amzn2-ami-hvm-*-x86_64-gp2"]
    }],
    owners: ["amazon"],
    mostRecent: true
});

// Use data source outputs in resources
const instance = new aws.ec2.Instance("web", {
    ami: ami.id,
    instanceType: "t3.micro",
    vpcSecurityGroupIds: [defaultVpc.apply(v => v.defaultSecurityGroupId)]
});

Data source characteristics:

  • Query existing infrastructure
  • Read-only (no create/update/delete)
  • Not tracked in state (re-queried on each run)
  • Useful for referencing pre-existing resources

When to use:

  • Resources: Infrastructure you want Pulumi to manage
  • Data sources: Infrastructure managed elsewhere (console, other tools, manual)

Dependency Management

Explicit Dependencies

Pulumi automatically infers dependencies when outputs are used as inputs:

// Automatic dependency inference
const bucket = new aws.s3.Bucket("my-bucket");

// Policy depends on bucket (inferred from bucket.id usage)
const policy = new aws.s3.BucketPolicy("my-policy", {
    bucket: bucket.id,
    policy: JSON.stringify({
        Version: "2012-10-17",
        Statement: [{
            Effect: "Allow",
            Principal: "*",
            Action: "s3:GetObject",
            Resource: pulumi.interpolate`${bucket.arn}/*`
        }]
    })
});

Explicit Dependencies

Sometimes you need to specify dependencies manually:

const cluster = new aws.rds.Cluster("db", {
    engine: "aurora-mysql",
    masterUsername: "admin",
    masterPassword: dbPassword
});

const instance = new aws.rds.ClusterInstance("db-instance", {
    clusterIdentifier: cluster.id,
    instanceClass: "db.t3.small",
    engine: cluster.engine
}, {
    // Explicit dependency ensures cluster is fully ready
    dependsOn: [cluster]
});

Use explicit dependencies when:

  • Resources have implicit ordering requirements
  • AWS resources need time to stabilize
  • Creating resources in a specific sequence
  • Non-output-based relationships exist

Parent-Child Relationships

Resources can have parent-child relationships for organization:

// Parent resource
const vpc = new aws.ec2.Vpc("my-vpc", {
    cidrBlock: "10.0.0.0/16"
});

// Child resources
const subnet1 = new aws.ec2.Subnet("subnet-1", {
    vpcId: vpc.id,
    cidrBlock: "10.0.1.0/24"
}, { parent: vpc });

const subnet2 = new aws.ec2.Subnet("subnet-2", {
    vpcId: vpc.id,
    cidrBlock: "10.0.2.0/24"
}, { parent: vpc });

// When parent is deleted, children are deleted first

Parent-child benefits:

  • Logical grouping in state
  • Children deleted before parent
  • Hierarchical visualization
  • Protection (can't delete parent without children)

Dependency Graph

Pulumi builds a dependency graph to determine execution order:

// Complex dependency example
const vpc = new aws.ec2.Vpc("vpc", { cidrBlock: "10.0.0.0/16" });
const subnet = new aws.ec2.Subnet("subnet", { vpcId: vpc.id, cidrBlock: "10.0.1.0/24" });
const sg = new aws.ec2.SecurityGroup("sg", { vpcId: vpc.id });
const instance = new aws.ec2.Instance("instance", {
    subnetId: subnet.id,
    vpcSecurityGroupIds: [sg.id],
    ami: "ami-12345",
    instanceType: "t3.micro"
});

// Execution order: vpc -> (subnet + sg) -> instance
// subnet and sg can be created in parallel

Graph properties:

  • Topologically sorted
  • Parallel execution where possible
  • Cycle detection prevents circular dependencies
  • Visible with pulumi preview --show-replacement-steps

Resource Options

All resources accept resource options to control behavior:

const bucket = new aws.s3.Bucket("my-bucket", {
    bucketName: "my-unique-bucket"
}, {
    // Resource options (third argument)
    protect: true,                    // Prevent accidental deletion
    deleteBeforeReplace: true,        // Delete before creating replacement
    ignoreChanges: ["tags"],          // Ignore changes to tags
    aliases: ["urn:pulumi:stack::project::aws:s3/bucket:Bucket::old-name"],
    dependsOn: [someOtherResource],
    provider: customProvider,
    parent: parentResource,
    retainOnDelete: true,             // Keep resource after stack deletion
    customTimeouts: {
        create: "10m",
        update: "10m",
        delete: "10m"
    }
});

Common options:

  • protect: Prevents deletion (must unset to delete)
  • ignoreChanges: Ignore drift in specified properties
  • deleteBeforeReplace: Useful for name-based resources
  • retainOnDelete: Keep resource when removing from Pulumi
  • customTimeouts: Override default operation timeouts

Common Patterns

String Interpolation

Combine outputs with strings using pulumi.interpolate:

import * as pulumi from "@pulumi/core";

const bucket = new aws.s3.Bucket("my-bucket");
const distribution = new aws.cloudfront.Distribution("cdn", { /* ... */ });

// Interpolate outputs into strings
export const websiteUrl = pulumi.interpolate`https://${distribution.domainName}`;
export const s3Uri = pulumi.interpolate`s3://${bucket.id}/data`;

// Use in resource properties
const policy = new aws.s3.BucketPolicy("policy", {
    bucket: bucket.id,
    policy: pulumi.interpolate`{
        "Version": "2012-10-17",
        "Statement": [{
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "${bucket.arn}/*"
        }]
    }`
});

All() for Multiple Outputs

Combine multiple outputs:

import * as pulumi from "@pulumi/core";

const bucket1 = new aws.s3.Bucket("bucket-1");
const bucket2 = new aws.s3.Bucket("bucket-2");
const bucket3 = new aws.s3.Bucket("bucket-3");

// Wait for all outputs
const allBuckets = pulumi.all([bucket1.id, bucket2.id, bucket3.id]);

export const bucketList = allBuckets.apply(([id1, id2, id3]) =>
    `Buckets: ${id1}, ${id2}, ${id3}`
);

// Use in resources
const policy = new aws.iam.Policy("multi-bucket-policy", {
    policy: pulumi.all([bucket1.arn, bucket2.arn, bucket3.arn])
        .apply(([arn1, arn2, arn3]) => JSON.stringify({
            Version: "2012-10-17",
            Statement: [{
                Effect: "Allow",
                Action: "s3:GetObject",
                Resource: [arn1, arn2, arn3].map(arn => `${arn}/*`)
            }]
        }))
});

Component Resources

Create reusable infrastructure components:

import * as pulumi from "@pulumi/core";
import * as aws from "@pulumi/aws";

// Component resource
class WebServer extends pulumi.ComponentResource {
    public readonly instance: aws.ec2.Instance;
    public readonly securityGroup: aws.ec2.SecurityGroup;
    public readonly publicIp: pulumi.Output<string>;

    constructor(name: string, args: {
        vpcId: pulumi.Input<string>,
        subnetId: pulumi.Input<string>,
        instanceType?: string
    }, opts?: pulumi.ComponentResourceOptions) {
        super("custom:WebServer", name, {}, opts);

        // Security group
        this.securityGroup = new aws.ec2.SecurityGroup(`${name}-sg`, {
            vpcId: args.vpcId,
            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"] }
            ],
            egress: [
                { protocol: "-1", fromPort: 0, toPort: 0, cidrBlocks: ["0.0.0.0/0"] }
            ]
        }, { parent: this });

        // EC2 instance
        this.instance = new aws.ec2.Instance(`${name}-instance`, {
            instanceType: args.instanceType || "t3.micro",
            ami: "ami-12345",
            subnetId: args.subnetId,
            vpcSecurityGroupIds: [this.securityGroup.id]
        }, { parent: this });

        this.publicIp = this.instance.publicIp;

        this.registerOutputs({
            instanceId: this.instance.id,
            publicIp: this.publicIp
        });
    }
}

// Usage
const webServer = new WebServer("my-web-server", {
    vpcId: vpc.id,
    subnetId: subnet.id,
    instanceType: "t3.small"
});

Dynamic Providers

Create custom resource providers for non-AWS resources or custom logic (advanced):

import * as pulumi from "@pulumi/pulumi";

interface RandomIdInputs {
    length: number;
}

class RandomIdProvider implements pulumi.dynamic.ResourceProvider {
    async create(inputs: RandomIdInputs): Promise<pulumi.dynamic.CreateResult> {
        const id = Math.random().toString(36).substring(2, 2 + inputs.length);
        return { id, outs: { result: id, length: inputs.length } };
    }
}

class RandomId extends pulumi.dynamic.Resource {
    public readonly result!: pulumi.Output<string>;

    constructor(name: string, args: RandomIdInputs, opts?: pulumi.CustomResourceOptions) {
        super(new RandomIdProvider(), name, { result: undefined, ...args }, opts);
    }
}

// Usage
const randomId = new RandomId("my-random-id", { length: 8 });
export const randomValue = randomId.result;

Best Practices

State Management

  • Always use a remote backend for team projects
  • Enable state locking to prevent concurrent modifications
  • Regularly backup state (export/snapshot)
  • Never manually edit state files

Configuration

  • Use stack config for environment-specific values
  • Mark sensitive values as secrets
  • Validate configuration early in the program
  • Document required configuration keys

Dependencies

  • Let Pulumi infer dependencies from outputs
  • Use explicit dependsOn only when necessary
  • Avoid circular dependencies
  • Use parent-child relationships for logical grouping

Resource Organization

  • Use consistent naming conventions
  • Group related resources with component resources
  • Use tags for organization and cost tracking
  • Keep stack files focused and modular

Testing

  • Use pulumi preview before every deployment
  • Test configuration changes in non-prod stacks first
  • Use unit tests for component resources
  • Implement integration tests for critical infrastructure

Install with Tessl CLI

npx tessl i tessl/npm-pulumi--aws

docs

index.md

quickstart.md

README.md

tile.json