Ansible โ€” configuration management system: getting to know it
Greetings!

In this note we will get acquainted with a popular open source configuration management system with the telling name โ€” Ansible ๐ŸŽป. We’ll perform the installation and configuration ๐Ÿ› , put together an inventory ๐Ÿ“‹, learn how to run playbooks ๐Ÿš€, talk about Ansible facts ๐Ÿ—‚, find out what Ansible console ๐Ÿ–ฅ is, and write a playbook ๐Ÿ“ that fixes the sshd config and copies files to the server. It’s going to be informative ๐Ÿ˜‰!

Preface

To get a slightly better understanding of what’s going on, I recommend reading my theoretical note ๐Ÿ“ about configuration management systems (Configuration Management, CM). It describes the term, what types exist, and briefly covers popular implementations of such systems and their differences.

Getting back to Ansible, it lets you automate server configuration by describing the desired state in a convenient yaml format. It’s also worth noting that Ansible operations have the property of idempotence ๐Ÿ™„. This means that if you run a playbook or task multiple times, the system’s state won’t change after the first successful run.

Before we dive deeper, let me list the main entities of this CM ๐Ÿ“’:

Installing Ansible on Debian / Ubuntu / Linux mint

I’ll demonstrate the installation on a Debian 12 distribution ๐Ÿ˜Ž:

Since Ansible works on a push principle (i.e., all management happens from the administrative server), it needs to be installed on only one machine. On the managed servers, you’ll need Python ๐Ÿ installed and SSH access.

For more details on using and configuring SSH on Linux, including working with keys, see a separate guide: SSH โ€“ Secure connection to remote hosts: an introduction.

All the required Ansible packages are in the standard repositories ๐Ÿ—„. So just open a terminal and enter the command:

BASH
sudo apt update && sudo apt install -y ansible sshpass
Click to expand and view more

We are also installing the sshpass utility, which will be needed for passing a password during the corresponding SSH authentication, as well as obtaining privileges via sudo ๐Ÿ’ช.

There are a lot of dependencies, so the installation will take some time โณ.

Configuring Ansible โ€” the ansible.cfg file

Let’s move on to the initial configuration. To do this, we create the necessary directories and the configuration file. We open it with any text editor (I prefer vim/neovim) ๐Ÿง‘โ€๐Ÿ’ป:

BASH
mkdir -p ~/ansible/{playbooks,roles,tmp}

nvim ~/ansible/ansible.cfg
Click to expand and view more

And fill it in:

PLAINTEXT
[defaults]
home = ~/ansible/                    ; main Ansible directory
local_tmp = ~/ansible/tmp/           ; local temporary directory for task execution
inventory = ~/ansible/inventory.yml  ; path to the inventory file with the list of hosts
playbook_dir = ~/ansible/playbooks/  ; directory where playbooks are stored
roles_path = ~/ansible/roles/        ; directory where Ansible looks for roles
remote_user = ivan                   ; user under which Ansible connects to hosts
become_method = sudo                 ; privilege escalation method (sudo, su)
become_user = root                   ; user on whose behalf sudo commands are executed
gather_facts = True                  ; gather facts about the system before executing tasks
private_key_file = ~/ansible/id_ed25519  ; path to the private key for SSH connections
host_key_checking = False            ; disables SSH host key checking
fact_caching = jsonfile              ; caching facts using the json format
fact_caching_connection = ~/ansible/tmp/  ; path for storing cached facts
Click to expand and view more

This configuration is enough to start using Ansible ๐Ÿ‘. This file defines Ansible’s default behavior, which can be overridden elsewhere: inventory files, playbooks, variable files, commands, or other configs.

If you need to explore all the available configuration file parameters ๐Ÿค“, you can generate it with this command:

BASH
ansible-config init --disabled > ~/ansible/ansible.cfg.example
Click to expand and view more

The file is well commented ๐Ÿง.

For more details on the configuration process see the official site: Ansible Configuration Settings.

Also, for convenience, I recommend immediately adding an environment variable pointing to our default config file:

BASH
export ANSIBLE_CONFIG="$HOME/ansible/ansible.cfg"

# to create the variable on login
echo 'export ANSIBLE_CONFIG="$HOME/ansible/ansible.cfg"' >> ~/.profile
Click to expand and view more

Replace ~/.profile with your shell’s environment loading file. In my case I use the interactive ZSH shell.

In that case you’ll be able to use Ansible commands from any directory without explicitly specifying the path to the settings file ๐Ÿ˜Œ.

See the full list of available environment variables also in the official docs: Environment Variables.

Inventory โ€” the inventory.yml file

