What to use when HCL and state files aren't working for your team
Before Terraform, provisioning infrastructure meant clicking through a cloud console and trusting that whoever did it wrote down what they changed. Writing those resources in HCL and applying them through a pipeline made infrastructure reviewable, and for plenty of organizations that is still enough.
State lives in a file that has to be stored remotely, locked against concurrent writes, and sometimes repaired by hand when it drifts from what the cloud actually has. HCL is a configuration language rather than a programming one, so it doesn't compose the way application code does. Adding a database to a service can mean editing a separate repository, reviewed through a separate pipeline, by someone who may not have written the service. AI coding agents face the same split: the application and its environment-specific infrastructure configuration may live in different contexts.
IBM's acquisition of HashiCorp and the BSL license change accelerated a search for alternatives that was already underway. The sunsetting of CDKTF in December 2025 and the deprecation of HCP Terraform's legacy free plan in March 2026 have pushed more teams to evaluate their options. But most Terraform alternatives, and most infrastructure as code alternatives in general, are still IaC with different syntax. This guide covers the full range, from IaC tools that replace HCL with real programming languages to approaches that eliminate separate infrastructure configuration entirely.
| Feature | Encore | OpenTofu | Pulumi | AWS CDK | SST | Crossplane |
|---|---|---|---|---|---|---|
| Approach | Infrastructure from code | IaC (HCL) | IaC (imperative) | IaC (constructs) | IaC (serverless-first) | IaC (Kubernetes-native) |
| Language | TypeScript / Go | HCL | TypeScript, Python, Go, C#, Java | TypeScript, Python, Go, Java, C# | TypeScript | YAML (K8s manifests) |
| State management | Managed by Encore for platform-managed resources | Remote state file | Managed (Pulumi Cloud) or self-managed | CloudFormation stacks | Pulumi (managed or self-hosted) | Kubernetes etcd |
| Cloud support | AWS, GCP (provisions in your account) | AWS, GCP, Azure, 3,900+ providers | AWS, GCP, Azure, 150+ 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 program files | Separate program files | Separate config files | Separate YAML manifests |
| Learning curve | TypeScript or Go SDK | Low (same as Terraform) | Medium (new SDK per cloud) | Medium (CDK constructs) | Medium (Pulumi under the hood) | High (Kubernetes + CRDs) |
| License | Open Source (MPL-2.0) | Open Source (MPL-2.0) | Open Source (Apache 2.0) | Open Source (Apache 2.0) | Open Source (MIT) | Open Source (Apache 2.0) |
| Vendor lock-in | SDK migration required; Docker export available | None | Depends on state backend and providers | AWS and CloudFormation | AWS and Pulumi | Kubernetes control plane |
| AI agent support | MCP server, infra in same code agents read | Same as Terraform | Pulumi AI for generating IaC | Limited | Limited | None |
| Best for | Teams that want backend infrastructure derived from application code | Existing Terraform users wanting open source | Teams who want IaC in a real language | AWS-only shops | Serverless on AWS | Kubernetes platform teams |
Encore is a backend platform built on an open source Infra SDK for TypeScript and Go. Instead of describing infrastructure in a separate configuration language, you declare what the application needs as objects in the code that uses them: databases, Pub/Sub topics, cron jobs, object storage, and caching. Encore reads those declarations into an application model, which it uses for local development and to provision the matching AWS or GCP resources when you deploy.
Because that model is rebuilt from source on every build, there is no separate application-infrastructure state file for your team to store, lock, or repair. Encore maintains the deployment state it needs for infrastructure it manages; self-hosted deployments leave physical resource state with your existing provisioning system.
For teams using AI-first development workflows, the application and its resource dependencies stay in the same context. Type safety and static analysis reject invalid declarations during the build, while deployment-specific settings such as sizing, networking, and backups remain outside the application code agents edit.
The difference is easiest to see on a single resource. Here is a Postgres instance in Terraform, with the subnet group, security group and parameter wiring it depends on left out:
resource "aws_db_instance" "orders" {
identifier = "orders"
engine = "postgres"
instance_class = "db.t4g.medium"
allocated_storage = 20
storage_type = "gp3"
db_name = "orders"
username = var.db_username
password = var.db_password
db_subnet_group_name = aws_db_subnet_group.main.name
vpc_security_group_ids = [aws_security_group.db.id]
backup_retention_period = 7
skip_final_snapshot = false
}
The Encore declaration contains the logical name and migration directory:
const db = new SQLDatabase("orders", { migrations: "./migrations" });
The Terraform block describes the AWS implementation and its deployment properties. The Encore declaration carries the two properties the application depends on: which logical database it connects to and where its schema comes from. Instance class, storage, backups, and networking are configured separately for each environment, which lets the declaration remain unchanged on a laptop, in a preview environment, and in production.
A whole service works the same way, with the endpoint that uses the database sitting alongside it:
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 ... RETURNING *`;
await orderEvents.publish({ orderId: order!.id, total: order!.total });
return order!;
}
);
// EventBridge on AWS, Cloud Scheduler on GCP.
new CronJob("daily-cleanup", { schedule: "0 2 * * *", endpoint: cleanupExpired });
// Object storage, caches and secrets are declared the same way.
That code maps to RDS, SNS, Fargate, and EventBridge on AWS, or Cloud SQL, Pub/Sub, Cloud Run, and Cloud Scheduler on GCP. The application declaration stays cloud-independent; the corresponding infrastructure and deployment settings are configured per environment.
Deployment settings for Encore-managed resources are configured per environment in the Encore dashboard. The available settings depend on the resource and cloud provider.

Compute has an equivalent panel: CPU and memory, min and max instances, request timeout, concurrency, and VPC settings. You also choose the compute platform, whether that is AWS Fargate, GCP Cloud Run, or Kubernetes on GKE or EKS, including pointing Encore at a cluster you already run.
Developers stop waiting on a queue, and the platform team keeps the defaults. Infrastructure ships in the same pull request as the feature that needs it, so adding a Pub/Sub topic doesn't mean filing a ticket against a separate repository and pipeline. Capacity, networking, and backups remain environment settings, while IAM is derived from which services use each resource.
Invalid declarations surface at build time. Encore derives the infrastructure model from the code, so unsupported or statically unresolved resource declarations fail the build. The same validated declarations then run on a laptop and in production.
Preview environments have real backend infrastructure. Encore creates a preview environment for each pull request, including its databases, topics, and cron jobs. The same declarations also support running multiple environments without maintaining a separate infrastructure program for each.
AI agents work inside guardrails. An agent declares a database or a topic in one line and Encore applies production defaults for everything it didn't specify. It cannot resize a production instance or widen a security group, because those settings aren't in the code it edits. An MCP server gives it schemas, traces and the service graph to check its own work against.
A failed request can be diagnosed without setting anything up. Distributed tracing, logs and metrics are on by default and cover async hops, so the person or agent debugging a change has something to read back from the run.
encore build docker for standard Docker images, deployable anywhereEncore's SDK supports TypeScript and Go, and Encore Cloud provisions supported infrastructure on AWS and GCP. For Azure, unsupported resource types, or infrastructure outside the application model, keep the existing IaC workflow or self-host Encore.
The Infra SDK is part of how a service is written, so Encore is not a drop-in replacement for Terraform configuration. It is most relevant when a team wants the backend itself to declare the infrastructure it uses.
Because the infrastructure model is derived by static analysis, resource names have to be string literals and resources cannot be created in a loop or conditionally. There is no equivalent of count or for_each, so ten topics means ten declarations.
Encore and Terraform can be used together: Encore can manage the supported resources tied to an application, while Terraform continues to manage shared, organization-level, or unsupported infrastructure. Which tool owns each resource is an explicit boundary.
Connect supported infrastructure you already have. Existing RDS and Cloud SQL instances can be selected when creating an environment. Buckets and topics use their own import workflows after the declaration has first been deployed. At the environment level, Encore can also deploy into an existing GKE cluster or GCP project.
Read Encore-provisioned resources from Terraform. The Encore Terraform Provider exposes data sources such as encore_database, encore_cache, and encore_pubsub_topic, so your existing Terraform can reference infrastructure Encore created:
data "encore_pubsub_topic" "orders" {
name = "order-events"
env = "prod"
}
# ...then use it like any other resource:
# target_arn = data.encore_pubsub_topic.orders.aws_sns.arn
Self-host and bind everything yourself. Build a standard Docker image with encore build docker and supply an infra config file that maps each resource to something you provisioned yourself.
Want to jump straight to a running app? Clone this starter and deploy it to your own cloud.
Use the quick start guide to build locally, or book a 1:1 intro for a walkthrough.
OpenTofu is a fork of Terraform created in response to HashiCorp's BSL license change. It's maintained by the Linux Foundation and backed by companies including Gruntwork, Spacelift, and env0. If you're happy with how Terraform works but uncomfortable with the license, OpenTofu is the most direct replacement. Your existing .tf files, providers, and modules work without changes.
OpenTofu tracks the Terraform feature set closely while diverging in specific areas. State file encryption is built in (Terraform requires third-party tools for this). Early variable evaluation enables dynamic provider configuration that Terraform doesn't support. The provider ecosystem is fully compatible since both use the same provider protocol.
# Same HCL syntax as Terraform
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
}
OpenTofu solves the licensing problem. State management, the two-codebase split and the HCL learning curve are all still there. If your frustration with Terraform is about the BSL license and the direction of HashiCorp under IBM, OpenTofu is the right move. If it is about state files, configuration drift, or the gap between application code and infrastructure code, switching to OpenTofu changes nothing, because the workflow is identical.
Pulumi replaces HCL with real programming languages. You write infrastructure in TypeScript, Python, Go, C#, or Java using Pulumi's SDK, with full IDE support, type checking, and the ability to use loops, conditionals, and abstractions from the language itself. If HCL's limited expressiveness is your primary frustration with Terraform, Pulumi addresses that directly.
Pulumi supports over 150 cloud providers and has a managed state backend (Pulumi Cloud) that handles state storage, locking, and secrets. You can also self-manage state in S3 or a local file.
import * as aws from "@pulumi/aws";
const db = new aws.rds.Instance("orders", {
engine: "postgres",
engineVersion: "16",
instanceClass: "db.t4g.micro",
dbName: "orders",
username: "app",
password: dbPassword,
});
const topic = new aws.sns.Topic("order-events");
const queue = new aws.sqs.Queue("order-processing");
new aws.sns.TopicSubscription("order-sub", {
topic: topic.arn,
protocol: "sqs",
endpoint: queue.arn,
});
Pulumi is still Infrastructure as Code. You're writing infrastructure configuration in a better language, but you're still writing infrastructure configuration. The state file exists (managed by Pulumi Cloud or self-hosted), the plan/apply cycle exists, and infrastructure lives in separate files from your application code. If you're comfortable with the IaC model and want a better language than HCL with broader cloud support, Pulumi is the strongest option in that category. If you want to eliminate separate infrastructure configuration entirely, Pulumi doesn't solve that.
AWS CDK (Cloud Development Kit) lets you define AWS infrastructure using TypeScript, Python, Go, Java, or C#. You write "constructs" that represent AWS resources, CDK synthesizes them into CloudFormation templates, and CloudFormation provisions the resources. It's AWS's answer to HCL: use a real language, get type safety and IDE support, but stay within the AWS ecosystem.
CDK has a large library of high-level constructs that bundle common patterns. A single ApplicationLoadBalancedFargateService construct, for example, creates a Fargate service, load balancer, target group, security groups, and IAM roles in one declaration.
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, Azure, or multiple clouds, CDK doesn't help. The CloudFormation layer underneath has its own set of limitations: stack size limits, slow rollbacks on failure, and occasional resources that get stuck in UPDATE_ROLLBACK_FAILED state. CDK makes the authoring experience better, but the deployment and state management experience is still CloudFormation. You can write CloudFormation templates directly in YAML or JSON without CDK, but most teams find the authoring experience too verbose without a higher-level tool on top. If you're already on AWS and want a better way to write infrastructure, CDK is mature and well-supported. If you want to avoid maintaining separate infrastructure configuration, CDK still requires that.
SST is a framework for building and deploying applications on AWS. It started as a serverless-focused tool built on CDK but has evolved into a broader platform that supports containers, static sites, and more. SST's main differentiator is its developer experience: live Lambda development with breakpoint debugging, a console for inspecting resources, and higher-level components that reduce boilerplate compared to raw CDK.
SST v3 (Ion) replaced CDK with Pulumi under the hood, giving it access to Pulumi's provider ecosystem while keeping SST's component abstractions.
// 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 is AWS-only and requires a Pulumi state backend (either Pulumi Cloud or self-managed). The move from CDK to Pulumi in v3 was a significant breaking change, and some SST v2 patterns don't carry over. SST's sweet spot is teams building on AWS Lambda and serverless infrastructure who want a better developer experience than raw CDK or Serverless Framework. If you're building container-based services or deploying to GCP, SST is less relevant. The infrastructure still lives in separate config files from your application code.
Crossplane manages cloud infrastructure through Kubernetes custom resources. You define infrastructure as Kubernetes manifests (YAML), and Crossplane controllers reconcile those manifests against your cloud provider. If your team already operates Kubernetes and wants to manage all infrastructure through kubectl and GitOps workflows, Crossplane fits into that model.
Crossplane supports AWS, GCP, and Azure through provider packages. It can also compose multiple resources into higher-level abstractions using Compositions, similar to Terraform modules or CDK constructs.
apiVersion: database.aws.crossplane.io/v1beta1
kind: RDSInstance
metadata:
name: orders
spec:
forProvider:
region: us-east-1
engine: postgres
engineVersion: "16"
instanceClass: db.t4g.micro
dbName: orders
masterUsername: app
writeConnectionSecretToRef:
name: orders-db-credentials
namespace: default
Crossplane requires a running Kubernetes cluster, which is a significant prerequisite. If you're not already running Kubernetes, adopting Crossplane means taking on Kubernetes complexity to manage your cloud infrastructure. The YAML manifests are verbose, the provider coverage varies (some AWS resources have better support than others), and debugging reconciliation failures requires understanding both Kubernetes and cloud provider APIs. Crossplane is the right choice for platform teams that have standardized on Kubernetes and want a single control plane for everything. For application developers who want to ship features without managing infrastructure, it adds a layer of complexity rather than removing one.
In 2026, teams leaving Terraform are usually frustrated by the license, the language, or the model. The BSL license change, the CDKTF sunset and the deprecation of HCP Terraform's legacy free plan turned that frustration into a deadline for a lot of organizations. Those three complaints have different answers, so it is worth being clear about which one is yours before comparing tools.
If the license is the issue and everything else works, OpenTofu is the direct answer. Same HCL, same providers, same workflow, open-source governance. Migration is a one-line change.
If HCL is the problem but the IaC model is fine, Pulumi is the strongest option. Real programming languages, broad cloud support, managed state. AWS CDK fills a similar role for teams committed to AWS. SST adds a better developer experience on top of CDK/Pulumi for serverless-heavy workloads.
If Kubernetes is your control plane for everything, Crossplane extends that model to cloud infrastructure. It makes sense for platform teams that have already bet on Kubernetes. For teams that haven't, it adds more complexity than it removes.
If the IaC model itself is the problem, with its state files, configuration drift, separate repositories, and the gap between application code and infrastructure code, those issues exist in every IaC tool on this list. OpenTofu, Pulumi, CDK, SST, and Crossplane all require you to write and maintain infrastructure configuration separately from your application, even the ones that support TypeScript.
Encore takes a different path: supported infrastructure is declared in the application code and the application team does not maintain a separate state file or plan/apply workflow for those resources. Deployment settings still exist per environment, and Terraform can remain in place for shared or unsupported infrastructure. For TypeScript or Go teams evaluating that model, the Coming from Terraform guide covers the practical migration and coexistence paths.
Want to jump straight to a running app? Clone this starter and deploy it to your own cloud.
It depends on what you want to leave behind. If the BSL license is the problem, OpenTofu is a drop-in replacement that keeps your existing HCL, providers, and modules. If HCL itself is the problem, Pulumi and AWS CDK let you write infrastructure in a real programming language. If the separate-config model is the problem, Encore declares infrastructure in your application code and manages deployment state for supported resources in your AWS or GCP account.
For most projects, yes. OpenTofu is a Linux Foundation fork of Terraform that uses the same HCL syntax and the same provider protocol, so existing .tf files, providers, and modules work without changes. It has diverged in a few areas (built-in state encryption, early variable evaluation), but the core workflow is identical.
Most do not. OpenTofu, Pulumi, AWS CDK, SST, and Crossplane all expose a state store of some kind (a remote state file, Pulumi backend, CloudFormation stacks, or Kubernetes etcd). Encore derives its application model from source and manages the deployment state for Encore-managed resources, so the application team does not maintain a separate infrastructure state file.
AWS CDK is the better fit if you are committed to AWS and want tight integration with AWS services and CloudFormation. Pulumi is the better fit if you deploy to more than one cloud or want managed state and secrets through Pulumi Cloud. Both keep infrastructure in separate program files from your application code.
Yes. OpenTofu reads the same configuration, so teams often migrate incrementally. Encore is designed to coexist: it handles common backend infrastructure (databases, Pub/Sub, cron, object storage) from your code, and you can keep Terraform for anything outside its primitives, such as custom VPC topologies or specialized services.
Usually not. Crossplane manages cloud infrastructure through Kubernetes custom resources, so it assumes a running cluster and familiarity with CRDs and reconciliation. It is a strong choice for platform teams already standardized on Kubernetes, but for teams that are not, it adds Kubernetes complexity rather than removing infrastructure work.
Automated infrastructure for humans and agents
Let agents build and validate features with real infrastructure in the dev loop. Encore automatically provisions infrastructure, from local dev to production in your cloud on AWS/GCP.