instanceType: "t3.micro" in your index.ts, your Production stack is doomed to crash under heavy load. To deploy the same code across multiple environments, we must externalize these values using the Pulumi Configuration system. Furthermore, we must ensure passwords are encrypted natively so they are never committed to Git in plain text.1. The Pulumi Config System#
In Terraform, you externalized values using variables.tf and terraform.tfvars.
In Pulumi, configuration is managed by the Pulumi.<stack-name>.yaml file. You do not edit this file by hand; you interact with it entirely through the Pulumi CLI.
Setting Configuration Values#
Ensure you are in the dev stack:
pulumi stack select devLet’s set a configuration variable for the AWS instance type we want the dev stack to use:
pulumi config set instanceSize t3.microIf you open Pulumi.dev.yaml, you will see:
config:
pulumi-first-project:instanceSize: t3.microNow, switch to the prod stack and set a different value:
pulumi stack select prod
pulumi config set instanceSize m5.largeThe Pulumi.prod.yaml file now contains m5.large.
Reading Configuration in TypeScript#
To consume these values in your index.ts file, you instantiate the pulumi.Config class.
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
// 1. Initialize the Config object
const config = new pulumi.Config();
// 2. Read the value. If the user forgot to set it, default to t3.micro
const size = config.get("instanceSize") || "t3.micro";
// OR, throw an error if the value is strictly required:
// const size = config.require("instanceSize");
// 3. Inject the configuration into the resource
const webServer = new aws.ec2.Instance("web-server", {
ami: "ami-0fc5d935ebf8bc3bc",
instanceType: size, // <--- Dynamically injected!
});When you run pulumi up, Pulumi will automatically inject t3.micro if you are in the dev stack, and m5.large if you are in the prod stack. You have achieved true multi-environment portability.
2. Secrets Management (The Pulumi Advantage)#
Handling secrets in Terraform is notoriously difficult. If you pass a database password into a .tfvars file, it lives in plain text on your hard drive. If you commit it to Git, you are compromised. Many teams have to deploy heavy third-party tools like HashiCorp Vault just to solve this.
Pulumi solves this out-of-the-box. Every Pulumi stack has its own cryptographic encryption key managed securely by the Pulumi Service.
Setting a Secret#
To set a secret, simply add the --secret flag to the CLI command:
pulumi config set dbPassword SuperSecretP@ssword! --secretIf you open your Pulumi.dev.yaml file, you will NOT see the plain text password. Instead, you will see something like this:
config:
pulumi-first-project:dbPassword:
secure: v1:1234abcd5678efgh:xyz9876543210The value is deeply encrypted using an AES-256 GCM authenticated encryption cipher. It is completely safe to commit this Pulumi.dev.yaml file to your public GitHub repository!
Reading a Secret in TypeScript#
Reading a secret is just as easy as reading standard config, but you use the getSecret or requireSecret methods.
const config = new pulumi.Config();
// Pulumi knows this is a secret and treats it specially
const dbPassword = config.requireSecret("dbPassword");
const database = new aws.rds.Instance("my-database", {
engine: "postgres",
instanceClass: "db.t3.micro",
allocatedStorage: 20,
username: "admin",
// Inject the decrypted secret at runtime!
password: dbPassword,
});During the pulumi up execution, the Pulumi CLI reaches out to the Pulumi Service, decrypts the secure: string in memory, passes the plain text password over TLS directly to the AWS API, and then purges the plain text from memory.
Furthermore, Pulumi tracks the lineage of this secret. If you attempt to run console.log(dbPassword) in your TypeScript code, Pulumi will mask the output in your terminal logs ([secret]), ensuring you cannot accidentally leak it to your CI/CD console.
3. Strongly Typed Configuration#
Because we are in TypeScript, we can take configuration a step further. Instead of reading simple strings, what if our configuration requires a complex JSON object? (e.g., an array of CIDR blocks to whitelist in a Security Group).
# We can set complex objects in the config
pulumi config set whitelist '["10.0.0.0/16", "192.168.1.0/24"]'In your code, you can parse this directly into a typed array:
const config = new pulumi.Config();
// requireObject parses the JSON string into a native JavaScript object
const cidrBlocks = config.requireObject<string[]>("whitelist");
const sg = new aws.ec2.SecurityGroup("web-sg", {
ingress: [{
protocol: "tcp",
fromPort: 443,
toPort: 443,
cidrBlocks: cidrBlocks, // Strongly typed array!
}],
});Troubleshooting & Common Errors#
Missing required configuration variable- Root Cause: Your TypeScript code calls
config.require("myVar"), but you haven’t runpulumi config set myVar <value>for the currently active stack. - Solution: Set the variable, or change the code to
config.get("myVar")to make it optional.
- Root Cause: Your TypeScript code calls
Error decrypting secret- Root Cause: This usually happens if you copy the
secure: v1:xxxstring fromPulumi.dev.yamland manually paste it intoPulumi.prod.yaml. Every stack has a unique encryption key; you cannot share encrypted strings across stacks. - Solution: You must run
pulumi config set dbPassword <value> --secretexplicitly in theprodstack.
- Root Cause: This usually happens if you copy the
Conclusion & Next Steps#
You have successfully parameterized your infrastructure. Your Staging and Production architectures can diverge completely based on the Pulumi.<stack>.yaml configuration, while sharing the exact same underlying logic.
Furthermore, you have eliminated the risk of plain text credential leaks by leveraging Pulumi’s native KMS encryption.
However, Pulumi is an asynchronous engine. When you write bucket.id, the bucket hasn’t actually been created yet! How does TypeScript handle this temporal paradox?
In Episode 5: Inputs, Outputs, and Promises, we will conquer Pulumi’s most challenging concept for new developers: understanding the Output<T> type and asynchronous execution.

