Skip to content
DBDeependra Bhatta~/notes
Infrastructure as Code#ssh · #linux · #ansible · #configuration-management

Getting Started with Ansible: Install, Inventory and Ad-hoc Commands

Install Ansible in a Python virtual environment, connect Vagrant VMs with passwords and SSH keys, write a first inventory and manage packages with ad-hoc commands.

· updated · 10 min read
ON THIS PAGE

Shell and Python scripts work for one machine, but they become hard to maintain when the same change must reach hundreds of servers. Ansible applies that change from a single control node over SSH, with no agent on the managed machines. This guide installs Ansible on an Ubuntu Vagrant VM, connects it to other VMs with a password and then an SSH key, builds a first inventory, and uses ad-hoc commands to install Apache, manage its service and copy a file.

Configuration management: push vs pull

Configuration management keeps servers in a known, desired state for their whole life: the right packages, files, users and services. Every tool has a central place where the configuration lives and a set of managed machines (nodes). Tools differ in how the configuration reaches the nodes.

Diagram comparing push-based tools, where the server sends config to nodes, with pull-based tools, where nodes fetch it

ModelHow it worksExamples
PullAn agent runs on every node. At a regular interval it fetches the configuration from the server, compares it with the node, and fixes any difference.Puppet, Chef, CFEngine
PushThe control machine connects to the nodes and pushes the changes. The control machine starts the conversation, not the nodes.Ansible, SaltStack (salt-ssh)

Ansible is push-based and agentless: the nodes only need SSH and Python. Terraform is often listed next to these tools, but it is mainly a provisioning tool (it creates infrastructure), while Ansible configures what runs on it.

Consider a vulnerability in a package that runs on a thousand servers. Patching each server by hand is slow and error-prone. With Ansible, you describe the fix once and apply it to every server in a single run.

Ansible architecture

Ansible architecture: users write playbooks, and Ansible uses inventory, modules, API and plugins to manage hosts and network devices

  • Control node: the machine where Ansible is installed and where you run commands and playbooks. It must be Linux or macOS. Windows cannot be a control node without WSL (Windows Subsystem for Linux), but Ansible can still manage Windows machines.
  • Managed nodes (hosts): servers, cloud instances or network devices that Ansible configures.
  • Inventory: a file (INI or YAML) that lists the managed nodes, puts them in groups, and stores connection details such as IP, user, port, credentials and variables.
  • Modules: small programs Ansible pushes to the nodes to do one job, such as apt, service or copy. Most modules take parameters that describe the desired state, not the steps.
  • Playbook: a YAML file with an ordered list of tasks. It describes the desired state of the systems. Playbooks are covered in part 2.

Install Ansible with pip in a virtual environment

You can install Ansible with apt, from the official PPA, with pip or with pipx. The Ubuntu apt package often lags behind the current release, so this guide uses pip inside a Python virtual environment.

On Ubuntu 24.04, pip refuses to install packages into the system Python (PEP 668, the "externally-managed-environment" error). A virtual environment avoids that and keeps Ansible separate from system packages.

terminal
$ sudo apt update
$ sudo apt install -y python3-pip python3-venv
$ python3 -m venv ~/myenv
$ source ~/myenv/bin/activate
(myenv) $ pip install ansible
(myenv) $ ansible --version

ansible --version inside the myenv virtual environment showing ansible core 2.18.6 and Python 3.12.3

The environment is active only in the current shell. After a reboot or a new login, activate it again with source ~/myenv/bin/activate.

Connect to the managed nodes

Ansible depends on SSH, so the control node must reach each node first. Check the network, then log in manually:

terminal
$ ping -c 5 192.168.56.211
$ ssh vagrant@192.168.56.211

Password authentication on Vagrant boxes

The main SSH server config is /etc/ssh/sshd_config. It includes every file in /etc/ssh/sshd_config.d/, and on my Vagrant Ubuntu box the file 50-cloud-init.conf there turns password login on.

