1. The Anatomy of a Pulumi Resource#
In Pulumi, every physical piece of cloud infrastructure is represented by a TypeScript Class.
To create a resource, you instantiate the class using the new keyword. Every resource class follows the exact same architectural signature:
new provider.namespace.ResourceClass("logical-name", { args }, { options });"logical-name": The string identifier Pulumi uses to track the resource in the state file. It must be unique across all resources of the same type in your project.{ args }: An object matching a specific TypeScript Interface that contains the configuration for the resource (e.g.,instanceType,ami).{ options }: (Optional) ACustomResourceOptionsobject used to configure Pulumi engine behavior, such as preventing destruction or specifying explicit dependencies.
2. Practice: Building an AWS VPC#
Let’s build a small network architecture. We will create a Virtual Private Cloud (VPC), a Subnet, and a Security Group.
Open index.ts and clear out the default S3 bucket code.
Step 2.1: The VPC and Subnet#
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
// 1. Create the VPC
const vpc = new aws.ec2.Vpc("main-vpc", {
cidrBlock: "10.0.0.0/16",
enableDnsHostnames: true,
tags: {
Name: "Pulumi-Main-VPC",
},
});
// 2. Create the Subnet
const subnet = new aws.ec2.Subnet("public-subnet", {
// Notice how we pass the VPC ID!
vpcId: vpc.id,
cidrBlock: "10.0.1.0/24",
mapPublicIpOnLaunch: true,
tags: {
Name: "Pulumi-Public-Subnet",
},
});The Magic of Implicit Dependencies#
In the Subnet arguments, look closely at this line: vpcId: vpc.id.
We are passing a property from the vpc object directly into the subnet object.
When Pulumi executes this code, it builds a Dependency Graph. It sees that the Subnet relies on the VPC’s ID. Therefore, Pulumi knows it MUST wait for AWS to finish creating the VPC before it even attempts to create the Subnet.
You do not need to write dependsOn manually; Pulumi infers it from the variable references!
Step 2.2: The Security Group#
Next, let’s create a Security Group to allow SSH (Port 22) and HTTP (Port 80) traffic.
// 3. Create a Security Group
const webSecurityGroup = new aws.ec2.SecurityGroup("web-sg", {
vpcId: vpc.id,
description: "Allow SSH and HTTP",
ingress: [
{
protocol: "tcp",
fromPort: 22,
toPort: 22,
cidrBlocks: ["0.0.0.0/0"],
},
{
protocol: "tcp",
fromPort: 80,
toPort: 80,
cidrBlocks: ["0.0.0.0/0"],
}
],
egress: [
{
protocol: "-1", // All traffic
fromPort: 0,
toPort: 0,
cidrBlocks: ["0.0.0.0/0"],
}
]
});Notice the ingress property. In Terraform, this would be a nested block. In Pulumi TypeScript, it is simply an Array of Objects. This makes it incredibly easy to map over data arrays to generate rules programmatically (which we will cover in Episode 6).
3. Practice: Deploying an EC2 Instance#
Finally, let’s deploy an EC2 instance into our new Subnet and attach the Security Group.
// 4. Create the EC2 Instance
const webServer = new aws.ec2.Instance("web-server", {
// We will hardcode an Ubuntu 22.04 AMI for this example
ami: "ami-0fc5d935ebf8bc3bc",
instanceType: "t3.micro",
// Wire up the dependencies
subnetId: subnet.id,
vpcSecurityGroupIds: [webSecurityGroup.id],
tags: {
Name: "Pulumi-Web-Server",
},
});
// 5. Export the Public IP so we can see it in the terminal
export const serverPublicIp = webServer.publicIp;Run pulumi up.
Pulumi will determine the exact order of operations:
- Create VPC
- Create Subnet & Security Group (in parallel)
- Create EC2 Instance (waits for Subnet and SG)
Expected Terminal Output:
Outputs:
+ serverPublicIp: "3.234.12.98"4. Explicit Dependencies (The dependsOn Option)#
In 99% of cases, passing an id or arn from one resource to another creates a sufficient Implicit Dependency.
However, sometimes you have two resources that do not share any data, but one must absolutely be created before the other due to cloud provider quirks (e.g., an IAM Role Policy Attachment must exist before a Lambda Function can boot up).
In these rare cases, you use the third argument of the resource class: the Resource Options object.
const iamRole = new aws.iam.Role("my-role", { ... });
const rolePolicy = new aws.iam.RolePolicyAttachment("my-policy", {
role: iamRole.name,
policyArn: "arn:aws:iam::aws:policy/AdministratorAccess",
});
const myLambda = new aws.lambda.Function("my-lambda", {
role: iamRole.arn,
// ... other args
}, {
// 3rd Argument: Resource Options
// Force Pulumi to wait for the Policy Attachment to finish
dependsOn: [rolePolicy],
});Troubleshooting & Common Errors#
Type 'number' is not assignable to type 'string'- Root Cause: This is not a Pulumi error; it is a TypeScript compiler error. You passed the wrong data type to an argument (e.g.,
fromPort: "80"instead offromPort: 80). - Solution: Listen to your IDE. Hover over the argument to read the TypeScript interface definition and provide the correct type.
- Root Cause: This is not a Pulumi error; it is a TypeScript compiler error. You passed the wrong data type to an argument (e.g.,
DependencyViolation: resource vpc-123 has dependent object- Root Cause: When running
pulumi destroy, AWS refused to delete the VPC because something else (created outside of Pulumi) is running inside it. - Solution: Log into the AWS Console, find the rogue resource in the VPC, delete it manually, and run
pulumi destroyagain.
- Root Cause: When running
Conclusion & Next Steps#
You have successfully mapped AWS infrastructure logic into object-oriented TypeScript classes. Your IDE is now acting as a real-time validation engine, drastically reducing the feedback loop compared to standard YAML/HCL development.
However, in this episode, we hardcoded the instanceType: "t3.micro" directly into the code. If we want to deploy this to Production, we need a way to dynamically inject a larger instance type (like m5.large) without modifying the index.ts file.
In Episode 4: Configuration and Secrets Management, we will learn how to inject environment-specific variables and securely manage sensitive passwords using the Pulumi Config system.

