To get started with Pulumi and TypeScript, create a cloud-specific project, declare a resource in index.ts, review pulumi preview, and deploy with pulumi up. This tutorial walks through that loop on AWS and Google Cloud, including credentials, stack outputs, an update, and cleanup.
This guide deploys a private bucket and a demo text file to AWS, then repeats the exercise on Google Cloud. The file is the application artifact for this first exercise; it is not a publicly hosted web application. You can complete either cloud section independently.
The original URL is preserved. This November 2026 revision replaces the earlier VM example with a smaller deployment and separates the two cloud projects so their state is easier to follow.
Choose your first Pulumi example
| Goal | Start here | Resources declared |
|---|---|---|
| Learn Pulumi on AWS | Part 1: create an AWS project | S3 bucket, public-access block, and text object |
| Learn Pulumi on Google Cloud | Part 2: repeat with Google Cloud | Cloud Storage bucket and text object |
| Understand projects, stacks, and state | What Pulumi manages | Concepts used in both examples |
Complete one provider first. Both examples keep the file private and use an authenticated CLI download to verify the result.
What Pulumi manages
A Pulumi program declares resources using a supported language. Provider packages translate resource operations to cloud APIs. Pulumi compares the program with recorded infrastructure state and proposes the changes needed for the selected stack.
Three terms matter before your first command:
| Term | Meaning in this guide |
|---|---|
| Project | A directory containing a Pulumi program and Pulumi.yaml |
| Stack | One configured instance of the project, here named dev |
| Backend | Where Pulumi records stack state |
A stack is not a cloud selector. An AWS program still declares AWS resources if you create a stack called gcp-demo. You must change the program or use a separate project to declare Google Cloud resources.
The official Pulumi stacks documentation describes stacks as isolated, configurable instances of a project. Use stack configuration for environment differences; use provider resources to choose what infrastructure exists.
Pulumi documents the relationship between state and backend options in State and Backends. Cloud credentials and state backend credentials serve different purposes.
Prerequisites and version baseline
Install Node.js 22 LTS, npm, and Pulumi CLI v3 using the official installation instructions. On macOS with Homebrew, the CLI installation is:
brew install pulumi/tap/pulumi pulumi version node --version npm --version
The examples below pin @pulumi/pulumi to 3.150.0, the AWS provider to 7.52.1, and the Google Cloud provider to 10.2.0. These are explicit package baselines, not an instruction to treat them as the newest releases. Preserve each generated package-lock.json and use npm ci to reproduce its installed dependency tree.
Both TypeScript programs were checked locally against the pinned packages with tsc --noEmit. They were not deployed to AWS or Google Cloud for this revision, so the deployment and download commands describe the expected workflow rather than a recorded cloud run.
Use a sandbox cloud account or project. The identity you use must be allowed to create and delete the demonstrated buckets and objects, manage the configured bucket access settings, and inspect them. Cloud storage and API operations can incur charges; this guide does not promise a free deployment.
Choose a Pulumi backend before creating the projects:
pulumi login
This selects the managed Pulumi Cloud flow. If you prefer local state for this exercise, use pulumi login --local instead. Keep access to that state: deleting a project directory is not a substitute for destroying its cloud resources.
Part 1: create an AWS project
Install and authenticate the AWS CLI using your organization's supported method. For an existing IAM Identity Center profile, the flow might be:
aws sso login --profile sandbox export AWS_PROFILE=sandbox aws sts get-caller-identity
The profile name is an example; substitute your configured profile. Check the returned account before deploying. Do not paste access keys into TypeScript files.
Create the project in a new directory:
mkdir pulumi-start-aws cd pulumi-start-aws pulumi new aws-typescript --name pulumi-start-aws --stack dev npm install --save-exact @pulumi/pulumi@3.150.0 @pulumi/aws@7.52.1 pulumi config set aws:region eu-west-1
The AWS TypeScript template creates the project files and the stack. Review its interactive prompts and the selected backend. Do not run pulumi stack init dev again after the template has created it.
Declare a private bucket and a file
Replace the complete index.ts with the following downloadable AWS program (save it as index.ts):
import * as aws from "@pulumi/aws"; const bucket = new aws.s3.Bucket("demo-files", { tags: { Project: "pulumi-start", Environment: "dev" }, }); new aws.s3.BucketPublicAccessBlock("demo-files-private", { bucket: bucket.id, blockPublicAcls: true, blockPublicPolicy: true, ignorePublicAcls: true, restrictPublicBuckets: true, }); const message = new aws.s3.BucketObjectv2("demo-message", { bucket: bucket.id, key: "hello.txt", content: "Hello from Pulumi and TypeScript.\n", contentType: "text/plain", }); export const bucketName = bucket.id; export const objectKey = message.key;
The logical name demo-files identifies the resource in Pulumi. We leave the physical bucket name to the provider's generated naming rather than requiring every reader to compete for the same globally unique name.
The resource definitions follow the provider references for Bucket, BucketPublicAccessBlock, and BucketObjectv2. Public access is explicitly blocked; this is not a website endpoint.
Preview and deploy
npx tsc --noEmit pulumi preview pulumi up
Review the preview before confirming the update. It should propose the stack and the three declared resources. If it proposes unrelated updates or deletions, stop and check the directory, selected stack, and backend.
After deployment, inspect the outputs:
pulumi stack output
Then verify the object with the cloud identity used for deployment:
DEMO_BUCKET=$(pulumi stack output bucketName) aws s3 cp "s3://${DEMO_BUCKET}/hello.txt" -
Expected file contents:
Hello from Pulumi and TypeScript.
The generated bucket name varies. A successful authenticated download is the intended check; an anonymous browser request should not be the way you access this private artifact.
Make a change and inspect the difference
Edit the content string in index.ts, then run pulumi preview. The meaningful change should concern the object, rather than a replacement of every resource. Apply it with pulumi up and repeat the authenticated download.
bucket.id is a Pulumi output: its value may not be known during preview. Passing it into the object's bucket input tells Pulumi about the dependency. You do not need to convert it into an ordinary JavaScript string first. See Inputs and Outputs for composing values and dependencies.
Clean up AWS
While still in pulumi-start-aws, confirm that pulumi stack identifies this exercise's stack, then run:
pulumi destroy
Review the deletion proposal and confirm only for the disposable exercise resources. Pulumi can remove the managed object before deleting the bucket because the object depends on the bucket.
We have not enabled forced removal of unknown bucket contents. If you uploaded extra objects manually, inspect them before deciding what to remove. Verify in AWS that the exercise bucket is gone. Keep the state until cleanup succeeds.
Part 2: repeat with Google Cloud
Use a Google Cloud project with billing configured where required and permissions for the demonstrated storage operations. Install the Google Cloud CLI, then authenticate both the CLI and application libraries:
gcloud auth login gcloud auth application-default login gcloud config set project YOUR_PROJECT_ID
Replace YOUR_PROJECT_ID. The active CLI login and Application Default Credentials can identify different principals, so check both if operations behave differently. Pulumi's Google Cloud getting-started guide describes the provider setup.
From the parent directory of pulumi-start-aws, create a separate project:
mkdir pulumi-start-gcp cd pulumi-start-gcp pulumi new gcp-typescript --name pulumi-start-gcp --stack dev npm install --save-exact @pulumi/pulumi@3.150.0 @pulumi/gcp@10.2.0 pulumi config set gcp:project YOUR_PROJECT_ID
Using separate projects is intentional. Destroying the AWS stack will not also destroy the Google Cloud exercise.
Declare the Google Cloud resources
Replace the complete index.ts with the following downloadable Google Cloud program (save it as index.ts):
import * as pulumi from "@pulumi/pulumi"; import * as gcp from "@pulumi/gcp"; const bucket = new gcp.storage.Bucket("demo-files", { location: "EU", uniformBucketLevelAccess: true, publicAccessPrevention: "enforced", }); const message = new gcp.storage.BucketObject("demo-message", { bucket: bucket.name, name: "hello.txt", source: new pulumi.asset.StringAsset("Hello from Pulumi and TypeScript.\n"), contentType: "text/plain", }); export const bucketName = bucket.name; export const objectName = message.name;
Here EU is an explicit bucket location, independent of a provider region setting. Choose an allowed location for your project before deployment; the provider documents that the bucket location cannot be changed after creation.
Uniform bucket-level access uses IAM rather than per-object ACLs, and public access prevention is explicitly enabled. The BucketObject reference documents object creation from an asset. This configuration is a private storage exercise, not a complete organization-wide storage policy.
Deploy and verify
npx tsc --noEmit pulumi preview pulumi up pulumi stack output
Expect the stack and two declared resources in a fresh deployment. Inspect the uploaded object using the authenticated CLI:
DEMO_GCP_BUCKET=$(pulumi stack output bucketName) gcloud storage cat "gs://${DEMO_GCP_BUCKET}/hello.txt"
The expected contents are the same greeting as in the AWS exercise. If the download fails but the deployment succeeded, check whether the CLI and Pulumi used different credentials.
To finish, confirm the selected stack in this directory and run pulumi destroy. Review the deletion proposal and verify the bucket is removed. Storage retention, soft-delete settings, or organization policy may affect retained data and billing after deletion; inspect the policy that applies to your project.
Troubleshooting a first deployment
| Symptom | What to inspect |
|---|---|
| Provider reports missing credentials | AWS profile or Google Application Default Credentials, not just Pulumi backend login |
| Access denied | Actual identity, target account/project, and permissions for the rejected operation |
| Bucket name conflict | A hardcoded name added to the example; return to generated naming or choose a unique name |
| Organization policy rejects a location or access setting | Your sandbox's allowed configuration; do not disable policies to force the example through |
| Preview targets surprising resources | Current directory, pulumi stack, backend, and configuration |
| Deletion fails because a bucket is not empty | Objects added outside the program, versions, holds, or retention policies |
| TypeScript import/type error | Installed package versions, lock file, and whether index.ts matches the selected provider |
How this differs from Terraform, and where to go next
Pulumi lets you use TypeScript expressions and ordinary package tooling to describe infrastructure. That is useful when your team benefits from language-level abstractions. It also means maintaining a Node.js dependency tree alongside provider and cloud API changes.
Terraform uses HCL for its usual configuration workflow. Both tools need infrastructure state, provider credentials, and a plan/update review process. Pulumi supports self-managed state; Terraform also supports remote backends. State management is not a simple “cloud versus local” distinction.
These examples demonstrate deployment mechanics, not a production application. They omit CI identity federation, centralized policy, recovery requirements, and shared environment governance. After completing the loop locally, move one project into version control with its lock file, choose a team backend, and automate previews before adding more infrastructure.
If you are comparing TypeScript infrastructure tools, the AWS CDK RDS and EC2 example shows a database workload with subnets, security groups, and generated credentials. For a smaller CDK networking task, see importing an existing VPC with fromLookup. These are different exercises, rather than a feature-for-feature comparison with the storage programs here.
Quick Recap
How do I get started with Pulumi?
Install the Pulumi CLI and a supported language runtime, configure your cloud credentials, create a provider-specific project, and run pulumi preview before pulumi up. Start with a small disposable resource and learn how to destroy it.
What is a Pulumi stack?
A stack is an independently configured instance of a Pulumi project with its own state, such as dev or production. Creating another stack does not change which cloud providers your program uses.
Do I need Pulumi Cloud?
No. Pulumi Cloud is a managed backend option. Pulumi also supports self-managed backends, including local storage. The backend stores infrastructure state; your AWS or Google Cloud credentials authorize cloud operations.
Can the same Pulumi program deploy to AWS and GCP?
A program can use both providers, but AWS and Google Cloud resource APIs remain different. This beginner guide uses separate projects so each deployment has an independent lifecycle.
Comments
Share your thoughts and questions below