The landscape at a glance

Terraform, OpenTofu and Ansible, the two families the rest of this topic uses, are not the only tools. The overview page divides tools into provisioning (creating cloud resources) and configuration management (setting up what runs inside a machine); a second useful question is whether a tool acts only when you run it, or keeps checking and correcting all the time.

A map of infrastructure as code tools on two axes. Horizontally: tools that create cloud resources on the left, tools that configure the inside of machines on the right. Vertically: tools that act when you run them at the top, tools with an agent or controller that keeps reconciling at the bottom. Top left: Terraform, OpenTofu, CloudFormation, Bicep, Pulumi and the AWS CDK. Bottom left: Crossplane. Top right: Ansible, PowerShell DSC version 3 and salt-ssh. Bottom right: Puppet, Chef and Salt with minions. creates cloud resources configures inside machines run on demand keeps reconciling Terraform, OpenTofu CloudFormation, Bicep Pulumi, AWS CDK Crossplane (Kubernetes controllers) Ansible PowerShell DSC v3 salt-ssh Puppet, Chef Infra Salt with minions (agent on every machine)
The two tool families used in this topic sit in the top row. Shaded: tools that keep correcting drift on their own.

The table compares the tools on this page with the two families the rest of the topic uses. Versions and licenses are as of October 2026.

Tool Manages You write Where it keeps track License
Terraform / OpenTofu Anything with a provider HCL A state file you store BSL 1.1 / MPL 2.0
AWS CloudFormation AWS YAML or JSON AWS (the stack) AWS service
ARM templates and Bicep Azure JSON, or Bicep Azure (the deployment) Bicep: MIT
Pulumi Anything with a provider TypeScript, JavaScript, Python, Go, .NET, Java or YAML A state backend Apache 2.0
AWS CDK AWS TypeScript, JavaScript, Python, Java, C# or Go CloudFormation Apache 2.0
Crossplane Cloud resources, through Kubernetes Kubernetes YAML The Kubernetes API Apache 2.0
Ansible Servers, network devices, some cloud YAML playbooks Nothing; checks each run GPL 3.0
Puppet / OpenVox Servers Puppet language Puppet server Commercial builds / Apache 2.0
Chef Infra Servers Ruby-based recipes Chef Infra Server Apache 2.0 source, commercial builds
Salt Servers YAML with Jinja2 Salt master Apache 2.0
PowerShell DSC v3 Windows, Linux and macOS settings YAML or JSON Nothing; checks each run MIT

The examples below all describe the same thing where they can: a private, versioned storage bucket for Kestrel Energy's nightly billing exports, owned by Joost Bakker. None of these tools was installed on the test machine, so all output on this page is illustrative, written from each tool's documentation.

AWS CloudFormation

AWS CloudFormation is the infrastructure as code service built into Amazon Web Services. You write a template in YAML or JSON; CloudFormation creates the resources as one unit called a stack, remembers which real resources belong to it, and deletes them together when the stack is deleted. There is no state file to protect: AWS keeps that bookkeeping itself.

billing-exports.yaml:

AWSTemplateFormatVersion: "2010-09-09"
Description: Kestrel Energy bucket for the nightly billing exports
Parameters:
  Environment:
    Type: String
    AllowedValues: [test, prod]
Resources:
  BillingExports:
    Type: AWS::S3::Bucket
    DeletionPolicy: Retain
    Properties:
      BucketName:
        Fn::Sub: kestrel-billing-exports-${Environment}
      VersioningConfiguration:
        Status: Enabled
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      Tags:
        - Key: Owner
          Value: joost.bakker
Outputs:
  BucketName:
    Value:
      Ref: BillingExports

Fn::Sub substitutes the parameter into the name and Ref returns the bucket's name. Most templates use the short forms !Sub and !Ref; those are YAML tags that only CloudFormation understands, so ordinary YAML tools (and linters that do not know them) reject the file. The long form above is valid plain YAML. DeletionPolicy: Retain keeps the bucket and its exports even if the stack is deleted.