cat sshd_config showing the Include line for /etc/ssh/sshd_config.d/*.conf

50-cloud-init.conf in /etc/ssh/sshd_config.d containing PasswordAuthentication yes

If you change it to PasswordAuthentication no and restart SSH, password login stops working and only keys are accepted. On a Windows host, Vagrant keeps the private key it generated for each VM under .vagrant\machines\<name>\virtualbox\private_key in the project folder.

SSH key authentication

Key-based login is more secure than passwords, and the remaining examples use it. Generate a key pair on the control node and copy the public key to each node:

terminal
$ ssh-keygen -t ed25519 -f ansible_key
$ ssh-copy-id -i ansible_key.pub vagrant@192.168.56.212
$ ssh -i ~/ansible_examples/example3/ansible_key vagrant@192.168.56.212

ssh-copy-id appends the public key to ~/.ssh/authorized_keys on the node. You can also paste the content of ansible_key.pub into that file manually. My first key used -t rsa; ed25519 is the modern default and produces shorter keys.

Write the first inventory

Create a project directory containing a file named inventory. Each line defines a host alias with its connection variables, and a name in square brackets starts a group.

Inventory file with alias vm1, its IP, user and password, and a webservergrp group containing vm1 and vm2

INIinventory
# Password login (needs sshpass on the control node)
vm1 ansible_host=192.168.56.211 ansible_user=vagrant ansible_password=vagrant
 
# SSH key login
centosvm1 ansible_host=192.168.56.212 ansible_user=vagrant ansible_ssh_private_key_file=/home/vagrant/ansible_examples/example3/ansible_key
 
[webservergrp]
vm1
 
[dbservers]
centosvm1

Ad-hoc commands

An ad-hoc command is a one-line Ansible command that runs a single module against one or more hosts. It suits quick checks. For repeatable work, use playbooks, which can be reviewed and kept in version control.

FlagMeaning
-ipath to the inventory file
-mmodule to run
-aarguments for the module
--becomerun the task with sudo (privilege escalation)

Test the connection with the ping module

terminal
$ ansible -i inventory -m ping vm1

The ping module logs in over SSH and checks that Python works. It is not an ICMP ping. The first attempt in my lab failed, because password login over SSH requires the sshpass program on the control node:

ansible ping fails with: to use the ssh connection type with passwords you must install the sshpass program

After sudo apt install sshpass the same command returned pong:

ansible ping to vm1 returns SUCCESS with changed false and ping pong

Install and remove a package

terminal
$ ansible -i inventory -m apt -a "name=apache2 state=present" vm1

The command fails with a permission error, because installing packages requires root:

apt module fails with could not open lock file /var/lib/dpkg/lock-frontend, permission denied

Add --become to run the module with sudo, and the installation succeeds:

terminal
$ ansible -i inventory -m apt -a "name=apache2 state=present" vm1 --become

apt module with --become returns CHANGED, and dpkg on the node lists apache2 2.4.58 installed

To remove it, set the state to absent:

terminal
$ ansible -i inventory -m apt -a "name=apache2 state=absent" vm1 --become

Manage the service

terminal
$ ansible -i inventory -m service -a "name=apache2 state=started" vm1 --become
$ ansible -i inventory -m service -a "name=apache2 state=started enabled=yes" vm1 --become
$ ansible -i inventory -m service -a "name=apache2 state=stopped enabled=no" vm1 --become

The first command starts Apache, the second also enables it at boot, and the third stops and disables it.

Copy a file and see idempotency

terminal
$ ansible -i inventory -m copy -a "src=index.html dest=/var/www/html/" vm1 --become

copy module returns CHANGED with the checksum, destination /var/www/html/index.html and mode 0644

Run the same command again without editing the file, and the result is SUCCESS with "changed": false. The copy module compares checksums and copies only when the file differs. This is idempotency: running the same task many times gives the same result and changes nothing that is already correct.

Second copy run returns SUCCESS with changed false and the same checksum

The ansible.cfg file

ansible.cfg stores default settings so you do not repeat them on every command. Ansible uses the first file it finds, in this order:

  1. The file in the ANSIBLE_CONFIG environment variable, if set. Example: ANSIBLE_CONFIG=/home/vagrant/ansible/example4/ansible.cfg ansible-playbook deploy.yaml
  2. ansible.cfg in the current directory (the project folder).
  3. ~/.ansible.cfg in the user's home directory.
  4. /etc/ansible/ansible.cfg. The apt package creates it; a pip install does not.

Run ansible-config view or ansible --version to see which file is in use. To generate a fully commented example file:

terminal
$ ansible-config init --disabled > ansible.cfg

Every line in the generated file starts with ; (a comment), with a description above it. Uncomment the lines you need. The most useful settings are:

Setting (in [defaults])What it does
inventorydefault inventory path (default /etc/ansible/hosts)
remote_userdefault SSH user on the nodes
private_key_filedefault SSH private key
host_key_checkingFalse skips the "authenticity of host can't be established" prompt on first connect; use in labs only
forkshow many hosts Ansible works on in parallel (default 5)
ask_passask for the SSH password at run time
become_password_filefile that holds the sudo password used by --become
executableshell used on the nodes (default /bin/sh)
homeroot directory for Ansible files on the controller (default ~/.ansible)
local_tmptemporary directory on the controller

A minimal project file looks like this:

INIansible.cfg
[defaults]
inventory = ./inventory
remote_user = vagrant
host_key_checking = False

Common mistakes

  • you must install the sshpass program: password login needs sshpass on the control node. Install it, or switch to SSH keys.
  • Could not open lock file /var/lib/dpkg/lock-frontend: the task needs root. Add --become.
  • Permissions 0664 for 'ansible_key' are too open: SSH ignores a private key that other users can read. Fix it with chmod 600 ansible_key.
  • pip externally-managed-environment error: install Ansible inside a virtual environment, or with pipx.

Key takeaways

  • Ansible is push-based and agentless; nodes need only SSH and Python.
  • Install the latest Ansible with pip in a virtual environment, created as your normal user.
  • The inventory holds hosts, groups and connection variables; use SSH keys instead of passwords.
  • Ad-hoc commands (ansible -i inventory -m module -a "args") are for quick tasks; add --become for root.
  • Modules are idempotent: a second run reports changed: false when nothing needs to change.
  • Ansible reads the first ansible.cfg it finds: ANSIBLE_CONFIG, then the current directory, then home, then /etc/ansible.

Next in this series: Ansible Playbooks.