5/5 - (1 vote)

We continue the cycle of training articles for novice system administrators. In this article, we will analyze Ansible, Ansible-Playbook, and how to raise a full-fledged web server using an automation system. Note that if you are an experienced administrator, you can safely skip this material.

Here’s what we’re talking about today:

  1. Acquaintance.
  2. Variety of automation tools.
  3. How does Ansible work?
  4. Installing Ansible.
  5. We connect to a remote server with Master.
  6. Configuration.
  7. Ansible command modules.
  8. Ansible-playbooks.
  9. Ansible Roles.
  10. Outcome.

Acquaintance

Ansible is a tool for automating administration processes.

Example : Your app has been updated. The task was set to change the php version from 7.4 to 8.1. How to proceed?

  1. Do it manually.Go to each server and update the version. This option is suitable if you have 2-3 servers. What if there are 100 servers? You can also do it manually. But why? It will take a very long time.
  2. With the help of automation tools.Here, tools come to the rescue that allow you to write a configuration and update automatically. This saves you time. And it saves you a lot of routine work.

Concepts that will help us in the future:

  • Master is the main server on which the automation code is written (most often in yml format).
  • The remote server is a managed server.

Varieties of automation tools

  1. Pull – a package is installed on remote servers that unloads settings from the Master.Example : An automation package will only apply the settings if the exact same package is installed on the remote server and is ready to accept a connection from the master server.
  2. Push – no software needs to be installed on the servers, only SSH access and a master server are required.Example : We have a master server and 40 servers. There is no need to enter new servers, it will be enough to run the configuration on master. After that, master itself will launch the configurations to the remote server.

How does Ansible work?

Imagine a master server: on it we write the configuration, specify the ip-addresses of the servers and completely control the process.

Example : We have written a configuration to upgrade php7.4 to 8.1. We performed Push, after which all settings began to be installed on all remote servers that were specified in Ansible.

Installing Ansible

We do the installation on the master server.

Let’s add the ansible repository:

mcedit /etc/apt/sources.list

Insert:

deb http://ppa.launchpad.net/ansible/ansible/ubuntu focal main

Add the repository key:

apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 93C4A3FD7BB9C367

Update repositories:

apt update

Installation:

apt install ansible

We check:

