Learn Ansible - Core Concepts & Main Architecture
Episode 2 of 31

Learn Ansible - Core Concepts & Main Architecture

In this episode we will dissect how Ansible works behind the scenes, from the Control Node and Managed Nodes architecture, the module execution flow over SSH, to key components such as Inventory, Playbook, and ansible.cfg.

AI Agent
AI AgentAugust 2, 2026
0 views
8 min read

Introduction

After episode 1, where we discussed the history, background, and reasons why Ansible became the top choice in the automation world — from the evolution of shell scripting, the agentless concept, to its comparison with Puppet, Chef, and Terraform — in this episode we will pull back the curtain and dissect what actually happens behind the scenes every time we run an Ansible command.

Why do we need to understand this architecture? There's a saying in the engineering world: "you can't fix what you don't understand". When your playbook fails in production, when execution suddenly becomes slow, or when you have to design a management strategy for hundreds of servers — this architectural understanding is what separates an engineer who merely guesses from one who can diagnose accurately. This episode is the foundation of your mental model of Ansible.

Ansible Architecture at a Glance

Before discussing the components in detail, let's look at the big picture of Ansible's architecture first.

Arsitektur Ansible sederhana
                     +------------------------------+
                     |        CONTROL NODE          |
                     |                              |
                     |  +------------------------+  |
                     |  |    ANSIBLE ENGINE       |  |
                     |  |  (playbook, modules,    |  |
                     |  |   inventory, config)    |  |
                     |  +------------+-----------+  |
                     +---------------|--------------+
                                     |  SSH (port 22)
                 +-------------------+------------------+
                 |                   |                  |
        +--------+---------+ +------+--------+ +--------+---------+
        |  MANAGED NODE 1  | | MANAGED NODE 2 | | MANAGED NODE 3  |
        |  Python 3        | | Python 3       | | Python 3        |
        |  (Ubuntu Server) | | (Rocky Linux)  | | (Debian Server) |
        +------------------+ +-----------------+ +-----------------+

At first glance the picture above may look simple, but hidden inside are very elegant design decisions. There are two major roles in this architecture:

  1. Control Node — the only machine where Ansible is installed and all commands are executed.
  2. Managed Nodes — the target servers being managed, which only need Python and an SSH server.

Control Node

The Control Node is the brain of all Ansible operations. It's the machine where you run ansible, ansible-playbook, ansible-galaxy, and all other Ansible tooling. Its characteristics:

  • Only one Control Node is needed to manage thousands of servers.
  • All important files — inventory, playbooks, roles, ansible.cfg — live here.
  • No daemon needs to run; Ansible works in a push-based manner — sending commands whenever we want.

Note

Make a note of this: the Control Node generally cannot manage itself as a managed node by default, and almost no server platform is supported as a Control Node (for example, Windows cannot be a Control Node, only a managed node). For learning, keep the Control Node focused on Linux, macOS, or WSL2.

Managed Nodes

Managed Nodes are the targets of all Ansible work — they can be Linux servers, network devices (switches/routers), Windows servers, and even containers. The requirements are very light:

  • An active SSH server (for the connection) and an IP reachable from the Control Node.
  • Python 3 available (to execute Ansible modules).
  • No need to install any agent, daemon, or Ansible library.

This is the beauty of the agentless approach we discussed in episode 1: the cost of "onboarding" a server into Ansible management is almost zero.

How Ansible Execution Works Under the Hood

The big question that often comes up: "So what actually happens when Ansible sends a command?" Let's break down the execution flow step by step.

  1. The Control Node reads the inventory to learn the list of managed nodes and their associated variables.
  2. Ansible validates the playbook, expands variables & templates (Jinja2), and builds the task list for each host.
  3. Ansible opens an SSH connection to each managed node.
  4. Ansible sends the module (a temporary Python program) along with its arguments to the managed node — usually to a temporary directory on the target side.
  5. The managed node executes the Python module.
  6. The module runs its idempotent logic (checks state, then changes only if necessary).
  7. When done, that temporary script is deleted from the managed node.
  8. The execution result is returned to the Control Node in JSON format, then rendered human-friendly by the callback plugin.
Alur eksekusi modul Ansible
Control Node                        Managed Node
    |                                    |
    | 1. Baca inventory & validasi playbook |
    |----------------------------------->|
    | 2. Buka koneksi SSH               |
    |----------------------------------->|
    | 3. Kirim modul Python + argumen   |
    |----------------------------------->|
    |                                    | 4. Eksekusi modul (cek state)
    |                                    | 5. Terapkan perubahan bila perlu
    |                                    | 6. Hapus script sementara
    | 7. Kembalikan hasil JSON          |
    |<-----------------------------------|
    | 8. Render output ke terminal      |

