How to Automate Cloud Infrastructure with TypeScript: A Complete Guide
By Braincuber Team
Published on May 9, 2026
Managing cloud infrastructure with YAML templates is painful. Hundreds of lines of markup for a dozen lines of application code. No IntelliSense, no compile-time checks, no reusable abstractions. This complete tutorial shows you how to replace YAML entirely with TypeScript using Pulumi, a tool that lets you define AWS resources like Lambda, DynamoDB, API Gateway, and S3 as real typed code. You will build a working URL shortener application, create custom reusable components, and understand how to avoid cloud vendor lock-in with cross-cloud abstractions.
What You Will Learn:
- Why YAML-based IaC tools like CloudFormation and Terraform fall short for developers
- How Pulumi translates TypeScript code into cloud resource provisioning
- How to define a DynamoDB table with a single TypeScript class
- How to create AWS Lambda functions with full IAM roles and policies
- How to configure API Gateway, S3, and bind them to Lambda endpoints
- How to build custom reusable components that hide infrastructure complexity
- How Pulumi's cloud library enables cross-cloud deployments without vendor lock-in
Prerequisites
| Requirement | Details |
|---|---|
| Node.js and npm | Node.js 12+ with npm installed locally |
| Pulumi CLI | Install from pulumi.com/quickstart/install.html |
| AWS Account | Active account with access to Lambda, DynamoDB, API Gateway, S3, and IAM |
| TypeScript Knowledge | Basic familiarity with TypeScript and Node.js module system |
| AWS CLI | Configured with credentials for programmatic access |
Why YAML Fails for Cloud Infrastructure Management
The rise of managed cloud services, cloud-native, and serverless applications brings both new possibilities and challenges. Practices from software development such as version control, code review, continuous integration, and automated testing now apply to cloud infrastructure automation. Most existing tools suggest defining infrastructure in text-based markup formats, with YAML being the most popular choice. AWS CloudFormation templates, Terraform configurations, and Serverless Framework definitions all use YAML. However, using YAML for infrastructure brings significant developer experience pain.
Consider a simple URL shortener application with AWS Lambda and DynamoDB. The application code for the Open URL Lambda is only 11 lines of JavaScript. A comparable CloudFormation template to deploy the same infrastructure spans 317 lines of YAML. A Terraform equivalent runs to about 450 lines. The Serverless Framework reduces this to around 45 lines, but only by sacrificing flexibility. As soon as you need lower-level resources or container-based components, you fall back to verbose YAML templates.
No Compile-Time Checks
YAML templates provide zero type safety. A misspelled resource property or incorrect type silently becomes a runtime failure during deployment, often after minutes of waiting for CloudFormation to fail.
No IntelliSense or Autocomplete
Writing YAML infrastructure definitions means memorizing hundreds of resource types, property names, and valid values. There is no editor autocomplete, no inline documentation, and no parameter validation as you type.
Poor Abstractions
YAML offers limited mechanisms for creating reusable abstractions. Terraform modules and CloudFormation macros exist but are clunky compared to the abstraction capabilities built into general-purpose programming languages.
Multiple Languages Across Tools
Teams often end up using CloudFormation YAML for core infrastructure, Serverless Framework YAML for functions, and Terraform HCL for multi-cloud resources. Developers must context-switch between three different configuration languages for a single application.
The ideal infrastructure tool should provide reproducible results, require no human intervention, define desired state rather than steps, support multiple cloud providers, be universal for any resource type, and be succinct. It should also use a real programming language that developers already know. Pulumi fulfills all of these requirements.
How Pulumi Works
Pulumi is a tool to build cloud-based software using real programming languages including TypeScript, Python, and Go. It supports all major cloud providers plus Kubernetes. Under the hood, Pulumi translates your TypeScript program into a desired state configuration. The engine compares this desired state against the last known deployed state stored in a backend and calculates the exact set of create, update, and delete operations needed to reach the target configuration.
Install Pulumi and Set Up a TypeScript Project
Install the Pulumi CLI from the official website. Create a new directory and install the required npm packages: @pulumi/pulumi for the core engine and @pulumi/aws for AWS resource definitions. Pulumi uses a stack system where each stack represents an isolated deployment environment like dev, staging, or production.
mkdir url-shortener && cd url-shortener
npm init -y
npm install @pulumi/pulumi @pulumi/aws
pulumi new aws-typescript
Defining a DynamoDB Table with TypeScript
The first resource to create is a DynamoDB table to store URL mappings. The table has a single primary key called name and starts with the minimum read and write capacity. Notice how this TypeScript code uses typed resource classes. If you misspell a property name, the TypeScript compiler catches it immediately. If you forget a mandatory option, you get a clear error before deployment even starts.
Define a DynamoDB Table with a Single TypeScript Class
The aws.dynamodb.Table class accepts strongly typed options for key schema, attributes, and capacity. Your IDE provides autocomplete for every field. Running pulumi up shows a preview of the changes and then provisions the table in AWS. This is declarative despite looking like an imperative command.
import * as aws from "@pulumi/aws";
let counterTable = new aws.dynamodb.Table("urls", {
name: "urls",
attributes: [
{ name: "name", type: "S" },
],
hashKey: "name",
readCapacity: 1,
writeCapacity: 1
});
When you run pulumi up, the CLI first shows a preview of the changes to be made. You confirm the update, and the resource is created along with a stack that serves as a container for all the resources of the application. Importantly, this is a declarative definition. If you change readCapacity to 2 and re-run the command, Pulumi detects the exact delta and suggests an in-place update rather than recreating everything from scratch.
> pulumi up
Previewing update (urlshortener):
Type Name Plan
+ pulumi:pulumi:Stack urlshortener create
+ aws:dynamodb:Table urls create
Resources:
+ 2 to create
Do you want to perform this update? yes
Updating (urlshortener):
Type Name Status
+ pulumi:pulumi:Stack urlshortener created
+ aws:dynamodb:Table urls created
Resources:
+ 2 created
Creating an AWS Lambda Function with IAM Role
Creating a Lambda function involves several AWS resources: the function itself, an IAM role that grants the Lambda permission to assume the role, a role policy attachment for Lambda execution access, and environment variables to pass configuration like the DynamoDB table name. In YAML, this means dozens of lines of boilerplate. In TypeScript with Pulumi, the same resources benefit from full IntelliSense, compile-time type checking, and the ability to use language features like JSON.stringify and npm modules directly within the infrastructure definition.
Define a Lambda Function with Full IAM Role and Policies
Create an IAM Role with an assume role policy document, attach the AWSLambdaFullAccess policy, and define the Lambda function. Reference the DynamoDB table name through object properties. The dependsOn option ensures the role policy attachment is created before the Lambda function. All property types are validated by TypeScript at compile time.
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
let policy: aws.iam.PolicyDocument = {
Version: "2012-10-17",
Statement: [{
Action: "sts:AssumeRole",
Principal: { Service: "lambda.amazonaws.com" },
Effect: "Allow",
Sid: "",
}],
};
let role = new aws.iam.Role("lambda-role", {
assumeRolePolicy: JSON.stringify(policy),
});
let fullAccess = new aws.iam.RolePolicyAttachment("lambda-access", {
role: role,
policyArn: aws.iam.AWSLambdaFullAccess,
});
let lambda = new aws.lambda.Function("lambda-get", {
runtime: aws.lambda.NodeJS8d10Runtime,
code: new pulumi.asset.AssetArchive({
".": new pulumi.asset.FileArchive("./app"),
}),
timeout: 300,
handler: "read.handler",
role: role.arn,
environment: {
variables: {
"COUNTER_TABLE": counterTable.name
}
},
}, { dependsOn: [fullAccess] });
Real-World Infrastructure Complexity
A serverless URL shortener is deceptively simple. The initial sketch of Lambda plus DynamoDB ignores critical details: API Gateway must handle HTTP requests and route them to Lambda, S3 must host static files, and IAM policies must be configured for API Gateway to invoke Lambda and Lambda to access DynamoDB. The actual setup involves about 20 AWS resources. YAML templates balloon to hundreds of lines while the application code stays minimal. Pulumi helps manage this complexity through typed abstractions.
Configuring API Gateway and Wiring Lambda Endpoints
API Gateway is one of the most complex AWS resources to configure. It requires stages, deployments, REST endpoints, resource paths, and methods all correctly wired together. In CloudFormation or Terraform, this results in hundreds of lines of YAML. The same complexity exists with Pulumi at the raw resource level, but the real power comes when you encapsulate this complexity into reusable components.
Create a Custom Lambda Component to Hide Infrastructure Boilerplate
Build a Lambda component that extends pulumi.ComponentResource. This component encapsulates all child resources: the IAM role, the role policy attachment, and the Lambda function itself. Consumers provide only the essential options: the code path, the handler file, and environment variables. The abstraction reduces the call site from 20+ lines to just 7 lines.
// lambda.ts - Reusable Custom Component
export interface LambdaOptions {
readonly path: string;
readonly file: string;
readonly environment?: pulumi.Input<{
[key: string]: pulumi.Input;
}>;
}
export class Lambda extends pulumi.ComponentResource {
public readonly lambda: aws.lambda.Function;
constructor(name: string,
options: LambdaOptions,
opts?: pulumi.ResourceOptions) {
super("my:Lambda", name, opts);
const role = new aws.iam.Role(...);
const fullAccess = new aws.iam.RolePolicyAttachment(...);
this.lambda = new aws.lambda.Function(`${name}-func`, {
runtime: aws.lambda.NodeJS8d10Runtime,
code: new pulumi.asset.AssetArchive({
".": new pulumi.asset.FileArchive(options.path),
}),
timeout: 300,
handler: `${options.file}.handler`,
role: role.arn,
environment: {
variables: options.environment
}
}, { dependsOn: [fullAccess], parent: this });
}
}
After building the component, the consumer code becomes dramatically simpler. Only the essential options remain, while all the IAM and Lambda machinery is hidden behind the abstraction. The same pattern applies to API Gateway through an Endpoint component that accepts a Lambda function and a URL path.
import { Lambda } from "./lambda";
const func = new Lambda("lambda-get", {
path: "./app",
file: "read",
environment: {
"COUNTER_TABLE": counterTable.name
},
});
const api = new Endpoint("urlapi", {
path: "/{proxy+}",
lambda: func.lambda
});
Custom components are reusable across teams, projects, and environments. When you inspect a Pulumi deployment preview, the component hierarchy is visible, showing the parent component and all its child resources in a tree structure. This makes it easy to understand the resource ownership and the structure of your infrastructure at a glance.
Using the Pulumi Cloud Library for High-Level Abstractions
Pulumi provides a standard component library under the @pulumi/cloud-aws package. The components from this library offer high-level abstractions specifically designed for serverless applications. With the cloud library, you can define a full serverless application including DynamoDB tables, Lambda implementations, API Gateway routing, and S3 static file hosting in a single TypeScript file. The implementation code of AWS Lambdas can even be written inline as TypeScript arrow functions, blending the application logic seamlessly with the infrastructure definition.
Deploy the Full URL Shortener with the Pulumi Cloud Library
The @pulumi/cloud-aws package provides Table, API, and static file serving abstractions. Lambda implementations are written as inline TypeScript arrow functions that have access to the table object through closure. The entire application including DynamoDB, two Lambdas, API Gateway, and S3 static hosting fits in a single file with inline handler code.
import * as aws from "@pulumi/cloud-aws";
let urlTable = new aws.Table("urls", "name");
let endpoint = new aws.API("urlshortener");
// Serve all static files from www directory to root
endpoint.static("/", "www");
// GET /url/{name} redirects to target URL
endpoint.get("/url/{name}", async (req, res) => {
let name = req.params["name"];
let value = await urlTable.get({name});
let url = value && value.url;
if (url) {
res.setHeader("Location", url);
res.status(301);
res.end("");
} else {
res.status(404);
res.end("");
}
});
// POST /url registers a new URL with a short name
endpoint.post("/url", async (req, res) => {
let url = req.query["url"];
let name = req.query["name"];
await urlTable.insert({ name, url });
res.json({ shortenedURLName: name });
});
export let endpointUrl = endpoint.publish().url;
Inline Lambda Handlers
The cloud library enables writing Lambda handler code directly inline using TypeScript arrow functions. The handler has access to variables through closure, eliminating the need to pass environment variables manually.
One File, Complete Stack
DynamoDB tables, Lambda functions, API Gateway endpoints, S3 static hosting, and IAM policies are all defined in a single TypeScript file with full IntelliSense and compile-time validation.
Avoiding Cloud Vendor Lock-In with Cross-Cloud Abstractions
Vendor lock-in is a significant concern when building applications heavily reliant on managed cloud services and serverless architectures. Pulumi provides a mechanism to address this through its cloud abstraction layer. In the code example above, only the import statement ties the application to AWS. By changing the import to @pulumi/cloud and replacing all aws. references with cloud., the same application can be deployed to any supported cloud provider by simply changing a stack configuration value.
// Change this single import to switch cloud providers
import * as cloud from "@pulumi/cloud";
// Then use cloud.* instead of aws.* everywhere
let urlTable = new cloud.Table("urls", "name");
let endpoint = new cloud.API("urlshortener");
// Stack configuration specifies the provider
// Pulumi.{stack}.yaml:
// config:
// cloud:provider: aws
Pulumi chooses the level of abstraction. You can work directly with raw cloud provider resources for maximum control and flexibility, or you can opt into higher-level cross-cloud abstractions where appropriate. You can even mix both approaches within the same program. This layered architecture gives teams the freedom to choose the right abstraction level for each component of their infrastructure.
Pulumi Abstraction Layers
| Layer | Package | Use Case |
|---|---|---|
| Raw Cloud Resources | @pulumi/aws | Full control over every AWS resource property |
| Custom Components | Your own class | Encapsulate IAM roles, policies, and common Lambda patterns |
| Cloud Library (AWS) | @pulumi/cloud-aws | High-level serverless abstractions with inline Lambda handlers |
| Cross-Cloud Library | @pulumi/cloud | Deploy the same code to AWS, Azure, or GCP by changing config |
Comparison of Infrastructure as Code Tools
Before adopting Pulumi, it helps to understand how it compares to the existing IaC tooling landscape. Each tool has its place, but Pulumi is unique in combining the power of real programming languages with full cloud resource coverage.
| Tool | Language | Cloud Support | Abstractions | Type Safety |
|---|---|---|---|---|
| AWS CloudFormation | YAML / JSON | AWS only | None | No |
| HashiCorp Terraform | HCL | Multi-cloud | Modules | No |
| Serverless Framework | YAML | Multi-cloud (AWS strongest) | Limited | No |
| AWS SAM | YAML | AWS only | Limited | No |
| Pulumi | TypeScript, Python, Go | Multi-cloud | Full language features | Yes |
Important Consideration
As of this writing, the high-level cloud abstractions from @pulumi/cloud only support TypeScript. The team is actively working on bridging this to Python and Go. Furthermore, you should evaluate whether vendor lock-in is genuinely a concern for your use case. Generic abstractions may reduce cloud-specific optimization opportunities and constrain you to the lowest common denominator across providers.
Desired Properties of a Modern Infrastructure Tool
Going through the existing landscape reveals the properties that a perfect infrastructure tool should possess. Pulumi was designed with these exact requirements in mind and delivers on every single one.
| Property | How Pulumi Delivers |
|---|---|
| Reproducible deployments | Desired state engine compares current vs target and applies deltas |
| Fully scriptable | No GUI interaction needed; all operations run from CLI or CI/CD |
| Desired state configuration | Define what you want, engine calculates how to achieve it |
| Multi-cloud support | AWS, Azure, GCP, Kubernetes all natively supported |
| Universal resource coverage | Raw cloud provider APIs, custom components, and cloud library |
| Succinct and readable | Components reduce boilerplate; inline handlers merge code and infra |
| Real programming language | TypeScript, Python, Go with IntelliSense, types, and npm/pip modules |
Frequently Asked Questions
How does Pulumi compare to Terraform for cloud automation?
Both handle desired state configuration and support multiple cloud providers. The key difference is language: Terraform uses HCL, a domain-specific language, while Pulumi uses TypeScript, Python, and Go. Pulumi provides IntelliSense, compile-time type checking, reusable class abstractions, and the ability to use npm packages directly in infrastructure code.
Can I use Pulumi with existing AWS resources managed by CloudFormation?
Yes, Pulumi can import existing resources into its state management. You can also co-exist with CloudFormation stacks. Pulumi does not require replacing all existing infrastructure at once. You can incrementally adopt it for new resources while existing YAML-based infrastructure continues to operate normally.
Does using real code for infrastructure increase the risk of bugs?
The opposite is true. TypeScript catches configuration errors at compile time that YAML would only surface as runtime deployment failures. Type-checked resource properties prevent misspellings and wrong types. The preview feature shows exactly what will change before any modification is applied, giving you the same safety net as plan files.
What happens if a Pulumi deployment fails midway through?
Pulumi tracks which resources were successfully created or updated. When you rerun the deployment, it detects which resources already exist in the desired state and only provisions the remaining ones. Unlike imperative CLI scripts, there is no need to manually repair a partially complete deployment.
Can I use Pulumi for non-serverless workloads like EC2 instances or VPCs?
Yes. Pulumi supports every AWS resource type including EC2, VPC, RDS, ECS, EKS, and more. The serverless-focused cloud library is an optional convenience layer. You can define raw low-level resources with full control just as you would in CloudFormation or Terraform, but with the benefits of a typed programming language.
Need Help Modernizing Your Cloud Infrastructure?
Our cloud experts can help you migrate from YAML-based templates to TypeScript with Pulumi, build reusable infrastructure components, and deploy multi-cloud serverless architectures optimized for your workloads.
