Ansible vs Chef vs Puppet vs SaltStack: Choose the Right Config Tool for Production
Ansible vs Chef vs Puppet vs SaltStack: Compare architecture, performance, and failure modes.
20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.
- ✓Basic familiarity with configuration management concepts. No prior experience with any specific tool required.
Choose Ansible for simplicity and agentless SSH-based management. Choose Chef for complex Ruby-driven automation. Choose Puppet for large-scale declarative state management. Choose SaltStack for high-speed event-driven orchestration.
Think of these tools as different ways to give instructions to a team of sysadmins. Ansible is like giving a written checklist that each admin reads and follows on the spot — no prep needed. Chef is like writing a full recipe book that each admin installs and follows step-by-step. Puppet is like setting up a rulebook that admins check every few minutes to keep things consistent. SaltStack is like having a walkie-talkie to shout commands instantly to everyone at once.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
Most teams pick a config management tool based on hype, not hard production data. I've seen a 500-node cluster melt down because the team chose Puppet for its 'maturity' but didn't understand its pull-model latency. Another team spent six months fighting Chef's Ruby DSL when all they needed was a simple playbook. The wrong choice costs you not just time, but outages.
This article gives you a decision framework based on real failure modes: agent overhead, push vs pull latency, language complexity, and scaling ceilings. You'll learn exactly which tool fits your team size, infrastructure scale, and operational style.
By the end, you'll be able to explain to your CTO why Ansible is the right call for a 50-node startup, but why a 2000-node bank should look at SaltStack or Puppet. You'll also know the exact gotchas that burn people in production — like Chef's search performance or Ansible's SSH fork bomb.
Why Architecture Matters More Than Features
Before you compare playbooks vs recipes, understand the fundamental architectural split: push vs pull and agent vs agentless. This single decision determines your latency, scalability, and failure modes.
Ansible is agentless push: it SSHes into nodes and runs modules. No agent to install, but every run is a fresh SSH connection. For 50 nodes, it's fine. For 5000, SSH multiplexing still creates a thundering herd on the control node.
Chef and Puppet are pull-based with agents. The agent runs on each node, polls a central server, and applies the desired state. This scales because the server doesn't initiate connections. But it introduces latency: a node might be misconfigured for up to 30 minutes before the next pull.
SaltStack is hybrid: it uses ZeroMQ for high-speed push but also supports pull via minions. It's the fastest for ad-hoc commands but adds a message queue dependency.
Here's the production truth: if you need instant remediation (e.g., block an IP across all nodes), push wins. If you need eventual consistency without a central bottleneck, pull wins. Choose based on your latency SLA, not your feature checklist.
Language and Learning Curve: YAML vs Ruby vs DSL
The language barrier is the #1 reason teams abandon a config tool. Chef uses Ruby — full Ruby, with all its metaprogramming power and all its debugging pain. Puppet uses its own DSL, which is simpler but limited. Ansible uses YAML — declarative, no logic, easy to read. SaltStack uses YAML with Jinja2 templating, plus Python for custom modules.
Here's the real-world trade-off: Chef's Ruby lets you write complex logic (loops, conditionals, data transformations) natively. But that power means your cookbooks can become unreadable spaghetti. I've seen a 400-line recipe that could have been 20 lines of Ansible YAML.
Ansible's YAML is deliberately limited. You can't write a for loop in pure Ansible — you use Jinja2 or lookup plugins. That's a feature, not a bug: it forces simplicity. But when you need dynamic behavior (e.g., iterate over a list of users with different SSH keys), you'll hit a wall.
SaltStack strikes a good balance: YAML for state definitions, Python for custom modules. Your team can write simple states without coding, and drop into Python when needed.
Puppet's DSL is Ruby-inspired but not Ruby. It's easy to learn but frustrating when you need to do something the DSL doesn't support — then you write custom facts or functions in Ruby, which feels like a hack.
Bottom line: if your team is strong in Ruby, Chef is powerful. If you want maximum simplicity, Ansible. If you want flexibility without full Ruby, SaltStack.
Performance at Scale: When SSH Becomes the Bottleneck
Ansible's SSH-based push is the simplest model, but it doesn't scale linearly. Each node requires a new SSH connection (even with multiplexing). On a 1000-node run, the control node's CPU and file descriptors become the bottleneck. I've seen Ansible runs take 30 minutes for 2000 nodes — and that's with forks=50.
SaltStack uses ZeroMQ, a high-performance messaging library. It maintains persistent connections to minions, so ad-hoc commands are near-instant. A 'salt '*' test.ping' on 5000 nodes returns in under a second. The trade-off: you need to manage the ZeroMQ broker and keep minion connections alive.
Chef and Puppet scale differently because the load is distributed: each node runs its own agent, which pulls from the server. The server's bottleneck is database queries (Chef's PostgreSQL, Puppet's PostgreSQL or PuppetDB). Chef search, as we saw, kills performance. Puppet's catalog compilation can be slow for complex manifests.
Here's a rule of thumb: under 500 nodes, any tool works. 500-2000, avoid Ansible for frequent runs (more than once per hour). 2000+, use SaltStack or Puppet with careful tuning. Chef can scale to 10,000+ but requires serious PostgreSQL optimization.
I once consulted for a company running Ansible on 3000 nodes every 15 minutes. Their control node was a 32-core machine with 64GB RAM, and it still hit 100% CPU during runs. They switched to SaltStack and the same workload ran in 2 minutes.
Idempotency and State Management: The Real Differentiator
All four tools claim idempotency — running the same config twice should produce the same result. But they achieve it differently, and the differences matter in production.
Ansible modules are idempotent by design: each module checks the current state and only makes changes if needed. But Ansible has no persistent state — it doesn't know what happened last run. This means every run starts from scratch, which is fine for simple tasks but wasteful for complex ones.
Chef and Puppet maintain a local state file (node object in Chef, YAML facts in Puppet). They know what was applied last time and can skip unchanged resources. This makes them faster for repeated runs, but the state can get out of sync (e.g., if someone manually edits a file).
SaltStack uses a similar approach: it maintains a state cache on each minion. It also supports 'test=True' to dry-run changes.
The production implication: if you run config every 30 minutes, Chef/Puppet/SaltStack's state awareness saves time. If you run ad-hoc once a week, Ansible's simplicity wins.
But here's the gotcha: Chef's state is stored in the node object on the server. If the server is down, the agent can't run (it caches the last run, but new runs fail). Puppet's agent can run without a server using cached catalogs. Ansible has no such dependency — it just needs SSH access.
Community and Ecosystem: Modules, Cookbooks, and Forge
The ecosystem matters because you don't want to reinvent the wheel. Ansible Galaxy has 20,000+ roles. Chef Supermarket has 7,000+ cookbooks. Puppet Forge has 6,000+ modules. SaltStack's community is smaller but growing.
But quantity isn't quality. I've used Ansible roles that were broken on RHEL 8 because they assumed Ubuntu. Chef cookbooks that pinned versions from 2015. The reality is: you'll write custom modules for your specific stack anyway.
Here's what to look for: active maintenance, support for your OS, and clear documentation. Ansible Galaxy has the most active community, but many roles are shallow. Chef's cookbooks tend to be more thorough because they're often maintained by companies (e.g., Chef's own nginx cookbook).
Puppet's modules are well-documented but often tied to Puppet Enterprise features. SaltStack's formulas are hit-or-miss.
My advice: don't choose based on ecosystem size alone. All four have enough coverage for common stacks (nginx, PostgreSQL, Docker). You'll write custom stuff anyway.
When Not to Use These Tools: The Overkill Zone
Config management tools are overkill for small setups. If you have 5 servers and a simple stack, use shell scripts and a README. I've seen teams spend weeks learning Ansible for a 3-node app that could be managed with a 20-line bash script.
Also, don't use these tools for container orchestration. Kubernetes and Docker Compose are better for managing containers. Using Ansible to deploy Docker containers on a single host is like using a sledgehammer to crack a nut.
Another anti-pattern: using config management for secrets management. Ansible Vault, Chef Vault, and Puppet's encrypted data bags are okay for small teams, but for production, use HashiCorp Vault or AWS Secrets Manager.
Finally, if your infrastructure is ephemeral (auto-scaling groups, spot instances), consider using a tool like Terraform for provisioning and Packer for images. Config management on ephemeral nodes is often wasted effort — just bake the config into the AMI.
Interview Questions That Actually Get Asked
Here are the questions that separate juniors from seniors in config management interviews. Not 'What is Ansible?' but real scenarios.
- 'How does Chef handle a node that fails to converge? Does it retry? What happens to the node object?'
- Answer: Chef retries up to 5 times by default (configurable via
retriesattribute). If all retries fail, the run exits with an error, but the node object is still updated with partial state. This can cause drift. Senior engineers addignore_failureor useonly_ifguards to prevent partial updates. - 'You have 2000 nodes running Puppet. The Puppet master goes down. What happens?'
- Answer: Nodes with cached catalogs continue to apply them for up to 30 minutes (configurable via
runinterval). After that, they fail. The fix: run multiple Puppet masters behind a load balancer, or use Puppet Enterprise's high-availability mode. - 'Compare Ansible's strategy plugins with Chef's search. When would you use each?'
- Answer: Ansible's
strategyplugins (linear, free, debug) control execution order across nodes. Chef'ssearchqueries the server for node data. Use Ansible strategies for multi-node orchestration (e.g., rolling updates). Use Chef search for dynamic configuration (e.g., finding database hosts). But avoid Chef search at scale — use data bags instead.
serial parameter is a senior-level concept. It controls how many nodes are updated at once. Use it for zero-downtime rolling deployments. Default is serial: 0 (all nodes at once), which can cause full outage if a change breaks the service.The 500-Node Chef Search That Killed Our API
- Never use Chef search in production without indexing and timeouts.
- It's a distributed query that scales poorly beyond 100 nodes.
systemctl status sshd. 2. Verify SSH key permissions: chmod 600 ~/.ssh/id_rsa. 3. Test manually: ssh -i key user@host. 4. Add -vvv to ansible command for verbose SSH debug.search_timeout 10. 2. Convert search to data bag lookup. 3. Add indexes on the Chef server: chef-server-ctl reindex. 4. Reduce search frequency: use not_if or only_if guards.ntpdate -q pool.ntp.org. 2. Regenerate certificates: puppet cert clean <hostname> on master, then puppet agent -t on node. 3. Verify DNS resolution: dig +short <puppet-master-fqdn>.systemctl status salt-minion. 2. Verify master address in /etc/salt/minion. 3. Test connectivity: salt-call test.ping. 4. Restart minion: systemctl restart salt-minion. 5. Check firewall: port 4505 and 4506 must be open.systemctl status sshdssh -vvv -i key user@hostsystemctl restart sshd. Fix key permissions: chmod 600 ~/.ssh/id_rsa.| File | Command / Code | Purpose |
|---|---|---|
| ansible_push_vs_pull.devops | ansible all -i inventory.ini -m ping # Single SSH connection per node | Why Architecture Matters More Than Features |
| language_comparison.devops | - name: Install packages | Language and Learning Curve |
| scale_test.devops | ansible all -i inventory.ini -m ping --forks=50 2>&1 | tail -5 | Performance at Scale |
| idempotency_check.devops | - name: Ensure nginx is installed | Idempotency and State Management |
| install_from_galaxy.devops | ansible-galaxy install geerlingguy.nginx | Community and Ecosystem |
| when_not_to_use.devops | - name: Run nginx container | When Not to Use These Tools |
| interview_scenarios.devops | - name: Rolling update | Interview Questions That Actually Get Asked |
Key takeaways
Common mistakes to avoid
3 patternsChoosing a tool based on hype instead of requirements
Underestimating agent maintenance costs
Assuming one tool fits all workloads
Interview Questions on This Topic
How does Chef's node object differ from Ansible's fact gathering? What are the failure modes of each?
Frequently Asked Questions
20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.
That's Ansible. Mark it forged?
6 min read · try the examples if you haven't