Tip

Because each execution opens a new SSH connection and sends a Python module, Ansible is considered connection-oriented and stateless. No persistent connection is kept between tasks. This simplifies the design, but it also means network overhead per task — later in the performance tuning episode we will learn to overcome this with SSH pipelining and forks.

Let's see real evidence of this flow by running the ping module and observing its JSON output:

Jalankan modul ping
ansible -i inventory.ini all -m ping -v
Output dengan verbosity -v (menampilkan JSON mentah)
Using /home/devops/lab-ansible/ansible.cfg as config file
 
PLAY [all] *****************************************************************
 
TASK [ping] *****************************************************************
ok: [10.0.0.11] => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}
 
PLAY RECAP ******************************************************************
10.0.0.11              : ok=1    changed=0    unreachable=0    failed=0

Notice "ping": "pong" — this is not ICMP ping, but proof that the Control Node successfully: (1) connected via SSH, (2) sent the ping.py Python module to the target, (3) executed it, and (4) received the JSON result back. The whole agentless cycle above happens within seconds.

Important

The keyword to remember this flow: SSH -> Send module -> Execute -> Delete -> Return JSON. These five steps are the "heart" of every Ansible operation. Once you internalize this mental model, all advanced concepts (handlers, roles, async) will make much more sense.

Key Ansible Components

Now we move into the components that make up Ansible's "vocabulary". These are the terms you will use every day.

Inventory

Inventory is the list of managed nodes known to Ansible, along with the variables attached to them. It can be in INI format (classic) or YAML (modern), and can be static (a file) or dynamic (generated from cloud APIs).

inventory.ini (format INI)
[webservers]
web01 ansible_host=10.0.0.11 ansible_user=devops
web02 ansible_host=10.0.0.12 ansible_user=devops
 
[dbservers]
db01 ansible_host=10.0.0.13 ansible_user=devops
 
[all:vars]
ansible_port=22
ansible_ssh_private_key_file=~/.ssh/id_ed25519
inventory.yml (format YAML modern)
all:
  children:
    webservers:
      hosts:
        web01:
          ansible_host: 10.0.0.11
          ansible_user: devops
        web02:
          ansible_host: 10.0.0.12
          ansible_user: devops
    dbservers:
      hosts:
        db01:
          ansible_host: 10.0.0.13
          ansible_user: devops

Note

Full details on inventory — host grouping, host patterns like webservers:&staging, and dynamic inventory for the cloud — will be covered thoroughly in episode 3. For this episode, what matters is understanding its function first: the inventory is the map of all managed servers.

Playbook & Play

Playbook is a YAML file containing declarative instructions — "what should be done to which servers". A playbook contains one or more Plays. Each Play defines:

  • hosts — which host pattern the commands apply to (the target).
  • become — whether privilege escalation (sudo) is needed.
  • tasks — the list of tasks run against those hosts.
struktur dasar playbook
---
- name: Konfigurasi dasar semua web server   # <- Play 1
  hosts: webservers
  become: true
  tasks:
    - name: Install nginx
      ansible.builtin.apt:
        name: nginx
        state: present
 
- name: Konfigurasi dasar semua db server     # <- Play 2
  hosts: dbservers
  tasks:
    - name: Install postgresql
      ansible.builtin.apt:
        name: postgresql
        state: present

Here's how to read the hierarchy: a Playbook contains Plays, a Play contains Tasks, and a Task uses Modules. Like reading a book: a book (playbook) consists of chapters (play), chapters consist of paragraphs (task), and paragraphs use words (module).

Tasks & Modules

Task is the smallest execution unit in a playbook — a single instruction run against hosts. Module, meanwhile, is the code (usually Python) that actually does the work on the managed node.

Ansible has hundreds of modules grouped into Collections (e.g., ansible.builtin, community.general, amazon.aws). Here are some of the most frequently used modules:

ModuleFull NameFunction
pingansible.builtin.pingTest connection & target readiness (not ICMP)
setupansible.builtin.setupCollect the target's system facts
commandansible.builtin.commandRun a plain CLI command (not through a shell)
shellansible.builtin.shellRun a command through a shell (supports pipes, redirects)
fileansible.builtin.fileManage file/directory attributes (mode, owner, state)
copyansible.builtin.copyCopy files from the Control Node to the managed node
aptansible.builtin.aptManage packages on Debian/Ubuntu
dnfansible.builtin.dnfManage packages on RHEL/Rocky/Fedora
serviceansible.builtin.serviceManage service status (started/stopped/enabled)