Let’s move on to taking inventory of the infrastructure we’ll be managing. To do this, we create the inventory file:

BASH
nvim ~/ansible/inventory.yml
Click to expand and view more

And add a list of our servers ๐Ÿ“‹ that we’ll be managing. In my example there are 5 Linux servers (3 โ€” Debian, 2 โ€” Ubuntu):

YAML
## HOSTS
all:
  hosts:
    debian12-vpn:
      ansible_host: 192.168.122.93
      ansible_port: 22
      ansible_user: ivan
      ansible_ssh_private_key_file: ~/.ssh/id_ed25519
      project: vpn
    debian12-dns:
      ansible_host: 192.168.122.201
      ansible_port: 22
      ansible_user: ivan
      ansible_ssh_private_key_file: ~/.ssh/id_ed25519
      project: vpn
    debian12-monitoring:
      ansible_host: 192.168.122.30
      ansible_port: 22
      ansible_user: ivan
      ansible_ssh_private_key_file: ~/.ssh/id_ed25519
      project: monitor
    ubuntu22-storage:
      ansible_host: 192.168.122.5
      ansible_port: 22
      ansible_user: ivan
      ansible_ssh_private_key_file: ~/.ssh/id_ed25519
      # ansible_password: "Pa$$w0rD"
      project: storage
    ubuntu22-backup:
      ansible_host: 192.168.122.249
      ansible_port: 22
      ansible_user: ivan
      ansible_ssh_private_key_file: ~/.ssh/id_ed25519
      # ansible_become_password: "Pa$$w0rD"
      project: storage

## GROUPS
debian:
  hosts:
    debian12-vpn:
    debian12-dns:
    debian12-monitoring:
ubuntu:
  hosts:
    ubuntu22-storage:
    ubuntu22-backup:
Click to expand and view more

Don’t forget to replace the parameters with your own ๐Ÿ˜‰

We save, we close.

Keep in mind that the YAML format is indentation-sensitive. This means indentation is part of the syntax, and even a single wrong space will cause an error.

Let me briefly describe the content using the debian12-vpn server as an example:

Also note that after the list of hosts comes the list of groups ๐Ÿ–ฅ๐Ÿ–ฅ:

which include hosts split by OS (in my case). Groups are also formed arbitrarily, the main thing is to follow the syntax.

See the full list of available connection parameters for the inventory file here: Connecting to hosts: behavioral inventory parameters.

Our inventory file is ready, let’s look at its structure with this command:

BASH
ansible-inventory --graph
Click to expand and view more

This visualization is especially useful if you have a large infrastructure of hundreds of servers split into groups ๐Ÿ“Š.

Writing our first playbook: checking server availability with the ping module

In the context of Ansible, the ping module does not perform a ping of the server in the classic sense ๐Ÿคทโ€โ™‚๏ธ. Its job is to connect to the server via SSH, perform some checks (e.g., the presence of a Python interpreter), and return a success/error message ๐Ÿ‘€.

Let’s create the playbook file in the directory we specified in the configuration file:

BASH
nvim ~/ansible/playbooks/ping.yml
Click to expand and view more

In its simplest form, a playbook looks like this:

YAML
---

- name: Check connection

  hosts: all
  gather_facts: false
  tasks:
    - name: Ping
      ping:
Click to expand and view more

Where:

We save the file and run the playbook:

BASH
ansible-playbook ./ansible/playbooks/ping.yml
Click to expand and view more

When using the ansible-playbook command, the path to the playbooks needs to be specified explicitly. To avoid doing this every time, for convenience you can create a short function and call it in the console (see further in the article).

In my example, access to the servers is configured via SSH keys. Therefore, the availability check completed successfully ๐Ÿ˜Ž.

If you use a password (which is not recommended) ๐Ÿ˜ณ you can set it in the ansible.cfg file (parameter ansible_password) or pass it interactively using the --ask-pass key (that’s what we installed the sshpass package for):

BASH
ansible-playbook ./ansible/playbooks/ping.yml --ask-pass
Click to expand and view more

In case of any errors, Ansible will output scary-looking red messages โ›”๏ธ. For example, if a host is unreachable via SSH, it looks like this:

To run the playbook on a specified group of servers, you can explicitly specify them in the command via the -l (limit) parameter:

BASH
ansible-playbook ./ansible/playbooks/ping.yml -l ubuntu
Click to expand and view more

To run the playbook on behalf of a privileged user ๐Ÿ’ช, add the -b (become) key. In my example, the ping module will run as root:

BASH
ansible-playbook ./ansible/playbooks/ping.yml -b
Click to expand and view more

In my server configuration, passwordless access to root via sudo is configured (since this is my test bench ๐Ÿ› ), so everything worked correctly. But if you have password-based access, you can pass it in the ansible.cfg file (parameter ansible_become_password, not recommended) or enter it interactively using the --ask-become-pass key:

BASH
ansible-playbook ./ansible/playbooks/ping.yml -b --ask-become-pass
Click to expand and view more

Well, for securely automating password transfer, encryption via ansible-vault is used, but that’s a topic for a separate article ๐Ÿ˜‰.

Ansible facts โ€” the setup and debug modules

As already mentioned, Ansible facts are information about a managed server in the form of system variables, such as OS type and version, IP address, package versions, etc., automatically collected by Ansible during playbook and command execution (if the gather_facts flag is enabled) ๐Ÿคฏ.

Let’s now gather these facts to see what they look like ๐Ÿ‘จโ€๐Ÿญ. For this, the standard setup module is used. Let’s run:

BASH
ansible all -m setup
Click to expand and view more

all โ€” here logically represents all the hosts from the inventory file. You can also specify any single host, a comma-separated list of hosts, or a group here.

Depending on the number of hosts, gathering facts may take some time. In the command’s output you’ll see all the collected facts in json format:

Facts can be filtered, for example like this:

BASH
ansible debian12-vpn -m setup -a 'filter=os_family,distribution_version'
Click to expand and view more

Earlier, in the ansible.cfg file, we set up fact caching using fact_caching โ˜๏ธ.

This means the collected facts will be saved as json files in the temporary directory ~/ansible/tmp, which we also specified in the config file:

BASH
ls -l ./ansible/tmp/
Click to expand and view more

You often need to get the values of only specific facts or variables. This can be done using the debug module ๐Ÿ”จ in playbooks or directly in the terminal.

Here’s an example of getting information about the OS, the size of the /dev/sda1 disk, the project variable, and the output of a shell command on the debian12-vpn server in a playbook:

BASH
nvim ./ansible/playbooks/debug.yml
Click to expand and view more
YAML
---

- name: Debug

  hosts: debian12-vpn
  gather_facts: true
  become: true

  tasks:

    - name: Debug 1
      debug:
        msg: "Example to use var: {{ ansible_facts.os_family }}"

    - name: Debug 2
      debug:
        var: ansible_facts.distribution_version

    - name: Debug 3
      debug:
        var: project

    - name: Shell 1
      shell:
        cmd: whoami
      register: user_name

    - name: Debug 4
      debug:
        var: user_name.stdout
Click to expand and view more

And we run it:

BASH
ansible-playbook ./ansible/playbooks/debug.yml
Click to expand and view more

Viewing facts using commands in the terminal:

In this case the facts will be taken from the cache.

BASH
ansible debian12-vpn -m debug -a 'var=ansible_facts.os_family,ansible_facts.distribution_version'

ansible debian12-vpn -m debug -a 'var=ansible_facts.devices.keys()'

ansible debian12-vpn -m debug -a 'var=ansible_facts.devices.sda.partitions.sda1.size'
Click to expand and view more

You may have noticed that facts are accessed using dot syntax, as in Python (Ansible is written in this language ๐Ÿ‘Œ).

For detailed info on fact gathering also see the official site: Discovering variables: facts and magic variables. Info on the debug module here.

Executing shell commands in the terminal on multiple servers simultaneously

Using Ansible and the standard shell module, you can execute remote commands on multiple servers at once ๐Ÿง‘โ€๐Ÿ’ป๐Ÿ’ป๐Ÿ’ป๐Ÿ’ป. This is very handy for debugging or performing simple operations.

The remote command is passed in quotes using the -a key. For example, to output the list of network interfaces and their IP addresses:

BASH
ansible all -m shell -a 'ip -br a'
Click to expand and view more

Or run a command from our Linux quiz ๐Ÿง, which outputs the server’s uptime in Unix time format:

BASH
ansible debian12-vpn,ubuntu22-storage -m shell -a 'date -d "$(uptime -s)" +%s'
Click to expand and view more

To run commands on behalf of a privileged user, -b is also used:

BASH
ansible all -m shell -a 'whoami' -b
Click to expand and view more

What is Ansible console

Ansible comes bundled with such a useful tool as ansible-console ๐Ÿ˜. This is an interactive shell for executing commands on multiple servers. It’s similar to the shell module commands we ran earlier, but the way of interacting is a bit different:

BASH
ansible-console

whoami
Click to expand and view more

To exit, type exit or press Ctrl+d.

Example of using the ping module on the debian group:

BASH
ansible-console debian

ping
Click to expand and view more

Viewing Ansible facts:

BASH
setup filter=ansible_hostname
Click to expand and view more

To execute privileged commands, add the -b key (but be careful!):

BASH
ansible-console debian -b

whoami
Click to expand and view more

When executing remote commands, ansible-console uses the command module by default โ—๏ธ. It calls commands directly, so some shell mechanics won’t work here. To use shell, specify it explicitly:

BASH
shell echo $HOME
Click to expand and view more

Ansible documentation right in the terminal

Ansible has very good documentation (in English ๐Ÿ™„, though) and it’s available even from the console using the ansible-doc utility. For example, to see the description and usage examples of the systemd module, run:

BASH
ansible-doc systemd
Click to expand and view more

You’ll end up in less viewing mode. Usually examples of module usage in playbooks are found at the bottom of the documentation:

Personally, I find this very convenient!

Example ansible playbook: fixing the sshd config and copying an SSH key

As practice, let’s write a playbook ๐Ÿ“ that:

Let’s generate a new key:

BASH
ssh-keygen -t ed25519 -f ~/ansible/id_ed25519_new
Click to expand and view more

We create the playbook:

BASH
nvim ./ansible/playbooks/sshd_config.yml
Click to expand and view more

We fill it in:

YAML
---

- name: Config sshd service and copy private key   # playbook name/description
  hosts: all                            # apply to all hosts
  gather_facts: false                   # do not gather facts (speeds up execution)
  become: true                          # execute with privilege escalation (sudo)
  force_handlers: true                  # execute handlers even if an error occurs

  vars:
    - new_ssh_port: 4444                # new SSH port number
    - user_name: "ivan"                 # username for whom we're adding an SSH key
    - user_ssh_privkey: "/home/ivan/ansible/id_ed25519_new" # path to the user's private SSH key
    - user_ssh_pubkey: "/home/ivan/ansible/id_ed25519_new.pub" # path to the user's public SSH key

  tasks:
    - name: Change sshd port number
      lineinfile:
        path: /etc/ssh/sshd_config      # path to the SSH configuration file
        regexp: "^Port"                 # regular expression to find the port line
        line: "Port {{ new_ssh_port }}" # change the port to the value from the variable
        backup: true                    # create a backup of the sshd_config file before changing
      notify:
        - Restart sshd                  # handler for restarting SSH after the change
      tags:
        - ssh_server                      # tag to allow selective task execution
    
    - name: Disable sshd password auth
      lineinfile:
        path: /etc/ssh/sshd_config      # path to the SSH configuration file
        regexp: '^PasswordAuthentication yes'  # regular expression to find the line
        line: 'PasswordAuthentication no'      # disable password authentication
      notify:
        - Restart sshd                  # handler for restarting SSH
      tags:
        - ssh_server                      # tag for the SSH server task

    - name: Create ssh directory if it does not exist
      file:
        path: "/home/{{ user_name }}/.ssh/"  # path to the .ssh directory for the user
        state: directory               # create the directory if it doesn't exist
        owner: "{{ user_name }}"       # set the owner
        group: "{{ user_name }}"       # set the group
        mode: 0700                     # directory access permissions
      tags:
        - ssh_client                   # tag for SSH client tasks
    
    - name: Add user ssh public key
      lineinfile:
        path: "/home/{{ user_name }}/.ssh/authorized_keys"  # path to the authorized_keys file
        line: "{{ lookup('file', '{{ user_ssh_pubkey }}') }}"  # use the content of the public key file as the line to add
        owner: "{{ user_name }}"       # set the owner
        group: "{{ user_name }}"       # set the group
        mode: 0600                     # file access permissions
        backup: true                   # create a backup of the file before changing
        create: true                   # create a new file if it doesn't exist
      tags:
        - ssh_client                   # tag for SSH client tasks

    - name: Copy user ssh private key
      copy:
        src: "{{ user_ssh_privkey }}"  # path to the file on the local machine
        dest: "/home/{{ user_name }}/.ssh/"  # path to the directory on the remote server, trailing slash is required
        owner: "{{ user_name }}"       # set the owner
        group: "{{ user_name }}"       # set the group
        mode: 0600                     # file access permissions
        backup: true                   # create a backup of the file before changing
      when: project == "vpn"           # execute only if the host's project variable = "vpn"
      tags:
        - ssh_client                   # tag for SSH client tasks

  handlers:
    - name: Restart sshd
      systemd:
        name: sshd                     # name of the systemd SSH service
        state: restarted               # restart the service
Click to expand and view more

And run it:

