DEV Community

Ayi NEDJIMI
Ayi NEDJIMI

Posted on

Terraform vs Pulumi vs Ansible: IaC for small teams

You're a team of three engineers. One of you just broke staging by running a script manually. Nobody knows what the current state of infra actually is. IaC is the obvious fix — but Terraform, Pulumi, and Ansible all claim to solve this. Which one?

The short answer: they're not the same kind of tool, and the right choice depends on your team's profile more than the tools' feature lists.

What each tool actually does

It's worth being precise about categories because "IaC" gets used too loosely:

  • Terraform (HashiCorp, now BSL licensed): declarative, state-file-based cloud provisioning. You describe desired state; Terraform computes the diff and applies it.
  • Pulumi: same concept as Terraform, but you write real code (Python, Go, TypeScript) instead of HCL.
  • Ansible: imperative, agentless configuration management. Strong for day-2 ops (installing packages, configuring services), weaker for provisioning.

These aren't identical tools competing for the same niche.

When Ansible makes sense — and when it doesn't

Ansible is solid for: configuring existing servers, deploying applications, running ordered multi-step procedures across a fleet.

It's awkward for: managing cloud resources from scratch. The modules exist (for EC2, VPCs, RDS), but state management is painful. Ansible doesn't track what it created — each run answers "did this task succeed?", not "does this resource still exist?". You end up with orphaned resources and no clear inventory.

For a small team provisioning cloud infra, Ansible alone isn't sufficient. The pattern that actually works: provision with Terraform, configure with Ansible.

# playbook.yml — configure a Python app on a freshly provisioned EC2 instance
- name: Deploy Python app
  hosts: web
  become: yes
  tasks:
    - name: Install system dependencies
      apt:
        name: [python3, python3-pip, nginx]
        state: present
        update_cache: yes

    - name: Deploy application files
      copy:
        src: ./app/
        dest: /opt/myapp/
        owner: www-data

    - name: Install Python requirements
      pip:
        requirements: /opt/myapp/requirements.txt
        executable: pip3

    - name: Enable and start service
      systemd:
        name: myapp
        state: started
        enabled: yes
Enter fullscreen mode Exit fullscreen mode

This works well. But you need Terraform or Pulumi to create that instance in the first place.

Terraform: the safe default

Terraform has been the standard for cloud provisioning for years. HCL is learnable in a day, the provider ecosystem covers AWS, GCP, Azure, Cloudflare, GitHub, Datadog, and hundreds more. The plan/apply workflow gives you an explicit diff to review before any change lands.

For a small team, the main advantages:

  • Anyone can read and write HCL without a deep programming background
  • terraform plan output is reviewable in PRs — non-engineers can spot obvious regressions
  • State locking via S3 + DynamoDB prevents concurrent applies from corrupting state
  • The BSL license change only affects vendors building Terraform-as-a-service; internal use remains free

The friction:

  • HCL is not a real programming language. Complex logic via for_each, dynamic blocks, and conditionals becomes unwieldy at scale
  • No native test framework — you need Terratest or heavy terraform validate discipline
  • State drift is a real burden: manual cloud console changes require terraform import or state rm
# main.tf — minimal VPC + EC2 for a Go API on AWS
resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
  tags       = { Name = "main" }
}

resource "aws_subnet" "public" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.1.0/24"
  availability_zone = "eu-west-1a"
}

resource "aws_security_group" "api" {
  name   = "api-sg"
  vpc_id = aws_vpc.main.id

  ingress {
    from_port   = 8080
    to_port     = 8080
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_instance" "api" {
  ami                    = "ami-0c55b159cbfafe1f0"
  instance_type          = "t3.small"
  subnet_id              = aws_subnet.public.id
  vpc_security_group_ids = [aws_security_group.api.id]
  tags                   = { Name = "go-api" }
}
Enter fullscreen mode Exit fullscreen mode

Readable, reviewable, and straightforward for any team member — including whoever joined last month.

Pulumi: real code, real tradeoffs

Pulumi replaces HCL with actual programming languages. The same stack in Python:

import pulumi
import pulumi_aws as aws

vpc = aws.ec2.Vpc("main",
    cidr_block="10.0.0.0/16",
    tags={"Name": "main"}
)

subnet = aws.ec2.Subnet("public",
    vpc_id=vpc.id,
    cidr_block="10.0.1.0/24",
    availability_zone="eu-west-1a"
)

sg = aws.ec2.SecurityGroup("api-sg",
    vpc_id=vpc.id,
    ingress=[{
        "from_port": 8080, "to_port": 8080,
        "protocol": "tcp", "cidr_blocks": ["0.0.0.0/0"]
    }],
    egress=[{
        "from_port": 0, "to_port": 0,
        "protocol": "-1", "cidr_blocks": ["0.0.0.0/0"]
    }]
)

instance = aws.ec2.Instance("go-api",
    ami="ami-0c55b159cbfafe1f0",
    instance_type="t3.small",
    subnet_id=subnet.id,
    vpc_security_group_ids=[sg.id],
    tags={"Name": "go-api"}
)

pulumi.export("instance_id", instance.id)
Enter fullscreen mode Exit fullscreen mode

The real appeal: you can loop over a list of environments without count gymnastics, write unit tests with pytest, reuse Python functions, and import your own internal libraries. For infra with 10+ environments or heavily dynamic configuration, Pulumi's expressiveness is a genuine win.

The tradeoffs:

  • State goes to Pulumi Cloud by default — there's a free tier, but self-hosting requires more setup than Terraform's S3 backend
  • Steeper onboarding: new team members need to know the language and Pulumi's async resource graph model. Debugging Output[T] types is not obvious
  • Smaller community: Stack Overflow coverage and available modules are thinner than Terraform's

Practical decision tree for small teams

Use Ansible when you're configuring existing servers, not provisioning from scratch.

Use Terraform when:

  • Your team has mixed backgrounds — not everyone writes Python or Go fluently
  • Your infra is relatively stable: fewer than five environments, no heavy dynamic resource generation
  • You want the largest provider ecosystem and the most community resources

Use Pulumi when:

  • Your team is developer-heavy and HCL feels like a cognitive tax
  • You need real programming constructs: dynamic loops, proper unit tests, shared libraries across stacks
  • You're comfortable with Pulumi Cloud pricing or willing to configure a self-hosted state backend

For most teams under five engineers, Terraform is the right starting point. The plan/apply review workflow fits naturally into PRs, HCL is fast to learn, and you won't hit its limits until your infra grows significantly.

One thing that matters regardless of tool: gate every apply behind CI/CD with mandatory code review on infra changes. A misconfiguration merged without review is where incidents start. We maintain a security checklist for IaC pipelines covering state file access control, least-privilege IAM for CI runners, and safe secrets handling in both Terraform and Pulumi.

The takeaway

Terraform, Pulumi, and Ansible aren't direct competitors — they solve overlapping but distinct problems. Ansible handles configuration; Terraform and Pulumi handle provisioning. Between the two provisioning tools, Terraform wins on simplicity and ecosystem breadth; Pulumi wins when your infra logic outgrows what HCL can express cleanly.

Pick based on your team's actual skills and infra complexity, not on which tool had the best talk at the last DevOps conference.


I run AYI NEDJIMI Consultants, a cybersecurity consulting firm. We publish free security hardening checklists — PDF and Excel.

Top comments (0)