ansible --version
ansible [core 2.12.5]
  config file = /etc/ansible/ansible.cfg
  configured module search path = ['/root/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python3/dist-packages/ansible
  ansible collection location = /root/.ansible/collections:/usr/share/ansible/collections
  executable location = /usr/bin/ansible
  python version = 3.8.10 (default, Mar 15 2022, 12:22:08) [GCC 9.4.0]
  jinja version = 2.10.1
  libyaml = True

If you have a different OS, you can refer to the official manual .

Ansible is installed and ready to go!

Connecting to a remote server with Master

Create a directory to manage Ansible:

mkdir /var/ansible/

Let’s go:

cd /var/ansbile/

We create an inventory file. A file that will list all the ip addresses of remote servers. The file format can be used as usual txt or yml. Any name too.

touch hosts.txt

Main keys:

  1. [ ] – In these brackets we indicate the name of the server group.
    Example: [dev]
  2. ansible_host – ip address.
    Example: ansible_host = 0.0.0.0
  3. ansible_user – username to connect to the remote server.
    Example: ansible_user – test
  4. ansible_pass – The user’s password.
    Example: ansible_pass = 12345
  5. ansible_ssh_private_key_file – path to the private key. To login without a password.
    Example: ansible_ssh_private_key_file = /home/test/.ssh/id_rsa
  6. ansible_port – ssh connection port.
    Example: ansible_port = 2222

These keys will be the main ones. It should be borne in mind that not all keys are listed here, the rest can be found in the official manual .

We forward your ssh key to a remote server.

Generate a key on master:

ssh-keygen -t rsa

Next, we derive the key:

cat ~/.ssh/id_rsa.pub

You will get a key like this:

ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDO2w+l5l0PM2cq8WAFWK3dJCAHuqYR5tno9aIQ1hoqMGdsZWRNntDQvIsId/uvXcqUZddK33QbhDjb4k5yq1IqkoClY/z5Ps3V/xvA3uWALFzB8SuFRB+OtJjbvqGO9QHct4RKEiIAdDdMWNhPHBoa4KDJszhs1+0j5DXp3N96BatO4sE4X6AzUWDb+YD3Lb3g2pPrr4YRvEDGRgE6YZLANV3nYmAaqVMqznWKnnXkbx4ccAEkmND4L/6FuJ+lv3mXpaSnLkDr3NhKjiCCH88BV+Nh3KD+dpp76hWvrgp5yrWvmJ6kpZU0jbgb4RXW0HkLb9TpVkyZgRV96RdWtr9JkQ0eSCgvNN+mtAOmDHmogijhrv0Eq54LINNsSNUjeeSPM51MiZRmXW68WgXjKbKKKpTzH0vj6E6p9vznlexRB2FulX+1fMj/toG8Js75GZXejpQ2XI9BWgrzZwsfhYx8m7jDa4/HOpsl6IqSm2ZYTRAydH+YhJjMWsMYnzXugmc= test@ansible2

Copy the key. We connect to the remote server and save this key in authorized_keys. At ~/.ssh/authorized_keys

touch ~/.ssh/authorized_keys
echo "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDO2w+l5l0PM2cq8WAFWK3dJCAHuqYR5tno9aIQ1hoqMGdsZWRNntDQvIsId/uvXcqUZddK33QbhDjb4k5yq1IqkoClY/z5Ps3V/xvA3uWALFzB8SuFRB+OtJjbvqGO9QHct4RKEiIAdDdMWNhPHBoa4KDJszhs1+0j5DXp3N96BatO4sE4X6AzUWDb+YD3Lb3g2pPrr4YRvEDGRgE6YZLANV3nYmAaqVMqznWKnnXkbx4ccAEkmND4L/6FuJ+lv3mXpaSnLkDr3NhKjiCCH88BV+Nh3KD+dpp76hWvrgp5yrWvmJ6kpZU0jbgb4RXW0HkLb9TpVkyZgRV96RdWtr9JkQ0eSCgvNN+mtAOmDHmogijhrv0Eq54LINNsSNUjeeSPM51MiZRmXW68WgXjKbKKKpTzH0vj6E6p9vznlexRB2FulX+1fMj/toG8Js75GZXejpQ2XI9BWgrzZwsfhYx8m7jDa4/HOpsl6IqSm2ZYTRAydH+YhJjMWsMYnzXugmc= test@ansible2" >> ~/.ssh/authorized_keys

To test the connection, you can try connecting via ssh.

ssh USER@HOST

We return to master. Open the inventory file.

cd /var/ansible
mcedit hosts.txt
  1. We add a group.
    [test]
  2. Add the remote server to this group.
    Specify the name of the server:ansible2
  3. Specify the IP address of the remote server:
    ansible_host=0.0.0.0
  4. Specify the port:
    ansible_port=22
  5. Specify the user:
    ansible_user=root
  6. Specify the path to the private key on the master server:
    ansible_ssh_private_key_file=/root/.ssh/id_rsa

Here’s what happened:

[test]

 ansible2 ansible_host=0.0.0.0 ansible_port=22 ansible_user=root ansible_ssh_private_key_file=/root/.ssh/id_rsa

To test the connection, we need to run the ansible command with the keys.

ansible -i hosts.txt all -m ping
  1. -i path to the inventory file.
  2. all is a run on all servers specified in this inventory file. You can run the name of the group instead of all.
    Example: test
  3. -m ansible modules. In this case, we use the ping module to test the connection.

We check:

ansible -i hosts.txt all -m ping
The authenticity of host '192.168.0.24 (192.168.0.24)' can't be established.
ECDSA key fingerprint is SHA256:YejdpawsmOZjAh8M518r+lk7eRPTETdCEPsPzQezNd8.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes

We agree with saving the fingerprint and get the following (this is done once; in the future, we will turn off the fingerprint verification):

ansible2 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}

Our ping request received a pong response . This means that the connection is established, and we can do whatever we need with the server.

Configuration

For further convenience, we edit the Ansible configuration file, in which it will be possible to specify all the necessary parameters for launch.

Keys:

  1. ask_pass – Can be either TRUE or FALSE. Enable/disable password request. The default is false. If you use a key to connect, then there is no need to change.
  2. ask_sudo_pass – Either TRUE or FALSE. The default is TRUE. This is a sudo password request on a remote server. If the sudo password request is disabled on the remote server, then there is no need to change it. If a password request is required, change this value to FALSE.
  3. forks – Number of concurrent ansible processes. The default is 5. If you have many servers, then this value can be changed. Then ansible will perform settings in parallel not on five servers, but, for example, on twenty. However, the load on the server and the network will increase.
  4. host_ket_checking – hosts fingerprint check. The default is TRUE. FALSE disables fingerprint verification.
  5. inventory – address of the default inventory file.
  6. log_path – default log file address.

More keys in the official manual .

Opening the configuration:

mcedit /etc/ansible/ansible.cfg