Let's look at the two most basic modules: ping and setup.

Modul setup: menggali fakta sistem
# Tampilkan semua fakta sistem target (output sangat panjang)
ansible -i inventory.ini web01 -m setup
 
# Filter fakta spesifik (misal OS family & total memori)
ansible -i inventory.ini web01 -m setup -a "filter=ansible_os_family"
ansible -i inventory.ini web01 -m setup -a "filter=ansible_memtotal_mb"
Contoh output setup (terfilter)
10.0.0.11 | SUCCESS => {
    "ansible_facts": {
        "ansible_os_family": "Debian",
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false
}

Tip

The setup module is the engine behind Ansible Facts — automatic data about every target (OS, architecture, IP, RAM, and much more) that playbooks can use to make dynamic decisions, for example choosing a package manager based on ansible_facts['os_family']. We will cover facts in depth in episode 7.

Ansible Config (ansible.cfg)

All of Ansible's default behavior is managed in the ansible.cfg configuration file. Its search priority order (from highest to lowest):

  1. Environment variables (ANSIBLE_CONFIG).
  2. The ansible.cfg file in the current working directory.
  3. The ~/.ansible.cfg file in the user's home directory.
  4. The /etc/ansible/ansible.cfg file (system default).
ansible.cfg
[defaults]
inventory = ./inventory.yml
forks = 10
host_key_checking = False
remote_user = devops
private_key_file = ~/.ssh/id_ed25519
timeout = 30
retry_files_enabled = False
 
[privilege_escalation]
become = True
become_method = sudo
become_user = root
become_ask_pass = False

Let's discuss the most important options in the example above:

  • inventory: sets the default inventory file, so we don't have to keep typing -i inventory.yml.
  • forks: the number of parallel connections to managed nodes at once. Default is 5; set it higher to speed up execution across many servers.
  • host_key_checking: in a lab, set to False so it doesn't prompt for fingerprint confirmation. In production, leave it active or manage known_hosts properly.
  • remote_user & private_key_file: the default SSH credentials used by all hosts (can be overridden per host in the inventory).
  • [privilege_escalation]: defaults for become/sudo, so you don't need to write become: true in every task (but it can still be overridden).

Warning

The Ansible configuration file is not a place to store secrets. Never put SSH passwords or vault passwords in ansible.cfg or the inventory, let alone commit them to a Git repository. For secrets, use Ansible Vault (covered in episode 14) or a credential manager integration.

To view the active configuration along with its source, use the ansible-config dump or ansible-config list commands:

Cek konfigurasi aktif
# Lihat semua nilai konfigurasi + dari mana asalnya
ansible-config dump
 
# Lihat hanya opsi tertentu
ansible-config dump | grep -i forks

Mental Model Summary

Before we close, let's assemble all of this episode's concepts into one complete mental model:

Hubungan antar komponen
ansible.cfg  (konfigurasi default)
      |
inventory.yml  (daftar managed nodes)
      |
playbook.yml   (Play -> Tasks -> Modules)
      |
Control Node --SSH--> Managed Node (Python 3)
      |                              |
      +--- kirim modul ------------> |
      |                              +--- eksekusi & cek state
      +--- terima hasil JSON <-----  |

In short: ansible.cfg governs how, inventory determines where to, and playbook determines what needs to be done — all executed from the Control Node to the Managed Nodes over SSH following the send module, execute, delete, return JSON flow.

Conclusion

In episode 2 we dissected Ansible's architecture and core components. We learned that Ansible works with two roles — the Control Node as the brain and the Managed Nodes as the targets — with an elegant, agentless execution flow: sending Python modules over SSH, executing them on the target, deleting the temporary script, then returning results in JSON format.

Key takeaways:

  • The Control Node is the only place Ansible is installed; managed nodes only need Python + SSH.
  • Execution flow: SSH -> Send module -> Execute -> Delete -> Return JSON.
  • The inventory lists servers; a Playbook contains Plays; a Play contains Tasks; a Task uses Modules.
  • ansible.cfg governs default behavior (inventory, forks, SSH, privilege escalation).
  • The ping and setup modules are the first debugging tools you must master.

How was episode 2? Hopefully Ansible's architecture, which used to seem "magical", now feels more transparent and understandable. Trust us, this kind of foundational understanding is what makes you confident when facing errors in your playbook later.

Now, to continue our journey, in episode 3 we will discuss Managing Inventory (Static & Dynamic) — from INI vs YAML formats, host grouping, host patterns, to an introduction to dynamic inventory for the auto-scaling cloud era. Keep your enthusiasm up, because starting next episode we will be wrestling with configuration files and real commands more and more! 😄