What to use when you want infrastructure without separate config programs
Pulumi improved on Terraform in a real way by letting teams write infrastructure in TypeScript, Python, or Go instead of HCL. You get full IDE support, type checking, and the ability to use loops and conditionals that actually work the way you'd expect. For a lot of teams, that's been enough.
The friction is in what didn't change. You're still writing infrastructure configuration in a separate program from your application code, and stack state still exists whether you use Pulumi Cloud or a self-managed backend. Teams that adopted Pulumi to escape Terraform's authoring limitations can find that the ownership and deployment workflow remains similar, just with a more expressive language.
Most Pulumi alternatives are other IaC tools with different tradeoffs on language, cloud support, or state management. This guide covers those, plus an application-driven approach where the team does not maintain a separate infrastructure program for supported backend resources.
| Feature | Encore | Terraform | OpenTofu | AWS CDK | SST | Crossplane |
|---|---|---|---|---|---|---|
| Approach | Infrastructure from code | IaC (HCL) | IaC (HCL) | IaC (constructs) | IaC (serverless-first) | IaC (Kubernetes-native) |
| Language | TypeScript / Go | HCL | HCL | TypeScript, Python, Go, Java, C# | TypeScript | YAML (K8s manifests) |
| State management | Managed by Encore for platform-managed resources | Remote state file | Remote state file | CloudFormation stacks | Pulumi (managed or self-hosted) | Kubernetes etcd |
| Cloud support | AWS, GCP (provisions in your account) | AWS, GCP, Azure, 6,000+ providers | AWS, GCP, Azure, 6,000+ providers | AWS only | AWS only | AWS, GCP, Azure via providers |
| Infrastructure config | Resource declarations in application code; deployment settings per environment | Separate HCL files | Separate HCL files | Separate program files | Separate config files | Separate YAML manifests |
| Learning curve | TypeScript or Go SDK | Medium (HCL) | Medium (HCL) | Medium (CDK constructs) | Medium (Pulumi under the hood) | High (Kubernetes + CRDs) |
| AI agent support | MCP server, infra in same code agents read | Limited | Limited | Limited | Limited | None |
| Vendor lock-in | SDK migration required; Docker export available | Depends on providers and modules | Depends on providers and modules | AWS and CloudFormation | AWS and Pulumi | Kubernetes control plane |
| Best for | Backend infrastructure derived from application code | Multi-cloud IaC with a broad ecosystem | Terraform-compatible IaC with open-source governance | AWS-only shops | Serverless on AWS | Kubernetes platform teams |
Encore is relevant when the friction comes from maintaining an infrastructure program and stack state alongside the backend. Its open source Infra SDK for TypeScript and Go declares the APIs, services, and supported infrastructure used by the application. Encore reads those declarations into an application model for local development and deployment.
Pulumi resources describe cloud infrastructure and belong to a stack. Encore resources record dependencies of the application. Capacity, networking, backups, and other deployment properties are configured separately for each environment.
The application model is also useful for AI-first development workflows. Agents can see APIs and resource relationships together, while type safety and static analysis provide build-time guardrails. Deployment-specific settings such as sizing, networking, and backups stay outside the application code agents edit.
import { api } from "encore.dev/api";
import { SQLDatabase } from "encore.dev/storage/sqldb";
import { Topic } from "encore.dev/pubsub";
import { CronJob } from "encore.dev/cron";
// RDS on AWS, Cloud SQL on GCP, Docker Postgres locally.
const db = new SQLDatabase("orders", { migrations: "./migrations" });
// SNS on AWS, Pub/Sub on GCP, in-memory locally.
const orderEvents = new Topic<OrderEvent>("order-events", {
deliveryGuarantee: "at-least-once",
});
export const createOrder = api(
{ method: "POST", path: "/orders", expose: true, auth: true },
async (req: CreateOrderRequest): Promise<Order> => {
const order = await db.queryRow`
INSERT INTO orders (customer_id, total)
VALUES (${req.customerId}, ${req.total})
RETURNING *`;
await orderEvents.publish({ orderId: order!.id, total: order!.total });
return order!;
}
);
// EventBridge on AWS, Cloud Scheduler on GCP.
const cleanup = new CronJob("daily-cleanup", {
title: "Clean up expired orders",
schedule: "0 2 * * *",
endpoint: cleanupExpiredOrders,
});
The SQLDatabase, Topic, and CronJob declarations are used by the backend itself. Encore analyzes those references to determine which services use each resource and provisions the cloud implementation configured for the environment. A Pulumi program describes that implementation directly and records it in stack state.
Capacity, networking, backups, regions, and compute settings are configured per Encore environment. Pulumi configuration remains the source of truth for every resource Pulumi continues to own.
Application and infrastructure changes stay together. A database or topic is declared in the service that uses it, so the resource and the feature can be reviewed in the same pull request.
The application team does not manage stack state for supported resources. Encore rebuilds the application model from source and manages the deployment state required for resources it provisions.
Agents get guardrails and application context. Resource declarations are type-checked and must be statically discoverable, so invalid changes fail during the build. The MCP server also exposes schemas, traces, and the service graph.
Each pull request can include a working backend environment. Preview environments use the same declarations as local development and production, including databases, topics, and cron jobs.
Observability follows the application structure. Traces, logs, metrics, and service relationships are available without defining a separate observability stack for each service.
Encore primitives must be statically discoverable, so there is no equivalent to creating resources through loops or runtime control flow. Unsupported and dynamically generated infrastructure can remain in Pulumi. Encore also does not consume Pulumi state; connecting an existing physical resource requires a resource-specific workflow.
Pulumi can manage organization-level, shared, unsupported, and dynamically generated infrastructure alongside Encore. Supported existing resources use Encore's resource-specific import workflows, while self-hosted environments can map Encore declarations to infrastructure provisioned by Pulumi.
Use the quick start guide to build locally, or book a 1:1 intro for a walkthrough.
Terraform is the tool Pulumi was designed to replace, and yet some teams go back. If your frustration with Pulumi is about pricing or the complexity of Pulumi's SDK, Terraform's ecosystem is larger, the community knowledge base is deeper, and the learning resources are more abundant. HCL is less expressive than TypeScript, but it's also more predictable. What you write is what you get.
Terraform's provider ecosystem covers 6,000+ providers through the registry, far beyond what any other tool offers. For teams managing infrastructure across multiple clouds and SaaS providers, that breadth is hard to match.
resource "aws_db_instance" "orders" {
identifier = "orders"
engine = "postgres"
engine_version = "16"
instance_class = "db.t4g.micro"
db_name = "orders"
username = "app"
password = var.db_password
}
Going from Pulumi back to Terraform means giving up real programming languages for HCL, which is a meaningful step backward in expressiveness. State management, the two-codebase problem, and the coordination overhead between infrastructure and application changes all remain. IBM's acquisition of HashiCorp and the BSL license change are also worth considering if licensing matters to your organization.
OpenTofu is the open-source fork of Terraform maintained by the Linux Foundation. If you're considering Terraform but the BSL license is a concern, OpenTofu gives you the same HCL, the same providers, and the same workflow under MPL-2.0. Your existing Terraform files work without changes.
OpenTofu has added features Terraform doesn't have: built-in state file encryption, early variable evaluation for dynamic provider configuration, and for_each on provider blocks. The provider ecosystem is fully compatible.
OpenTofu solves the licensing concern with Terraform. It doesn't change the fundamental model. You're still writing HCL, managing state files, and maintaining infrastructure configuration separately from your application code. If your frustration with Pulumi was about the IaC model itself rather than the specific language, OpenTofu has the same structural limitations.
AWS CDK lets you define AWS infrastructure in TypeScript, Python, Go, Java, or C#. Like Pulumi, you get real programming languages with type safety and IDE support. The key difference is that CDK synthesizes to CloudFormation templates, so the deployment model is CloudFormation's stack-based approach rather than Pulumi's graph-based state.
CDK's high-level constructs bundle common patterns. A single ApplicationLoadBalancedFargateService creates a Fargate service, load balancer, target group, security groups, and IAM roles together.
import * as cdk from "aws-cdk-lib";
import * as rds from "aws-cdk-lib/aws-rds";
import * as ec2 from "aws-cdk-lib/aws-ec2";
const vpc = new ec2.Vpc(this, "VPC");
const db = new rds.DatabaseInstance(this, "Orders", {
engine: rds.DatabaseInstanceEngine.postgres({
version: rds.PostgresEngineVersion.VER_16,
}),
vpc,
instanceType: ec2.InstanceType.of(
ec2.InstanceClass.T4G, ec2.InstanceSize.MICRO
),
databaseName: "orders",
});
CDK is AWS only. If you deploy to GCP or Azure, CDK doesn't help, and that's a step backward from Pulumi's multi-cloud support. CloudFormation's deployment model has its own limitations: stack size limits, slow rollbacks on failure, and occasional stuck resources. CDK also still requires maintaining infrastructure code separately from application code, same as Pulumi.
SST is a framework for building and deploying applications on AWS. SST v3 (Ion) replaced CDK with Pulumi under the hood, which means SST is less a Pulumi alternative and more a higher-level layer on top of it. If your frustration with Pulumi is about the amount of boilerplate you write rather than the model itself, SST's components reduce that significantly.
SST's standout features are live Lambda development with breakpoint debugging and its Console for inspecting deployed resources.
// sst.config.ts
export default $config({
app(input) {
return { name: "orders", home: "aws" };
},
async run() {
const db = new sst.aws.Postgres("OrdersDB", { scaling: { min: "0.5 ACU", max: "2 ACU" } });
const api = new sst.aws.Function("API", {
handler: "src/api.handler",
link: [db],
url: true,
});
return { url: api.url };
},
});
SST uses Pulumi under the hood, so you're not escaping Pulumi's state management or deployment model. You still need a Pulumi state backend. SST is AWS-only and primarily designed for serverless workloads. If you're leaving Pulumi because of the IaC model, SST doesn't change that. If you're leaving because Pulumi's SDK is too low-level for AWS work, SST is a reasonable step up.
Crossplane manages cloud infrastructure through Kubernetes custom resources. You define infrastructure as Kubernetes manifests and Crossplane controllers reconcile those manifests against your cloud provider. If your team has standardized on Kubernetes and wants to manage all infrastructure through kubectl and GitOps workflows, Crossplane fits that model.
Crossplane requires a running Kubernetes cluster, which is a significant prerequisite. The YAML manifests are verbose, provider coverage varies, and debugging reconciliation failures requires understanding both Kubernetes and cloud provider APIs. Moving from Pulumi's TypeScript to Crossplane's YAML is a step backward in developer experience. Crossplane makes sense for platform teams that have bet on Kubernetes as their control plane. For application developers, it adds complexity.
Teams leaving Pulumi are usually frustrated by one of three things: the pricing, the operational overhead of state management, or the realization that writing infrastructure in TypeScript is still writing infrastructure.
If the pricing is the issue but the IaC model works for you, Terraform or OpenTofu get you back to a tool with no per-resource fees. You give up TypeScript for HCL, but the ecosystem is larger and the community support is deeper. OpenTofu adds the benefit of open-source governance.
If you want to stay in TypeScript on AWS but want less boilerplate, SST is a higher-level layer on top of Pulumi that reduces the configuration you write. AWS CDK is the alternative if you prefer CloudFormation's deployment model over Pulumi's.
If Kubernetes is your control plane for everything, Crossplane extends that model to cloud infrastructure. It makes sense for platform teams, not application developers.
If the IaC model itself is the frustration, with state files, separate infrastructure programs, and the coordination overhead between infrastructure and application changes, every tool on this list except Encore still requires that. Pulumi made the authoring experience better by using real languages, but the operational model is the same.
Encore removes the separate infrastructure program for the backend resources represented by its primitives. The application team declares those resources in code, while deployment settings and managed infrastructure state remain per environment. Pulumi can continue to own everything outside that model. For teams evaluating that boundary, Coming from Pulumi covers state, imports, coexistence, and an incremental workflow.
Ship without waiting on Terraform
Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.