Pulumi Secrets & Stack Structure Best Practices

In this article, we delve into Pulumi secrets management and stack structure best practices for production systems. You will learn how to secure sensitive configurations using Pulumi's encryption, establish robust multi-environment stack organizations, and leverage stack references for safe output sharing, ensuring scalable and compliant infrastructure deployments.

Ahmet Çelik

10 min read
0

/

Pulumi Secrets & Stack Structure Best Practices

Most teams begin their Infrastructure as Code (IaC) journey by placing secrets directly into configuration files or environment variables with minimal control. But this common practice inevitably leads to significant security vulnerabilities, operational overhead, and compliance nightmares when scaled across multiple environments and projects.


TL;DR Box


  • Encrypt sensitive configuration data using Pulumi's built-in secrets management or integrate with a dedicated secret backend.

  • Adopt a clear, hierarchical stack structure (e.g., `org/project/environment/stack`) to manage infrastructure lifecycle effectively.

  • Leverage Pulumi stack references to securely share outputs and dependencies between stacks without duplicating secrets.

  • Implement robust IAM policies to control access to Pulumi state files and secret decryption keys.

  • Design your CI/CD pipelines to interact with encrypted secrets using least-privilege principles, preventing exposure.


The Problem: When IaC Secrets and Stacks Go Sideways


In 2026, relying on unencrypted secrets in IaC configurations remains a critical security lapse. I've observed countless teams struggle with this, often resorting to hardcoding API keys, database credentials, or sensitive network configurations directly into `Pulumi.dev.yaml` files. This approach might seem convenient for quick development, but it rapidly escalates into a major risk factor in production.


Consider a backend team managing 15 distinct microservices across `development`, `staging`, and `production` environments. If each service's Pulumi project has its own `Pulumi..yaml` file containing plain-text database credentials, API keys, and third-party tokens, you're looking at 45 separate files housing sensitive data. Not only does this exponentially increase the attack surface, but it also creates an unmanageable sprawl of secrets. Auditing which secrets are used where, rotating them effectively, and ensuring consistent access controls becomes a full-time job. Teams commonly report spending 30-50% more time on security audits and incident response directly attributable to poor secret management in IaC, compared to those with robust strategies. This scenario is precisely where robust Pulumi secrets management and deliberate stack structure become indispensable.


How It Works: Securing Your IaC with Pulumi Secrets


Pulumi provides first-class support for secrets, treating them as encrypted configuration values that are never stored in plain text within your state file. When you mark a configuration value as a secret, Pulumi encrypts it before storing it and decrypts it only when required during an update or preview operation. This mechanism fundamentally changes how you handle sensitive data in your infrastructure definitions.


Pulumi Secrets Encryption Mechanisms


Pulumi offers flexibility in how your secrets are encrypted:


  1. Pulumi Service Backend (Default): For projects using the Pulumi Service for state management, secrets are encrypted using a service-managed key. This is often the simplest and most secure option for many teams, as Pulumi handles key rotation and access control for the encryption key itself.

  2. Cloud-Specific Key Management Services (KMS): You can configure Pulumi to use your own keys in services like AWS KMS, Azure Key Vault, or Google Cloud Secret Manager. This provides maximum control over the encryption key lifecycle, which is crucial for organizations with stringent compliance requirements.

  3. Passphrase-based Encryption: For local state files or specific offline scenarios, Pulumi can encrypt secrets using a passphrase you provide. While offering local control, this places the burden of passphrase management and security directly on your team, which can be challenging at scale.


When you interact with a secret, Pulumi ensures it's decrypted only within the Pulumi engine process, minimizing exposure. The encrypted value lives in your `Pulumi..yaml` file.


// project1/index.ts - Defining a resource with a secret
import * as aws from "@pulumi/aws";
import * as pulumi from "@pulumi/pulumi";

// Retrieve a secret value from configuration.
// This value will be encrypted in Pulumi.dev.yaml.
const config = new pulumi.Config();
const dbPassword = config.requireSecret("dbPassword");

// Create an RDS instance, using the secret for its master password.
// The password will be handled securely by Pulumi.
const db = new aws.rds.Instance("my-app-db-2026", {
    engine: "postgresql",
    engineVersion: "13.4",
    instanceClass: "db.t3.micro",
    allocatedStorage: 20,
    username: "admin",
    password: dbPassword, // Using the decrypted secret here
    skipFinalSnapshot: true, // For example purposes
});

// Export the database endpoint (but not the password itself!)
export const dbEndpoint = db.endpoint;

The code above defines an AWS RDS instance and fetches its master password from the Pulumi configuration. By calling `config.requireSecret("dbPassword")`, we explicitly tell Pulumi to treat this value as sensitive, ensuring it's encrypted in the stack configuration file and only decrypted during `pulumi up`.


How It Works: Pulumi Stack Structure Best Practices


Effective stack structure goes beyond just environment separation; it's about defining clear ownership, managing blast radius, and enabling secure cross-stack communication. A well-organized structure reduces cognitive load and allows teams to scale their infrastructure without tangled dependencies.


Hierarchical Stack Organization


A recommended approach for multi-environment, multi-project organizations is a hierarchical structure: `organization/project/environment/stack`.


  • Organization: The top-level grouping, often mapping to your company or a major division.

  • Project: A logical unit of infrastructure, typically aligning with a service, application, or shared platform component (e.g., `vpc-network`, `api-gateway`, `data-processing`).

  • Environment: Represents a deployment stage, such as `dev`, `staging`, `production`, `qa`, etc.

  • Stack: The specific instance of infrastructure within an environment.


This structure allows for fine-grained control and clear separation. For instance, `myorg/platform/production/network` might define your production VPC, while `myorg/backend-service/staging/app-server` defines a specific application's servers in staging.


Securely Sharing Data with Stack References


The challenge with segregated stacks often involves sharing outputs, such as a VPC ID, subnet IDs, or a database endpoint, from one stack to another without re-provisioning or duplicating configuration. Pulumi's Stack References solve this elegantly and securely.


A stack reference allows a Pulumi program to retrieve the outputs of another stack programmatically. This is crucial when the referenced stack output contains sensitive information that should not be manually copied. When a stack output is itself a Pulumi `Secret`, the stack reference will also return it as a `Secret`, maintaining its encrypted state across stack boundaries.


// project2/index.ts - Referencing an output from project1
import * as aws from "@pulumi/aws";
import * as pulumi from "@pulumi/pulumi";

// Reference the stack that created the database (e.g., 'dev' stack of 'project1').
// Format: <org>/<project>/<stack-name>
const dbStackRef = new pulumi.StackReference("myorg/project1/dev");

// Retrieve the database endpoint from project1's dev stack.
// If dbEndpoint was exported as a Secret in project1, it remains a Secret here.
const referencedDbEndpoint = dbStackRef.getOutput("dbEndpoint");

// Example: Create an EC2 instance that needs to connect to the database.
// The database endpoint is securely passed via the stack reference.
const appServer = new aws.ec2.Instance("app-server-2026", {
    ami: "ami-0abcdef1234567890", // Placeholder AMI ID for us-east-1 (2026)
    instanceType: "t3.micro",

WRITTEN BY

Ahmet Çelik

Former AWS Solutions Architect, 8 years in cloud and infrastructure. Computer Engineering graduate, Bilkent University. Lead writer for AWS, Terraform and Kubernetes content.Read more

Responses (0)

    Hottest authors

    View all

    Ahmet Çelik

    Lead Writer · ex-AWS Solutions Architect, 8 yrs · AWS, Terraform, K8s

    Alp Karahan

    Contributor · MongoDB certified, NoSQL specialist · MongoDB, DynamoDB

    Ayşe Tunç

    Lead Writer · Engineering Manager, ex-Meta, Google · System Design, Interviews

    Berk Avcı

    Lead Writer · Principal Backend Eng., API design · REST, GraphQL, gRPC

    Burak Arslan

    Managing Editor · Content strategy, developer marketing

    Cansu Yılmaz

    Lead Writer · Database Architect, 9 yrs Postgres · PostgreSQL, Indexing, Perf

    Popular posts

    View all
    Ahmet Çelik
    ·

    S3 vs EFS vs EBS for Backend Workloads 2026: A Deep Dive

    S3 vs EFS vs EBS for Backend Workloads 2026: A Deep Dive
    Ahmet Çelik
    ·

    CloudFront vs ALB vs API Gateway: Choosing the Right API Front Door

    CloudFront vs ALB vs API Gateway: Choosing the Right API Front Door
    Deniz Şahin
    ·

    Avoiding Serverless Cost Surprises: An Observability Checklist

    Avoiding Serverless Cost Surprises: An Observability Checklist
    Zeynep Aydın
    ·

    SOC 2 Technical Controls Checklist for Startups: A Deep Dive

    SOC 2 Technical Controls Checklist for Startups: A Deep Dive
    Zeynep Aydın
    ·

    WebAuthn Recovery & Device Sync Pitfalls

    WebAuthn Recovery & Device Sync Pitfalls
    Ahmet Çelik
    ·

    Pulumi Secrets & Stack Structure Best Practices

    Pulumi Secrets & Stack Structure Best Practices