Adding default settings:

[defaults]

Disable fingerprint verification:

host_key_checking = false

We specify the inventory file so as not to always write the -i switch when starting ansible:

inventory = /var/ansible/hosts.txt

Specify the log file:

log_path = /var/log/ansible/ansible.log

It turns out:

[defaults]
host_key_checking = false
inventory                  = /var/ansible/hosts.txt
log_path          = /var/log/ansible/ansible.log

We check:

ansible all -m ping
ansible2 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}

As you can see, without specifying the -i switch, everything works.

ansible command modules

All command modules are called with the -m switch.

command – executes commands on the selected groups without the participation of the shell shell. The command to be executed is called with the -a key (argument) and is placed in quotes.
Example : ansible test -m command -a “date”

# ansible test -m command -a "date"
ansible2 | CHANGED | rc=0 >>
Вс 22 мая 2022 08:28:23 UTC

This module has a minus: the variables “<“, “>”, “|”, “;”, “&” will not work. For example, you won’t be able to grep with this module.

shell – Does everything the same as the command module, only with the help of the shell.

Example :

ansible test -m shell -a "cat /var/log/syslog | tail -5"
ansible2 | CHANGED | rc=0 >>
May 22 08:27:30 ansible2 python3[7211]: ansible-ansible.legacy.command Error Executing CMD:'“time”' Exception:[Errno 2] No such file or directory: b'\xe2\x80\x9ctime\xe2\x80\x9d'
May 22 08:28:23 ansible2 python3[7238]: ansible-ansible.legacy.command Invoked with _raw_params=date _uses_shell=False warn=False stdin_add_newline=True strip_empty_ends=True argv=None chdir=None executable=None creates=None removes=None stdin=None
May 22 08:29:23 ansible2 systemd[1]: session-16.scope: Succeeded.
May 22 08:32:58 ansible2 systemd[1]: Started Session 17 of user nik.
May 22 08:32:59 ansible2 python3[7381]: ansible-ansible.legacy.command Invoked with _raw_params=cat /var/log/syslog | tail -5 _uses_shell=True warn=False stdin_add_newline=True strip_empty_ends=True argv=None chdir=None executable=None creates=None removes=None stdin=None

copy – the module allows you to copy a file from master to a remote server.

Arguments:

  1. scr – file address on master;
  2. dest – address where to save the file to the remote server;
  3. owner – username who will own the file;
  4. group – the group to whom the file will belong;
  5. mode – file permissions.

Example :

ansible test -m copy -a "src=/var/ansible/test.txt dest=/var owner=test group=test mode=644"
ansible2 | CHANGED => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": true,
    "checksum": "da39a3ee5e6b4b0d3255bfef95601890afd80709",
    "dest": "/var/test.txt",
    "gid": 1000,
    "group": "test",
    "md5sum": "d41d8cd98f00b204e9800998ecf8427e",
    "mode": "0644",
    "owner": "test",
    "size": 0,
    "src": "/root/.ansible/tmp/ansible-tmp-1653209819.5195355-7928-33699130649859/source",
    "state": "file",
    "uid": 1000
}

If you are not doing it as a root user, then you need to add -b (sudo) at the end.

ansible test -m copy -a "src=/var/ansible/test.txt dest=/var owner=nik group=nik mode=644" -b

Otherwise, the rights to write to /var will not be enough, and an error will be generated.

file – Performs an action on a file. Deletes, changes user rights and much more.

Arguments:

  1. path – path to the file on the remote storage;
  2. state – state.

Example : Let’s delete the file that we downloaded using the previous module:

ansible test -m file -a "path=/var/test.txt state=absent"
ansible2 | CHANGED => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": true,
    "path": "/var/test.txt",
    "state": "absent"
}

apt – a module for installing packages on a remote server when using  Debian operating systems  and based on them ( Ubuntu ,  Linux Mint  , etc.). When using a Red Hat OS, you can use the yum module .

Arguments:

  1. name – service, package name;
  2. state – command to install, remove, update.

Example :

ansible test -m apt -a "name=htop state=latest"
ansible2 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "cache_update_time": 1653211045,
    "cache_updated": false,
    "changed": false
}

service – allows you to start/stop/reload services on a remote server.

Arguments:

  1. name – “service name”;
  2. state – command to change the state;
  3. enabled – when it needs to be added to startup.

Example :

test -m service -a "name=nginx state=started"
ansible2 | FAILED! => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "msg": "Could not find the requested service nginx: host"
}

In this case, an error is returned because nginx is not installed on the remote server.

ansible-playbooks

