Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Pulumi: Infrastructure as Code in Real Languages

By Kokil Thapa | Last reviewed: August 2026

Pulumi: Infrastructure as Code in Real Languages solves a persistent friction point for application developers who need to manage cloud resources but resist learning domain-specific configuration languages like HCL. Instead of context-switching between your application code and a separate declarative syntax, Pulumi allows you to define infrastructure using TypeScript, Python, Go, C#, or Java with the same IDEs, linters, and testing frameworks you already use. For full-stack engineers building production Laravel APIs or complex eCommerce platforms, this means infrastructure logic can live alongside business logic, sharing types, validation, and abstractions without leaving your primary development environment.

How does Pulumi: Infrastructure as Code in Real Languages actually work?

Understanding the execution model prevents the most common mistakes new users make when treating Pulumi like a scripting library. Pulumi is not an SDK that makes direct API calls to AWS or Azure at runtime; it is a two-phase system that separates evaluation from deployment. When you run pulumi up, the host language runtime (Node.js, Python, etc.) executes your program first. This evaluation phase constructs an in-memory resource graph representing your intended state. Only after this graph is fully built does the Pulumi engine compare it against the stored state file and issue the necessary create, update, or delete operations to the cloud provider.

Phase 1: Language HostYour TS / Python / Go CodeConstruct Resource GraphSerialize Desired StatePhase 2: Pulumi EngineDiff Against State FileGenerate Execution PlanApply Cloud Provider APIsRPC
Pulumi execution model: language host evaluates code to build a resource graph, then the engine diffs and applies changes via cloud APIs

This architecture has practical consequences. You cannot use runtime values from cloud resources (like an auto-generated IP address) to control flow during the evaluation phase, because those values do not exist until after the plan is applied. Pulumi handles this through Output<T> types, which represent future values. You transform these outputs using .apply() rather than awaiting them directly. On real client projects, I have seen developers attempt to use async/await on resource properties, which returns a pending promise rather than the resolved value. Always treat resource outputs as opaque futures that propagate through the dependency graph.

State management and backend options

Pulumi stores the current state of your infrastructure separately from your code. By default, the managed Pulumi Cloud service handles this, but for teams requiring self-hosted state (common in regulated industries or Nepal-based clients avoiding foreign data residency concerns), you can configure alternative backends. Supported options include S3 + DynamoDB, Azure Blob Storage, Google Cloud Storage, or a local filesystem for development. The state file contains sensitive metadata, so encryption at rest is mandatory. When working on server security hardening, ensure your state backend has strict IAM policies and versioning enabled to support rollback.

Why choose Pulumi over Terraform for application developers?

The decision between Pulumi and Terraform often comes down to team composition and complexity requirements. Terraform’s HCL is purpose-built for infrastructure and excels at straightforward provisioning where configuration is largely static. However, when infrastructure logic requires iteration, conditional branching, abstraction, or integration with external validation systems, HCL’s limitations become apparent. Pulumi removes these constraints by using real languages with mature ecosystems.

