# AWS IaC scaffold: Terraform / OpenTofu / Pulumi read-only smoke + Ansible (SSM) template

Published with:

- *[AWS IaC: Terraform vs OpenTofu vs Ansible (2026 decision guide)](https://www.factualminds.com/blog/aws-terraform-opentofu-ansible-iac-decision-guide/)*
- *[Terraform & OpenTofu vs Pulumi on AWS (2026)](https://www.factualminds.com/blog/terraform-opentofu-vs-pulumi-aws-2026/)*

Browse this tree at [`www.factualminds.com/examples/aws-iac-decision-demo/`](https://www.factualminds.com/examples/aws-iac-decision-demo/README.md).

It intentionally **creates zero billable AWS resources** in the Terraform and Pulumi roots: only provider wiring plus **read-only lookups** so you validate credentials and provider pins without risking drift in a sandbox.

## Prerequisites

- AWS credentials with **`sts:GetCallerIdentity`** (and IAM read APIs if you uncomment optional data sources later).
- **OpenTofu >= 1.12** or **Terraform >= 1.5** (`tofu` substitutes 1:1 for `terraform` in commands below).
- **Pulumi >= 3.x** and **Node >= 24** for the Pulumi scaffold.
- **Python 3** for the benchmark summary script (stdlib only).

## Smoke test (Terraform or OpenTofu)

```bash
cd terraform
terraform init -backend=false -input=false   # or: tofu init -backend=false -input=false
terraform plan -input=false -no-color       # expect: Plan: 0 to add, 0 to change, 0 to destroy
```

You should see **`No changes`** if your AWS caller identity resolves.

## Smoke test (Pulumi)

```bash
cd pulumi
npm install
pulumi stack select dev --create
pulumi preview --non-interactive   # expect: 0 creates, 0 updates, 0 deletes
```

## Benchmark (OpenTofu plan vs Pulumi preview)

From the repo root of this example:

```bash
make bench                    # default: 5 runs per tool, uses `tofu` if installed
make bench IAC_BIN=terraform  # compare Terraform instead of OpenTofu
make bench BENCH_RUNS=10      # more samples
```

Output CSV: `out/bench-{timestamp}.csv` with columns `tool,run,elapsed_ms,exit_code`.

The blog post cites **median** wall-clock times from this harness on the same read-only stack (caller identity + region lookups only).

## Ansible (Systems Manager wiring)

Collections (install once per controller):

```bash
ansible-galaxy collection install -r ansible/requirements.yml
```

Configure inventory for **SSM** targets (recommended on AWS instead of bastion SSH for private subnets). Copy `ansible/inventory/group_vars/target_aws_ec2.yml.example` to `group_vars/target_aws_ec2.yml` (when using dynamic inventories) or adapt inline.

```bash
cd ansible
ANSIBLE_PYTHON_INTERPRETER=auto ansible-playbook -i inventory/static.example.yml \
  playbooks/ping-ssm-compatible.yml --check
```

`--check` performs a dry-run of module argument validation suitable for laptops without live targets.

## Decision tree (summary)

| Layer                     | Typical tool                                   |
| ------------------------- | ---------------------------------------------- |
| VPC, subnets, SGs, IAM    | OpenTofu or Terraform (+ remote state S3+Dynamo) |
| App/platform abstractions | Pulumi (TypeScript/Python) when HCL loops hurt |
| OS packages / app rollout | Ansible (SSM Session Manager plugin or SSH)    |
| Vendor / license stance   | HashiCorp **BSL Terraform** vs **MPL OpenTofu**|

## File count

| Path        | Files | Role                                      |
| ----------- | ----- | ----------------------------------------- |
| `terraform/`| 4     | HCL read-only scaffold                    |
| `pulumi/`   | 5     | TypeScript read-only scaffold             |
| `ansible/`  | 4     | SSM-compatible playbook template          |
| `scripts/`  | 1     | Benchmark harness                         |

## Disclaimer

Infrastructure patterns here are deliberately minimal templates—not production blueprints.