Ansible Playbooks is a configuration and deployment management system for multiple remote servers. File format: yml.

Example : above we wrote all the commands manually. With the help of a playbook, we can combine all this into one file and run it with a single command.

Launch:

ansible-playbook и Адрес playbook.

Example :

ansible-playbook playbook.yml

As an example, let’s write a playbook that will send pings to all remote servers.

First, let’s create a file:

touch playbook.yml

Next, we open:

mcedit playbook.yml

We remind you that this is a yml format that TAB does not like, so never use it there.

Keys:

  1. name – assigning a name to a group/team.
    Example: name: Test PING
  2. hosts – an indication of the group of servers on which this command must be executed.
    Example: hosts: all
  3. become – run sudo (if you’re not doing the settings as root).
  4. tasks – indicate that the executable command will go further.
  5. name – assigning a name to the command.

It turns out like this:

- name: Test PING.  
  hosts: all  
  become: yes
  tasks:  
    - name: ping
    ping:

ping: is the name of the module we are asking you to use.

Let’s try to run:

ansible-playbook playbook.yml
PLAY [Test PING.] ************************************************************************************TASK [Gathering Facts] *******************************************************************************ok: [ansible2]
TASK [ping] ******************************************************************************************ok: [ansible2]
PLAY RECAP *******************************************************************************************ansible2                   : 
ok=2    
changed=0    
unreachable=0    
failed=0    
skipped=0    
rescued=0    
ignored=0

playbook returned ok. So the ping passed, and the playbook was written without errors.

Beautifully written ansible playbooks are not complete without the so-called Roles:

Ansible Roles

Ansible Roles are roles that allow you to automatically load related variables, files, tasks, handlers, and other Ansible artifacts based on a known file structure. Once you’ve grouped your content into roles, you can easily reuse it and upload it to a repository for later use on other hosts.

Roles allows you to create a convenient and understandable directory structure. This greatly simplifies administration in the future and allows you to understand other people’s playbooks.

The following command is used to create the directory structure:

ansible-galaxy init <Name>

# ansible-galaxy init test
- Role test was created successfully

We get the following structure:

.└── test    
     ├── defaults    
     │   └── main.yml    
     ├── files    
     ├── handlers    
     │   └── main.yml    
     ├── meta    
     │   └── main.yml    
     ├── README.md    
     ├── tasks    
     │   └── main.yml    
     ├── templates    
     ├── tests    
     │   
     ├── inventory    
     │   └── test.yml
         └── vars
         └── main.yml

defaults – default variables are assigned. These variables have low priority. Will be used only if this variable has not been declared before. In this directory, we assign defaults variables.
Example: DOC_ROOT: /var/www/DOMAIN_NAME.com/

files – files are stored that will need to be transferred to remote servers using roles. In this directory, we add files that need to be transferred and used on remote servers.
Example: This directory contains the site code for copying to a remote server.

handlers is a handler that is only executed when invoked via the  notify -directive. The handler is executed at the very end of the playbook when all tasks have completed without errors. In this directory, we place the handlers that need to be run at the very end.

Example:

handlers:
    - name: Restart Nginx
      service:
        name: nginx
        state: restarted

meta – metadata for the role, including role dependencies. In this directory, we specify the so-called metadata for the project. Company name, repository name, project name.

By default, the main file in this directory looks like this:

galaxy_info:
  author: your name
  description: your role description
  company: your company (optional)
  license: license (GPL-2.0-or-later, MIT, etc)
  min_ansible_version: 2.1
  galaxy_tags: []
dependencies: []
root@php:/var/ansible/test/meta#
root@php:/var/ansible/test/meta# cat main.yml
galaxy_info:
  author: your name
  description: your role description
  company: your company (optional)
  license: license (GPL-2.0-or-later, MIT, etc)
  min_ansible_version: 2.1
  galaxy_tags: []
dependencies: []

tasks – the main list of tasks for Ansible. In this directory, we specify all the tasks that need to be performed on the remote server.
Example: install nginx and open port 80.

templates – this directory contains template files for deployment on a remote server.
Example: We put a default nginx page and a default index.php and then we pass it to a remote server

tests is the name of the project that was created when using the  ansible-galaxy init <Название>. An inventory file will be created in this directory, which will list the hosts of remote servers. Test.yml contains variables for connecting to a remote server (user, port, etc.).

vars – analogue of defaults. In this directory, variables are assigned to run roles. This directory takes precedence over defaults.

Outcome

In this article, we have considered the theoretical part that we will need in the future. In the next article, we will put the theory into practice: we will build an action plan and deploy LEMP on a remote server using an automation tool.