CriterionTerraform (HCL)Pulumi (Real Languages)
Language paradigmDeclarative DSL, limited expressionsImperative + declarative, full language features
Abstraction & reuseModules, variable blocksClasses, functions, packages, inheritance
Type safetyBasic type checking at plan timeStatic typing (TS, Go, C#) with IDE support
TestingIntegration tests via terratest/kitchenUnit tests with Jest/pytest/go-test natively
EcosystemProvider registry, module registryNPM/PyPI/GitHub Packages + provider registry
Learning curveNew syntax, new toolingExisting language skills transfer directly
Best fitPlatform teams, pure infra opsFull-stack devs, app-coupled infra, complex logic

In practice, I recommend Pulumi when infrastructure is tightly coupled to application deployment. For example, a Laravel SaaS platform might need per-tenant databases, Redis clusters, and CDN configurations generated dynamically based on subscription tiers. Expressing this in HCL requires awkward for_each patterns and external templating. In TypeScript, it is a simple loop over a tenant array with proper typing and error handling. Conversely, if your team has dedicated platform engineers who prefer a strict boundary between app and infra, Terraform’s opinionated constraints may actually reduce risk.

Start HereInfra requires loops / conditionals?YesNoChoose PulumiTeam uses HCL already?YesNoStick with TerraformChoose PulumiBoth tools interoperate via state imports & providers
Decision framework: choose Pulumi when infrastructure logic demands programming constructs or when full-stack developers own deployment

How do you set up a Pulumi project for a Laravel application?

Setting up Pulumi for a PHP/Laravel workload follows the same pattern as any Node.js or Python project, but with specific considerations for artifact management. Since production servers typically should not run Node.js or compile TypeScript, you build assets locally or in CI and deploy only the compiled output. Below is a practical setup for a Laravel API deployed to AWS ECS with RDS PostgreSQL, using TypeScript as the infrastructure language.

  1. Initialize the project: pulumi new aws-typescript creates the scaffold with Pulumi.yaml, index.ts, and package.json.
  2. Install dependencies: npm install @pulumi/aws @pulumi/docker @pulumi/command. The @pulumi/command package is essential for running Artisan commands post-deployment.
  3. Configure stack-specific settings: pulumi config set aws:region ap-south-1 and pulumi config set --secret dbPassword. Never commit secrets to version control.
  4. Define resources in index.ts using typed constructors. Export critical outputs like the ALB DNS name for DNS automation.
  5. Add a Command resource to run php artisan migrate --force after the RDS instance is ready, using the connection string constructed from resource outputs.
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
import * as docker from "@pulumi/docker";

const config = new pulumi.Config();
const dbName = config.require("dbName");
const dbPassword = config.requireSecret("dbPassword");

// RDS PostgreSQL instance
const db = new aws.rds.Instance("laravel-db", {
    engine: "postgres",
    engineVersion: "16.4",
    instanceClass: "db.t4g.micro",
    allocatedStorage: 20,
    dbName: dbName,
    username: "laravel",
    password: dbPassword,
    skipFinalSnapshot: true,
    publiclyAccessible: false,
});

// Docker image for Laravel app
const repo = new aws.ecr.Repository("laravel-app-repo");
const image = new docker.Image("laravel-app-image", {
    build: { context: "../", dockerfile: "../Dockerfile" },
    imageName: repo.repositoryUrl,
});

// Export DB endpoint for application config
export const dbEndpoint = db.endpoint;
export const repositoryUrl = repo.repositoryUrl;

A common mistake on real client projects is forgetting that RDS endpoints are Output<string> types. You cannot interpolate them directly into environment variables passed to containers. Use pulumi.all([db.endpoint, dbPassword]).apply(([endpoint, pwd]) => ...) to resolve multiple outputs safely before constructing configuration objects. This pattern appears repeatedly when wiring Laravel environment variables to cloud resources.

Handling migrations and cache clearing

Laravel deployments require post-deploy hooks that Terraform struggles with. Pulumi’s @pulumi/command provider executes shell commands as part of the resource graph, ensuring migrations run only after the database is provisioned and before traffic shifts. Define a remote.Command or local.Command with explicit dependencies:

import { local } from "@pulumi/command";

const migrate = new local.Command("run-migrations", {
    create: `ssh deploy@${appServerIp} 'cd /var/www/current && php artisan migrate --force'`,
    triggers: [db.endpoint, image.repoDigest],
}, { dependsOn: [db, ecsService] });

This ensures idempotent execution tied to actual infrastructure changes. When integrating payment gateways like eSewa or Khalti on Nepali eCommerce projects, similar patterns handle webhook endpoint registration or API key rotation as infrastructure resources rather than manual steps.

What are the production gotchas when adopting Pulumi in 2026?

After shipping multiple Pulumi-managed systems, several recurring issues emerge that documentation rarely emphasizes. Addressing these proactively prevents painful debugging sessions during critical deployments.

  • Dependency version pinning: Pulumi providers are tightly coupled to CLI versions. A mismatch between @pulumi/aws v6.x and CLI v3.x causes cryptic gRPC errors. Always pin exact versions in package.json and match CLI version in CI. As of mid-2026, Pulumi CLI v3.130+ with @pulumi/aws v6.50+ is the stable combination.
  • Resource renaming destroys recreation: Changing a logical resource name (e.g., web-server to api-server) causes Pulumi to delete and recreate the resource, even if cloud-side properties are identical. Use aliases option to preserve identity during refactors.
  • Secret leakage in logs: Accidentally logging an Output<string> containing secrets exposes decrypted values in Pulumi Console or CI logs. Always mark sensitive outputs with pulumi.secret() and audit console.log statements.
  • Parallelism limits: Default parallelism (10 concurrent operations) can trigger cloud provider rate limits during large deployments. Set --parallel 4 for AWS accounts with low throttling thresholds or when provisioning many Lambda functions simultaneously.
  • State drift detection: Manual console changes bypass Pulumi’s state. Run pulumi refresh periodically to reconcile, especially after incident response. Automate this in CI as a non-blocking check.
Version MismatchPin CLI + ProviderResource RenameUse aliases OptionSecret Leakagepulumi.secret() WrapRate Limit Errors--parallel FlagState DriftScheduled RefreshOutput Misuse.apply() TransformMitigation Checklist in CI PipelineLint → Type Check → Preview → Deploy → Refresh Audit
Production pitfall matrix: six common Pulumi issues mapped to concrete mitigations enforceable in CI pipelines

Another subtle issue affects teams migrating from Terraform. Pulumi’s import functionality works well for individual resources, but bulk-importing entire stacks requires careful mapping. Use pulumi import with the --protect flag initially to prevent accidental deletion during validation. For Nepal-based legal-tech portals I have maintained, we imported existing EC2 instances and RDS databases incrementally, validating each resource’s state match before removing protection. This cautious approach prevented downtime during IaC adoption.

Testing infrastructure code properly

One of Pulumi’s strongest advantages is native unit testing. Unlike Terraform’s integration-heavy testing model, you can test infrastructure logic without provisioning anything. Use @pulumi/pulumi/testing to mock resource creation and assert on inputs:

import * as pulumi from "@pulumi/pulumi";
import { describe, it } from "node:test";
import assert from "node:assert";

pulumi.runtime.setMocks({
    newResource: function(args: pulumi.runtime.MockResourceArgs): {id: string, state: any} {
        return { id: args.inputs.id || "mock-id", state: args.inputs };
    },
    call: function(args: pulumi.runtime.MockCallArgs) { return args.inputs; },
}, "test-project", "test-stack", false);

describe("Database configuration", () => {
    it("uses encrypted storage", async () => {
        const db = await import("./index").then(m => m.database);
        const storageEncrypted = await pulumi.output(db.storageEncrypted).promise();
        assert.strictEqual(storageEncrypted, true);
    });
});

This runs in milliseconds during PR checks, catching misconfigurations before they reach staging. For teams managing Laravel applications serving Nepali businesses, this level of confidence is critical when infrastructure changes could affect payment processing or legal document storage compliance.

When should you avoid Pulumi entirely?

Pulumi is not universally superior. Recognizing when to choose alternatives demonstrates engineering maturity. Avoid Pulumi when:

  • Your team consists primarily of operations engineers deeply fluent in HCL and resistant to learning programming paradigms. The productivity gain vanishes if the maintainers dislike the tool.
  • Infrastructure is simple, static, and rarely changed. The overhead of maintaining a TypeScript/Python project exceeds the benefit for three EC2 instances and an S3 bucket.
  • You require vendor-neutral abstractions across multiple clouds with identical semantics. While Pulumi supports multi-cloud, provider-specific optimizations often leak through. Crossplane or CDKTF may offer better portability for pure multi-cloud strategies.
  • Regulatory requirements mandate approved, audited DSLs. Some financial and government frameworks explicitly recognize Terraform’s HCL as a controlled configuration language, while general-purpose code faces additional scrutiny.

For many Nepal-based SMEs and legal-tech platforms I work with, the sweet spot is hybrid: Pulumi for dynamic, application-coupled resources (containers, serverless functions, tenant-specific configs) and Terraform for foundational networking and IAM that changes quarterly. Both tools can coexist, referencing each other’s outputs via remote state backends or exported stack references.

Getting started with Pulumi: Infrastructure as Code in Real Languages

If you are evaluating Pulumi: Infrastructure as Code in Real Languages for your next project, start small. Provision a single non-production environment using your preferred language. Focus on understanding the Output/Input distinction and state management before tackling complex architectures. The official examples repository provides production-grade patterns for Laravel, Django, Next.js, and other frameworks. Invest time in setting up CI previews (pulumi preview on every PR) early; this habit prevents more incidents than any amount of documentation reading.

For teams needing guidance on integrating Pulumi with existing Laravel or WordPress deployments, or for cloud hosting strategy in Nepal, reaching out to experienced practitioners accelerates adoption significantly. The initial learning investment pays dividends in deployment velocity and operational confidence, but only if grounded in real-world patterns rather than theoretical examples. If you are planning an infrastructure modernization project and want to discuss whether Pulumi fits your specific constraints, get in touch to review your architecture.

Frequently Asked Questions

Pulumi is an Infrastructure as Code tool that uses general-purpose languages like TypeScript, Python, Go, or C# instead of a domain-specific language. Unlike Terraform's HCL, Pulumi lets you use loops, classes, and existing package managers to define cloud resources, reducing context switching for application developers already proficient in these ecosystems.

Yes, the Pulumi Individual tier is free forever for personal projects and single-user accounts. It includes unlimited stacks, deployments, and state management via the Pulumi Cloud backend. Teams requiring RBAC, audit logs, or SSO must upgrade to Team or Enterprise tiers, which start at approximately USD 30 per user monthly (Rs 4,000).

Pulumi supports TypeScript, JavaScript, Python, Go, C#, F#, Java, and YAML. TypeScript and Python remain the most mature with the broadest provider coverage. All runtimes require current LTS versions: Node.js 22 LTS, Python 3.11+, Go 1.22+, and .NET 8. The CLI itself is version-agnostic regarding language runtime installation.

Pulumi stores state as encrypted JSON snapshots rather than flat tfstate files. By default, state lives in the managed Pulumi Cloud backend with automatic encryption and versioning. You can alternatively self-host state in S3, Azure Blob, GCS, or local filesystems using the --backend flag during login. Each stack maintains independent state history with full rollback capability.

Yes, use pulumi import to adopt existing resources without recreation. The command generates the corresponding code definition and updates state to track the resource. You must specify the resource type token, logical name, and cloud provider ID. This is essential when migrating manual infrastructure or transitioning from other IaC tools to avoid downtime or data loss during adoption.

Pulumi encrypts secrets at rest using unique per-stack keys before storing them in state. Use pulumi config set --secret to mark values as sensitive. These are decrypted only during deployment and never appear in plaintext logs or console output. For production systems, integrate external secret managers like AWS Secrets Manager or HashiCorp Vault via native providers rather than storing credentials directly in Pulumi config.

Components are reusable abstractions that group multiple resources behind a single interface, similar to functions or classes in application code. Use them to enforce organizational standards, encapsulate complex architectures, or share patterns across teams. Unlike modules in HCL, components support inheritance, interfaces, and testing frameworks native to your chosen language, making infrastructure composition more maintainable at scale.

Both use real languages, but Pulumi is multi-cloud while CDK targets AWS exclusively. Pulumi offers broader provider coverage including Kubernetes, Datadog, and Cloudflare alongside AWS. CDK synthesizes CloudFormation templates as an intermediate step; Pulumi calls cloud APIs directly via gRPC, resulting in faster feedback loops. Choose CDK if deeply embedded in AWS ecosystem tooling; choose Pulumi for polyglot cloud strategies or non-AWS workloads.

Most failures stem from unhandled async operations, missing dependency declarations between resources, or incorrect provider configuration. Always await resource outputs before passing them downstream. Explicitly declare parent-child relationships for proper ordering. Ensure provider credentials are scoped correctly per stack. Run pulumi preview before every apply to catch drift or unintended replacements. Treat infrastructure code with the same rigor as application code: lint, test, and review.

Use separate stacks for dev, staging, and production within the same project directory. Share base configuration via Pulumi.yaml defaults and override per-stack values using pulumi config set --stack. Organize code into component libraries consumed by environment-specific entry points. Avoid conditional logic based on stack names inside components; instead, pass explicit parameters. This keeps environments isolated while maintaining DRY principles through composition rather than branching.

Yes, Pulumi has first-class Kubernetes support via the @pulumi/kubernetes provider. You can define Helm charts, YAML manifests, and CRDs alongside VPCs, databases, and IAM roles in the same program. Resources deploy in dependency order across providers automatically. This eliminates the need for separate kubectl workflows or GitOps tools for initial provisioning, though ArgoCD or Flux remain valuable for ongoing cluster reconciliation post-bootstrap.

Pulumi provides native testing libraries for unit, integration, and policy tests. Unit tests mock resource creation to validate logic without deploying. Integration tests spin up real ephemeral stacks and assert against live outputs. Policy as Code via CrossGuard enforces compliance rules before deployment. Write tests in your host language using standard frameworks like Jest, pytest, or Go testing. Infrastructure should be tested with the same discipline as application business logic.

Pulumi records partial state and marks failed resources for retry on next update. It does not automatically roll back changes already applied. Manually run pulumi refresh to reconcile state with actual cloud resources, then fix the root cause and re-run pulumi up. For critical systems, implement explicit rollback logic in your CI pipeline or use stack policies to prevent destructive changes. Never assume atomicity across multi-resource deployments.

Start by importing live resources using pulumi import rather than converting HCL line-by-line. Generate equivalent TypeScript or Python definitions from imported state. Validate parity with pulumi preview --expect-no-changes against the live environment. Migrate incrementally by module, keeping both systems operational during transition. Use tf2pulumi for automated HCL-to-code conversion as a starting point, but expect manual refinement. Budget significant time for validation; syntax translation is mechanical but behavioral equivalence requires careful testing.

Beyond the free tier, Pulumi Team costs USD 30/user/month (Rs 4,000) for collaboration features. Self-hosted backends eliminate SaaS fees but require managing storage, encryption keys, and access control yourself. Cloud provider costs for provisioned resources remain unchanged regardless of IaC tool. Factor in engineering time for learning curves, migration effort, and ongoing maintenance. For Nepal-based teams, weigh the productivity gains of familiar languages against licensing costs and operational overhead of self-hosting versus managed service.

Share this article

Quick Contact Options
Choose how you want to connect me: