Skip to content
Log in Sign up My account

6.7 Agent-Based and Agentless Orchestration Tools

Blueprint 6.7: Compare agent vs. agentless orchestration tools, such as Chef, Puppet, Ansible, and SaltStack

  • Agent-based versus agentless: what runs on the managed node, who starts the connection, push versus pull
  • Puppet, Chef, SaltStack and Ansible: architecture, language, transport, terminology
  • Declarative versus procedural definitions, idempotence and configuration drift
  • Why Ansible is the usual choice for network devices

The verb is compare. Expect drag-and-drop items that match each tool to its model, language or component names, and multiple-choice items about which tool fits a scenario. You will not be asked to write a playbook on the exam, but reading one makes the comparison concrete.

Why configuration management

Configuration management tools describe the desired state of many systems in text files, keep those files in version control, and apply them consistently. They replace logging in to devices one by one. The benefits are the same whichever tool you use: repeatable changes, fewer typing errors, a record of who changed what (in Git), and the ability to detect and correct configuration drift, where a device has been changed by hand away from the intended state.

A key property is idempotence: applying the same definition twice produces the same result, and the second run changes nothing because the system is already in the desired state. Well-written modules check before they change.

Agent-based versus agentless

graph TB
  subgraph AB ["Agent-based, typically pull"]
    S1["Server<br/>Puppet server or Chef Infra Server"] 
    A1["Agent on node 1"] -->|"polls every 30 min,<br/>fetches its config"| S1
    A2["Agent on node 2"] -->|"polls"| S1
  end
  subgraph AL ["Agentless, push"]
    CN["Ansible control node"] -->|"SSH or API, on demand"| N1["Router"]
    CN -->|"SSH or API"| N2["Switch"]
  end
Agent-based Agentless
Software on the managed node An agent (daemon) must be installed and kept updated Nothing extra; uses SSH, NETCONF or an HTTPS API the device already has
Who starts the connection Usually the agent (pull) The control node (push)
When changes apply On the agent's schedule, or when triggered When an operator or pipeline runs the job
Drift correction Automatic on every agent run Only when the job is run again
Scale Very large fleets; the agents do the work Large, but the control node opens every connection
Network devices Hard: most routers and switches cannot run a third-party agent Natural fit: SSH and APIs are already there
Examples Puppet, Chef, SaltStack (minion) Ansible, SaltStack (salt-ssh), Puppet Bolt

Exam trap

Push versus pull is not the same question as agent versus agentless, but the exam pairs them. Puppet and Chef: agent, pull. Ansible: agentless, push. SaltStack: agent (minion), with the master pushing commands over a message bus, plus an agentless SSH mode. If an option says Ansible "requires an agent on each switch", it is wrong.

The four tools

Puppet

Item Detail
Model Agent-based, pull. The Puppet agent contacts the Puppet server (formerly "master") on TCP 8140, by default every 30 minutes
Language Puppet DSL (a Ruby-based declarative language). Files are manifests (.pp), grouped into modules
How it works The agent sends facts about the node (gathered by Facter); the server compiles a catalog of desired state for that node; the agent applies it and reports back
Style Declarative: you describe the end state, Puppet works out the order from dependencies
Network devices Through a proxy model (puppet device), where a proxy agent manages devices that cannot run an agent
node 'ntp-client.example.com' {
  package { 'chrony':
    ensure => installed,
  }
  service { 'chronyd':
    ensure  => running,
    enable  => true,
    require => Package['chrony'],
  }
}

Chef

Item Detail
Model Agent-based, pull. Chef Infra Client (chef-client) on each node pulls from the Chef Infra Server over HTTPS
Language Ruby DSL. Instructions are recipes, packaged in cookbooks; resources such as package and service
Workstation Engineers write and test cookbooks on a workstation and upload them with the knife tool
Style Usually described as procedural (imperative): resources in a recipe run in the order written
package 'chrony'

service 'chronyd' do
  action [:enable, :start]
end

SaltStack (Salt)

Item Detail
Model Agent-based. A Salt master controls minions over a ZeroMQ message bus on TCP 4505 (publish) and 4506 (return). The master pushes commands; minions can also apply states on a schedule
Language State files (SLS) in YAML, templated with Jinja. Written in Python
Terminology Grains: facts about a minion. Pillar: secure per-minion data from the master. Reactor: actions triggered by events on the bus
Agentless options salt-ssh runs over SSH with no minion; proxy minions manage network devices that cannot run a minion
Style Declarative states plus imperative remote execution (salt '*' cmd.run ...)
chrony:
  pkg.installed: []
  service.running:
    - name: chronyd
    - enable: True
    - require:
      - pkg: chrony

Ansible

Item Detail
Model Agentless, push. A control node (Linux with Python) connects to managed nodes when a job runs
Transport SSH for servers; for network devices, connection plugins such as network_cli (SSH to the CLI), netconf, and httpapi (REST APIs)
Language Playbooks in YAML. A playbook contains plays; a play targets hosts from the inventory and runs tasks; each task calls a module. Templates use Jinja2
Reuse Roles and collections (such as cisco.ios, cisco.nxos, cisco.dnac for Catalyst Center)
Style Tasks run in order (procedural flow), but most modules are declarative and idempotent
Scale and GUI AWX and Red Hat Ansible Automation Platform (formerly Ansible Tower) add a web UI, RBAC, scheduling and an API

Side-by-side

Puppet Chef SaltStack Ansible
Agent Yes (agent) Yes (chef-client) Yes (minion), or salt-ssh No
Push or pull Pull Pull Push (master to minions) Push
Central server Puppet server Chef Infra Server Salt master Control node (no daemon needed)
Definitions Manifests Recipes in cookbooks States (SLS) Playbooks
Language Puppet DSL Ruby DSL YAML plus Jinja YAML plus Jinja2
Transport HTTPS, TCP 8140 HTTPS ZeroMQ, TCP 4505/4506 SSH, NETCONF, HTTPS APIs
Written in Ruby (and Clojure for the server) Ruby (and Erlang for the server) Python Python
Node facts Facts (Facter) Ohai attributes Grains Facts (gather_facts)
graph LR
  Q1{"Can the device run an agent?"} -->|"no: router, switch"| ANS["Ansible, or salt-ssh / proxy minion"]
  Q1 -->|"yes: Linux servers"| Q2{"Need continuous drift correction?"}
  Q2 -->|"yes"| PULL["Puppet or Chef agent runs"]
  Q2 -->|"event-driven, fast remote exec"| SALT["SaltStack minions"]

Exam trap

Match the language to the tool carefully. Ruby: Chef recipes (and Puppet DSL is Ruby-based). YAML: Ansible playbooks and Salt states. A distractor that says "Ansible playbooks are written in Python" is wrong: Ansible itself is written in Python, but playbooks are YAML.

Exam trap

The term "manifest" belongs to Puppet, "cookbook" and "recipe" to Chef, "state", "grain" and "pillar" to Salt, and "playbook", "inventory" and "module" to Ansible. Drag-and-drop items often mix these.

Reading an Ansible playbook for IOS XE

[routers]
r1 ansible_host=192.168.100.1

[routers:vars]
ansible_connection=ansible.netcommon.network_cli
ansible_network_os=cisco.ios.ios
ansible_user=admin
ansible_password=Cisco123!
---
- name: Configure NTP on IOS XE routers
  hosts: routers
  gather_facts: false

  tasks:
    - name: Add the NTP server
      cisco.ios.ios_config:
        lines:
          - ntp server 192.168.100.10

    - name: Collect NTP associations
      cisco.ios.ios_command:
        commands:
          - show ntp associations
      register: ntp_out

    - name: Print the output
      ansible.builtin.debug:
        var: ntp_out.stdout_lines

How to read it: the play targets the routers group from the inventory. ansible_connection=ansible.netcommon.network_cli tells Ansible to log in over SSH to the CLI, and ansible_network_os picks the IOS command handling. ios_config compares the requested line with the running configuration and only sends it if it is missing, which is what makes the task idempotent. register stores a task's result in a variable for later tasks. In production the password would come from Ansible Vault, not plain text.

Lab: run an idempotent Ansible playbook

Topology: AUTO (Ubuntu, 192.168.100.50) as the Ansible control node, R1 (C8000v or IOSv) at 192.168.100.1 with SSH enabled.

graph LR
  AUTO["AUTO<br/>Ansible control node"] -- "SSH TCP 22, 192.168.100.0/24" --- R1["R1<br/>192.168.100.1"]
  1. Enable SSH on R1:

    hostname R1
    ip domain name lab.local
    crypto key generate rsa modulus 2048
    username admin privilege 15 secret Cisco123!
    line vty 0 4
     login local
     transport input ssh
    

    Confirm from AUTO with ssh [email protected].

  2. On AUTO, install Ansible in a Python virtual environment (python3 -m venv ~/ans && ~/ans/bin/pip install ansible paramiko). The full ansible package includes the cisco.ios and ansible.netcommon collections. Save the two files above.

  3. Run the playbook:

    $ ansible-playbook -i inventory.ini ntp.yml
    
    PLAY [Configure NTP on IOS XE routers] ****************************************
    
    TASK [Add the NTP server] *****************************************************
    changed: [r1]
    
    TASK [Collect NTP associations] ***********************************************
    ok: [r1]
    
    TASK [Print the output] *******************************************************
    ok: [r1] => {
        "ntp_out.stdout_lines": [
            [
                "  address         ref clock       st   when   poll reach  delay  offset   disp",
                " ~192.168.100.10  .INIT.          16      -     64     0  0.000   0.000 15937.",
                " * sys.peer, # selected, + candidate, - outlyer, x falseticker, ~ configured"
            ]
        ]
    }
    
    PLAY RECAP ********************************************************************
    r1   : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
    

    The NTP output depends on whether 192.168.100.10 exists in your lab; an unreachable server shows .INIT. and stratum 16.

  4. Run the same command again. The first task now reports ok instead of changed, and the recap shows changed=0. That is idempotence: the line is already present, so nothing is sent.

  5. Create drift: on R1, no ntp server 192.168.100.10. Run the playbook a third time and confirm it reports changed=1 and restores the line. Note that an agentless tool corrects drift only when you run it; a Puppet agent would have done it on its next scheduled run.
  6. Break it on purpose: change ansible_password to a wrong value and run again. The recap shows unreachable=1 (or failed=1, depending on the version), and the error names an authentication failure.

Quick check

Which tool uses minions and grains?

SaltStack. Minions are the agents, and grains are facts about each minion.

A team must automate 400 switches that cannot run third-party software and wants to start without deploying a server. Which tool fits best?

Ansible, because it is agentless and runs from a control node over SSH or APIs with no permanent server component.

What is the default model for Puppet: push or pull, agent or agentless?

Agent-based pull. The Puppet agent contacts the Puppet server on TCP 8140, by default every 30 minutes.

References