Change sets

A change set is CloudFormation's preview: a list of what an update to a stack would add, modify or remove, and whether a modification replaces a resource, created without changing anything. It plays the role of tofu plan, and it is what a reviewer approves before someone executes it.

$ aws cloudformation deploy --template-file billing-exports.yaml \
    --stack-name kestrel-billing-exports-prod \
    --parameter-overrides Environment=prod --no-execute-changeset

Waiting for changeset to be created..
Changeset created successfully. Run the following command to review changes:
aws cloudformation describe-change-set --change-set-name arn:aws:cloudformation:eu-west-1:111122223333:changeSet/awscli-cloudformation-package-deploy-1791277200/…

CloudFormation also offers drift detection (which resources were changed outside the stack) and StackSets, which roll the same stack out to many AWS accounts and Regions. Its limit is in the name: it manages AWS only. A DNS record at another provider, a GitHub repository or a monitoring dashboard needs another tool.

Azure: ARM templates and Bicep

On Azure, every deployment goes through Azure Resource Manager. An ARM template (Azure Resource Manager template) is the JSON format that Resource Manager deploys directly: complete and precise, but verbose and hard to review. Bicep is Microsoft's domain-specific language for the same thing: during deployment, the Bicep CLI converts a .bicep file into an ARM JSON template. Bicep is the recommended way to write new templates; version 0.48.1 is current (as of October 2026).

@allowed(['test', 'prod'])
param environment string
param location string = resourceGroup().location

resource billingExports 'Microsoft.Storage/storageAccounts@2023-05-01' = {
  name: 'stkestrelbill${environment}'
  location: location
  sku: { name: 'Standard_ZRS' }
  kind: 'StorageV2'
  properties: {
    allowBlobPublicAccess: false
    minimumTlsVersion: 'TLS1_2'
    supportsHttpsTrafficOnly: true
  }
  tags: { owner: 'joost.bakker' }
}

output storageAccountName string = billingExports.name

The @2023-05-01 part pins the version of the Azure API the resource definition follows, so a template keeps meaning the same thing over the years. Azure has no state file either: it stores the state of every deployment itself. The preview is called what-if:

$ az deployment group what-if --resource-group rg-kestrel-billing-prod \
    --template-file billing-exports.bicep --parameters environment=prod
Resource and property changes are indicated with this symbol:
  + Create

The deployment will update the following scope:

Scope: /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-kestrel-billing-prod

  + Microsoft.Storage/storageAccounts/stkestrelbillprod [2023-05-01]
…

Resource changes: 1 to create.

Deployments are idempotent: deploying the same file twice changes nothing the second time. Google Cloud's counterpart is Infrastructure Manager, which runs Terraform configurations for you (see native tools on the Cloud page).

Infrastructure in a programming language

HCL, YAML and Bicep are configuration languages: loops and conditions exist, but they are limited on purpose. Some teams would rather use the language they already know, with its editor support, test frameworks and package manager. Two tools are built for that.

Pulumi

Pulumi is an infrastructure as code tool in which you describe resources in TypeScript, JavaScript, Python, Go, .NET languages, Java or YAML. Running the program does not create anything directly: it registers the desired resources, and the Pulumi engine compares them with its state and works out the changes, like a Terraform plan. State lives in Pulumi Cloud or in a backend you run yourself, such as an object storage bucket. Pulumi is open source under Apache 2.0 (version 3.267.0, as of October 2026).

"""Kestrel billing export buckets, one per environment (Pulumi, Python)."""
import pulumi
import pulumi_aws as aws

for env in ["test", "prod"]:
    bucket = aws.s3.Bucket(
        f"billing-exports-{env}",
        tags={"Owner": "joost.bakker", "Environment": env},
    )
    aws.s3.BucketVersioning(
        f"billing-exports-{env}-versioning",
        bucket=bucket.id,
        versioning_configuration={"status": "Enabled"},
    )
    pulumi.export(f"bucket_{env}", bucket.bucket)