ATTENTION โ—๏ธ This playbook changes the sshd daemon’s port number and disables password access to the server via SSH. It is strongly recommended to run untested playbooks in test environments. You perform all actions at your own risk and responsibility. I warned you)

BASH
ansible-playbook ./ansible/playbooks/sshd_config.yml
Click to expand and view more

If everything worked correctly, the SSH access port on the remote servers will be changed. Don’t forget to update the port number in the inventory file. Or revert the old port number with a similar command:

BASH
ansible-playbook ./ansible/playbooks/sshd_config.yml -e 'ansible_port=4444' -e 'new_ssh_port=22'
Click to expand and view more

Here, the ansible_port variable (from the ansible.cfg config), used to connect to the hosts, and the custom new_ssh_port variable (from the sshd_config.yml playbook), which defines the new port number in the playbook, are overridden ๐Ÿ˜ถโ€๐ŸŒซ๏ธ.

Just in case, let me repeat that the playbook also adds our new public (~/ansible/id_ed25519_new.pub) key to the servers.

You may have noticed ๐Ÿง that tags ๐Ÿ”– are used in the playbook. They allow selective execution. For example, run only the tasks tagged ssh_client (key -t or --tags):

BASH
ansible-playbook ./ansible/playbooks/sshd_config.yml -t ssh_client
Click to expand and view more

Or, conversely, ignore tasks (--skip-tags):

BASH
ansible-playbook ./ansible/playbooks/sshd_config.yml --skip-tags ssh_server
Click to expand and view more

A brief word about Ansible roles

As mentioned at the beginning of the article, an Ansible role is a structure of files and directories for conveniently organizing tasks, templates, and variable files for a Playbook ๐Ÿšœ. You can create a role template with this command:

BASH
ansible-galaxy init ./ansible/roles/base-config
Click to expand and view more

A brief description of the structure:

BASH
./ansible/roles/base-config/
โ”œโ”€โ”€ README.md           # role documentation
โ”œโ”€โ”€ defaults/           # default variable values (main.yml)
โ”œโ”€โ”€ files/              # files that will be copied to target hosts
โ”œโ”€โ”€ handlers/           # handlers for notifications (main.yml)
โ”œโ”€โ”€ meta/               # role metadata, including dependencies (main.yml)
โ”œโ”€โ”€ tasks/              # main role tasks (main.yml)
โ”œโ”€โ”€ templates/          # Jinja2 templates that will be rendered
โ”œโ”€โ”€ tests/              # tests for the role (inventory, test.yml)
โ””โ”€โ”€ vars/               # role variables with higher priority (main.yml)
Click to expand and view more

In the future, we’ll definitely be writing Ansible roles for various tasks!

But that’s already a completely different story)

Creating a function for conveniently running playbooks

Earlier I promised a function for conveniently executing playbooks. It’s quite simple, but useful. Let’s run:

BASH
echo 'ap() { ansible-playbook ~/ansible/playbooks/"$@"; }' >> ~/.profile

source ~/.profile

which ap
Click to expand and view more

Let’s check that it works:

BASH
ap ping.yml
Click to expand and view more

Excellent!

A couple of words about variables

As you’ve understood, variables in Ansible are a separate topic that the official documentation will explain better than I can. I’ll just point out the most problematic spot โ€” variable overriding. In Ansible, there’s a hierarchy for them (lower ones override higher ones):

Afterword

So we’ve gotten acquainted with such a great and useful configuration management tool called Ansible ๐Ÿ˜Ž. We have the basic knowledge, now all that’s left is to apply it in practice to solve routine tasks ๐Ÿ˜Œ. I’m planning to write a playbook for basic Linux server setup, installing and running various services, installing Arch Linux on encrypted partitions, and much more ๐Ÿคฏ.

All the example files are available in my repository on GitHub.

Thanks for taking the time ๐Ÿ˜Š. If you have any questions โ€“ I invite you to our friendly Raven chat ๐Ÿšถโ€โ™€๏ธ๐Ÿง๐Ÿšถ๐Ÿง๐Ÿšถโ€โ™‚๏ธ๐Ÿง on telegram ๐Ÿ˜…. Also make sure to subscribe to the main telegram channel: @r4ven_me, notifications about new materials on the site arrive there on the day of publication.

Good luck!

Useful sources

Copyright Notice

Author: Ivan Cherniy

Link: https://r4ven.me/en/automation/ansible-sistema-upravleniya-konfiguraciyami-znakomstvo/

License: CC BY-NC-SA 4.0

Blog materials may be used with attribution to the author and source, for non-commercial purposes, and under the same license.

Start searching

Enter keywords to search articles

โ†‘โ†“
โ†ต
ESC
โŒ˜K Shortcut