pulumi preview shows the planned changes and pulumi up applies them. The ordinary for loop is the point: anything Python can compute, the configuration can use. That is also the risk, because a clever program is harder to review than a plain list of resources.

AWS Cloud Development Kit

The AWS Cloud Development Kit (CDK) is a framework for describing AWS infrastructure in TypeScript, JavaScript, Python, Java, C# or Go. It does not deploy anything itself: cdk synth turns the program into a CloudFormation template, cdk diff compares it with the deployed stack, and cdk deploy hands it to CloudFormation. Its strength is constructs, reusable building blocks with safe defaults. Version 2.272.0 is current (as of October 2026), under Apache 2.0.

from aws_cdk import App, RemovalPolicy, Stack
from aws_cdk import aws_s3 as s3
from constructs import Construct


class BillingExportsStack(Stack):
    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)
        s3.Bucket(
            self,
            "BillingExports",
            versioned=True,
            block_public_access=s3.BlockPublicAccess.BLOCK_ALL,
            enforce_ssl=True,
            removal_policy=RemovalPolicy.RETAIN,
        )


app = App()
BillingExportsStack(app, "kestrel-billing-exports-prod")
app.synth()

One line, enforce_ssl=True, adds a bucket policy that refuses unencrypted connections; the construct writes the policy for you. Read the synthesized template at least once, so you know what a construct really creates.

Crossplane: infrastructure through Kubernetes

Crossplane turns a Kubernetes cluster into a control plane for cloud resources: a bucket or a database becomes a Kubernetes object, and a Crossplane provider running in the cluster creates the real resource and keeps it matching the object, like any other Kubernetes controller. It is a CNCF project, graduated in October 2025; Crossplane 2 appeared in August 2025 and 2.4.2 is current (as of October 2026).

apiVersion: s3.aws.m.upbound.io/v1beta1
kind: Bucket
metadata:
  name: kestrel-billing-exports-prod
  namespace: billing
spec:
  forProvider:
    region: eu-west-1
    tags:
      Owner: joost.bakker
$ kubectl get buckets -n billing
NAME                           SYNCED   READY   EXTERNAL-NAME                  AGE
kestrel-billing-exports-prod   True     True    kestrel-billing-exports-prod   3m12s

The difference with Terraform is timing: Terraform acts when someone runs apply, while Crossplane reconciles continuously, so a change made by hand in the console is reverted within minutes. Platform teams use Crossplane's compositions to offer simple custom resources ("a Kestrel database") that expand into many cloud resources, and deliver them with GitOps. The price is that you need, and must protect, a Kubernetes cluster that holds credentials for your cloud.

Puppet, Chef and Salt

Before Ansible became common, configuration management meant one of these three. All three manage the inside of servers (packages, files, services, users), and all three traditionally run an agent on every machine that regularly pulls its configuration from a central server and corrects drift on its own, where Ansible pushes over SSH only when someone runs a playbook (see other tools on the Network Automation page). Each example below writes the same file: the billing export's settings.

Puppet

Puppet is a declarative configuration management tool with its own language: you declare resources and their relationships, and the agent on each node (by default every 30 minutes) fetches a compiled catalog from the Puppet server and enforces it.

file { '/etc/kestrel/billing-export.conf':
  ensure  => file,
  owner   => 'billing',
  group   => 'billing',
  mode    => '0640',
  content => "EXPORT_DIR=/srv/billing/out\n",
}

Puppet has belonged to Perforce since 2022. According to Perforce's announcement in late 2024, its official Puppet packages have been published since 2025 in a private repository under a commercial license, free only for small installations; the public source repository stops at version 8.10.0. The community responded with OpenVox, a fork by the Vox Pupuli community under Apache 2.0; OpenVox 9.0.0 was released on 2 October 2026. Check which one an organization runs before following a tutorial.

Chef Infra

Chef Infra describes configuration as recipes written in a Ruby-based language, grouped in cookbooks. Resources in a recipe run in the order written; the Chef Infra Client on each node pulls its cookbooks from a Chef Infra Server and converges the node. Chef has been owned by Progress Software since 2020; the source code is Apache 2.0, but Progress's own builds require accepting a license, and community builds exist under other names (such as Cinc). Chef Infra Client 19 is the current major version (as of October 2026).

file '/etc/kestrel/billing-export.conf' do
  content "EXPORT_DIR=/srv/billing/out\n"
  owner 'billing'
  group 'billing'
  mode '0640'
end

Salt

Salt (also SaltStack) is a configuration management and remote execution tool built around a fast event bus: a master sends states and commands to minions (agents), and can react to events from them. It can also work without agents over SSH (salt-ssh). States are YAML with Jinja2 templating. Salt passed from SaltStack to VMware in 2020 and with VMware to Broadcom; Salt 3008 is the current long-term support release (as of October 2026). Not to be confused with the salt in password hashing.

/etc/kestrel/billing-export.conf:
  file.managed:
    - contents: EXPORT_DIR=/srv/billing/out
    - user: billing
    - group: billing
    - mode: "0640"

PowerShell DSC

PowerShell DSC (Desired State Configuration) is Microsoft's declarative configuration tool. The classic version, built into Windows PowerShell 5.1, compiled configurations to MOF files and enforced them with the Local Configuration Manager service on Windows. DSC v3, generally available since 12 March 2025, is a new, cross-platform tool: a dsc command for Windows, Linux and macOS that reads configuration documents in YAML or JSON, with no background service. Resources can be written in any language. Version 3.3.0 is current (as of October 2026), under the MIT license.

billing-export.dsc.config.yaml:

$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
  - name: Billing export folder setting
    type: Microsoft.Windows/Registry
    properties:
      _exist: true
      keyPath: HKLM\SOFTWARE\Kestrel\BillingExport
      valueName: ExportDir
      valueData:
        String: D:\Billing\Out
$ dsc config test --file billing-export.dsc.config.yaml
$ dsc config set --file billing-export.dsc.config.yaml

test reports whether the machine is in the desired state; set makes it so. Because DSC v3 has no agent, something else decides when it runs: a scheduled task, a pipeline, or another configuration tool that calls it. PowerShell itself is covered in the Scripting topic.

How to choose

There is rarely one right tool. These questions narrow it down quickly:

  • What do you manage? One cloud only: its native tool (CloudFormation, Bicep) is well supported and needs no state storage. Several clouds or SaaS services as well: Terraform, OpenTofu or Pulumi. The inside of servers: Ansible, Puppet, Chef, Salt or DSC, usually together with a provisioning tool.
  • Who writes and reviews it? Operations teams tend to read configuration languages more easily; development teams may prefer Pulumi or the CDK. Everyone who approves changes must be able to read the code and the preview.
  • On demand or continuous? Run-on-demand tools (Terraform, Ansible) need a scheduled drift check; agent and controller tools (Puppet, Crossplane) correct drift themselves but need servers or a cluster to run on.
  • License and governance. Since 2023, Terraform, Vault and Packer moved to the Business Source License, Puppet's packages became commercial, and CDKTF was archived. Tools under open-source licenses and neutral foundations (OpenTofu, Crossplane, OpenVox) protect against one vendor changing the rules, but check their activity and support too.
  • What do you already run? A second tool for the same job doubles the knowledge, review effort and failure modes.

Kestrel Energy recorded its choice in an architecture decision: the standard is OpenTofu for cloud resources (open license, the team already knew Terraform) and Ansible for servers such as db01 and the web hosts. The meter-api team, which came with an acquisition, keeps its AWS CDK code because it produces reviewable CloudFormation change sets; new services follow the standard. Images and first-boot configuration, the remaining piece, are covered on Images and Bootstrapping.