Skip to content

Ansible Fundamentals

Siddharth Rawat edited this page Dec 12, 2023 · 16 revisions

Ansible

The very basics on how to get started with Ansible

Install Ansible on your workstation

Configure SSH keys

  • Install ssh key pair on your workstation and servers

    • Generate ssh key-pair
    # create a private key and a .pub public key
    ssh-keygen -t ed25519 -C "description"
    # follow the on-screen instructions to generate the ssh-key
    • Add the generated ssh-keys to known_hosts
    ssh-add <keyname>
    • Dump/ copy the public key to all the other servers to which the main ansible machine is going to talk to.
    # comand to copy ssh public key from the main workstation to the server in order for the main server to talk to the target server
    ssh-copy-id -i ~/.ssh/<keyname>.pub <server-ip-or-hostname>
    • List active keys:
    ssh-add -L
    • Remove an ssh-key:
    ssh-add -D <keyname>
  • Keep the private key on the workstation that the Ansible will use to ssh to other servers

Inventory file

  • Add all the IP or Hostnames of the server you want ansible to connect to and work with
  • Dynamic Inventory is also supported in Ansible
  • For Dynamic just get the server info from the cloud provider.

Connection check

  • In order to check the connection to all the servers from the main where ansible is installed
# create a file `inventory`
# this contains server hostnames and ips
10.0.0.136
pihole
# this will create a connection and check if we can connect to our servers
# here the path till ssh private is given and the key name is `ansible` (private key)
# inventory contains all the server ips to which using this key the ansible workstation is going to connect.
# -i is for the inventory file
# -m is for the module
ansible all --key-file ~/.ssh/<keyname> -i inventory -m ping
# To list all the hosts
ansible all --list-hosts
# Lists all the data/information of all the servers
# m is the module flag
ansible all -m gather_facts
# Lists all the data of a specific server
ansible all -m gather_facts --limit <server-ip-or-hostname>

Ansible config file

The ansible.cfg file can be used to setup some default values when running the ansible command.

NOTE: There is a default ansible.cfg file in /etc/ansible/, but the one we create in our working dir will take priority over the one defined in /etc/ansible.

# this file is read by ansible when we run it
[defaults]
inventory = <filename>
private_key_file = ~/.ssh/<filename>

This will help us shorten our ansible commands and avoid passing long command line options.

# previous command:
# ansible all --key-file ~/.ssh/<filename> -i inventory -m ping
# is shortened to:
ansible all -m ping

Output:

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

AdHoc Commands

  • Some commands which require sudo privileges to work with.
  • Elevated privileges can be achieved using the below syntax.
# Tell ansible to use sudo (become) -> asks for password!
ansible all -m apt -a update_cache=true --become --ask-become-pass
# -m apt is for ubuntu/debian distros

Output:

BECOME password:
pihole | CHANGED => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "cache_update_time": 1695323800,
    "cache_updated": true,
    "changed": true
}
# Install a package named tmux via the apt module
# Equivalent to sudo apt-get install tmux
ansible all -m apt -a name=tmux --become --ask-become-pass

To check what command was run by ansible on all your servers, login to any one server, navigate to /var/log/apt/history.log file.

# Install a package via the apt module, and also make sure it’s the latest version available
ansible all -m apt -a "name=snapd state=latest" --become --ask-become-pass
# Upgrade all the package updates that are available
ansible all -m apt -a upgrade=dist --become --ask-become-pass

Learn more about apt modules here: ansible.builtin.apt module

Playbooks

  • Before Ansible runs any playbooks or before running any commands on servers it will First Gather Facts and do the further things.
  • To disable fact gathering for a play, set the gather_facts key to no
  • Follow a proper directory structure for defining an ansible playbook. Ref
# create the playbook
---

- hosts: all
  become: true
  tasks:
    - name: "install the apache2 package"
      apt:
        name: apache2
# Run the playbook
 ansible-playbook --ask-become-pass install_apache.yml

Targeting specific nodes

  • Lets say you 3 servers one for front end, second one contains some business logic as in REST API, and last one is a DB server
  • So, we would have to target specific servers for specific configurations
  • Need to create groups in inventory file, ex: one for db_servers, file_servers, web_servers
# inventory_group file
[web_servers]
172.16.250.132
172.16.250.248

[db_servers]
172.16.250.133

[file_servers]
172.16.250.134

[workstations]
172.16.250.135
---
# target specific nodes
# ansible-playbook --ask-become-pass site.yml

# runs on all servers
- hosts: all
  become: true
  tasks:
    # `pre_tasks:` can be used with roles when we want to mandate some things to run before others
    - name: install updates (CentOS)
      dnf:
        update_only: true
        update_cache: true
      when: ansible_distribution = "CentOS"

    - name: install updates (Ubuntu)
      apt:
        upgrade: dist
        update_cache: true
      when: ansible_distribution == "Ubuntu"

# runs only on web_servers defined in the `inventory` file
- hosts: web_servers
  become: true
  tasks:
    - name: "install the apache2 and php package for Ubuntu"
      apt:
        name:
          - apache2
          - libapache2-mod-php
        state: latest
      when: ansible_distribution == "Ubuntu"

    - name: "install the apache and php package for CentOS"
      dnf:
        name:
          - httpd
          - php
        state: latest
      when: ansible_distribution == "CentOS"

# runs only on db_servers defined in the `inventory` file
- hosts: db_servers
  become: true
  tasks:
    - name: install mariadb package (CentOS)
      dnf:
        name: mariadb
        state: latest
      when: ansible_distribution == "CentOS"

    - name: install mariadb package (Ubuntu)
      apt:
        name: mariadb-server
        state: latest
      when: ansible_distribution == "Ubuntu"

# runs only on file_servers defined in the `inventory` file
- hosts: file_servers
  become: true
  tasks:
    - name: install samba package
      package:
        name: samba
        state: latest
  • To include multiple distros in a single task:
---
tasks:
    - name: "update repository index"
      apt:
        update_cache: true
      when: ansible_distribution in ["Debian", "Ubuntu"]

Consolidated playbooks (w/ variables & conditionals)

We can make the above playbook even more modular and use variables in places for an easier configuration.

  • Variables in the inventory
# inventory
172.16.250.132 apache_package=apache2 php_package=libapache2-mod-php
172.16.250.248 apache_package=apache2 php_package=libapache2-mod-php
172.16.250.133 apache_package=apache2 php_package=libapache2-mod-php
172.16.250.134 apache_package=httpd php_package=php
---
# ansible-playbook --ask-become-pass install_apache.yml
- hosts: all
  become: true
  tasks:
    # Ubuntu server setup
    - name: "install the apache2 and php package for Ubuntu"
      apt:
        name:
          - "{{ apache_package }}"
          - "{{ php_package }}"
        state: latest
        update_cache: true
      when: ansible_distribution == "Ubuntu"

    # CentOS server setup
    - name: "install the apache and php package for CentOS"
      dnf:
        name:
          - "{{ apache_package }}"
          - "{{ php_package }}"
        state: latest
        update_cache: true
      when: ansible_distribution == "CentOS"

We can further shorten our yaml file to a single play by defining a single package instead of 2 different plays for apt and dnf, for different distros.

  • Using package

Package is a module in ansible that is a generic package manager and it automatically uses the underlying package manager for the hosts or servers.

---
# ansible-playbook --ask-become-pass install_apache.yml
- hosts: all
  become: true
  tasks:
    # Ubuntu server setup
    - name: "install the apache and php"
      package:
        name:
          - "{{ apache_package }}"
          - "{{ php_package }}"
        state: latest
        update_cache: true

NOTE: It can be argued that package may or may not be the best way to handle multi-distribution linux shop since we are hard-coding the package names. But this again is scenario and admin based.

Tags

  • Used to run SPECIFIC TASK in the playbook instead of running all the tasks for testing just one change in one task.
  • So, we dont need to run all the tasks just for one change in other task.
  • We Tag all the tasks and then based on that tag we can run that particular task and skip all the other tasks.
---
# target specific nodes
# ansible-playbook --ask-become-pass site.yml

# runs on all servers
- hosts: all
  become: true
  tasks:
    # `pre_tasks:` can be used with roles when we want to mandate some things to run before others
    - name: install updates (CentOS)
      tags: always
      # dnf:
      #   update_only: true
      #   update_cache: true
      # when: ansible_distribution = "CentOS"

    - name: install updates (Ubuntu)
      tags: always
      # apt:
      #   upgrade: dist
      #   update_cache: true
      # when: ansible_distribution == "Ubuntu"

# runs only on web_servers defined in the `inventory` file
- hosts: web_servers
  become: true
  tasks:
    - name: "install the apache2 and php package for Ubuntu"
      tags: apache,apache2, ubuntu
      # apt:
      #   name:
      #     - apache2
      #     - libapache2-mod-php
      #   state: latest
      # when: ansible_distribution == "Ubuntu"

    - name: "install the apache and php package for CentOS"
      tags: apache,centos,https
      # dnf:
      #   name:
      #     - httpd
      #     - php
      #   state: latest
      # when: ansible_distribution == "CentOS"

# runs only on db_servers defined in the `inventory` file
- hosts: db_servers
  become: true
  tasks:
    - name: install mariadb package (CentOS)
      tags: centos,mariadb
      # dnf:
      #   name: mariadb
      #   state: latest
      # when: ansible_distribution == "CentOS"

    - name: install mariadb package (Ubuntu)
      tags: ubuntu,db,mariadb
      # apt:
      #   name: mariadb-server
      #   state: latest
      # when: ansible_distribution == "Ubuntu"

# runs only on file_servers defined in the `inventory` file
- hosts: file_servers
  become: true
  tasks:
    - name: install samba package
      tags: samba
      # package:
      #   name: samba
      #   state: latest

1. List all tags in a playbook

# List the available tags in a playbook
ansible-playbook --list-tags site_with_tags.yml

2. Run a playbook with a specific tag

# Examples of running a playbook but targeting specific tags
ansible-playbook --tags db --ask-become-pass site_with_tags.yml
ansible-playbook --tags centos --ask-become-pass site_with_tags.yml
ansible-playbook --tags apache --ask-become-pass site_with_tags.yml

3. Run a playbook with multiple tags

# target multiple tags
ansible-playbook --tags "apache,db" --ask-become-pass site_with_tags.yml

File management & downloading binaries

  • You can copy files from Local System to all the other servers
  • We can also download binaries like Terraform, Unzip, etc on a workstation locally or on other servers.

1. Example of copying file from local to all the web servers

# Example of copying file from local to all the web servers
- hosts: web_servers
   become: true
   tasks:
    - name: copy html file for site
        tags: apache,apache,apache2,httpd
        copy:
          src: default_site.html
          dest: /var/www/html/index.html
          owner: root
          group: root
          mode: 0644

2. Example of downloading binaries in local workstation

# Example of downloading binaries in local workstation
- hosts: workstations
   become: true
   tasks:

   - name: install unzip
     package:
       name: unzip

   - name: install terraform
     unarchive:
      src: https://releases.hashicorp.com/terraform/0.12.28/terraform_0.12.28_linux_amd64.zip # download link address
      dest: /usr/local/bin # binary will be downloaded here
      remote_src: yes # notify ansible that this package is from the internet and not a package manager
      mode: 0755
      owner: root
      group: root

3. Run the playbook

ansible-playbook --ask-become-pass file_management.yml

Services

  • It is a module that can be used to start, stop, restart, or reload services on remote hosts. This is useful for managing the state of services, especially when deploying updates or changes.
  • This is not the best approach, a better approach is using handlers
# changes related to start httpd automatically and
# enabling it in case the server goes down to restart httpd automatically
- name: start and enable httpd (CentOS)
     tags: apache,centos,httpd
     service: # module to start/stop/restart/enable a service
       name: httpd
       state: started
       enabled: yes
     when: ansible_distribution == "CentOS"
# If the configuration file is changed and needs a restart so this is how it can be done
- name: change e-mail address for admin
     tags: apache,centos,httpd
     lineinfile: #module to be used to change a line
       path: /etc/httpd/conf/httpd.conf # path to the file which is to be changed
       regexp: '^ServerAdmin' # line begin with ServerAdmin
       line: ServerAdmin [email protected] #replace that with this line
     when: ansible_distribution == "CentOS"
     register: httpd # variable declare that stores the state of httpd whether its changed or not

- name: restart httpd (CentOS)
     tags: apache,centos,httpd
     service:
       name: httpd
       state: restarted
     when: httpd.changed  # this play check if changed then run this which will restart the server

Registered Variables

  • Registered Variables -> One can capture the output of a command by using the register statement.
  • The output is saved into a variable that could be used later for either debugging purposes or in order to achieve something else, such as a particular configuration based on a command's output.

Adding users & bootstrapping

1. Adding users

  • Create users and provide them privileges using ansible with the user module.
  • We can also add ssh-keys for these users to the servers using ansible module authorized_keys.
- hosts: all
  become: true
  tasks:
    - name: create user simone
      tags: always
      user:
        name: simone
        groups: root

    # ansible ssh public key
    - name: add ssh-key for simone
      tags: always
      authorized_keys:
        user: simone
        key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQChspJkMu9vZ2BmL3X2R8hVBHzT1BXZklUTPwmd8vq5DrpX4SZ9wTymwPm0EoeS2Fp6KDyAAKY88p7m6SRSpC0/KJtBQK4JVjpVTnWI3DYATb2amkomJpicAsc2Hbb+EBkDgF7rznQo7VLmQ3nSNG72Qm1oL5/WU3fjroiIFXH09zf3Cv0FCdIuuwnfx6KIFA4OxT2a1Yh5XdEGCAR3ucmAjSNdsHvzjMsYjpiWbK9vMzmAp2P7Z9v/9xVVeCUzMick8JvOGBHpt3DwHP7i88/X5Ym/f1aZxF19LKdK12p1/P9qD5f0TkdTQI2nVcUUPPMghwqqs86kFmGL0rPBrAD/WjI1i53xcY+VxgRdcPUjZKbHbJE/wM8v8Gz7VvqmvYm601nHgKmeYekZ0DWJw5e1xucfYLgygnWDv24MSBCMsGIMFfRX0b+IOKH+YPoXWsKU9ZDyyBN4C38kHjgFHcJ8zQA0XlBMv7JilNxvIWHUKQlZlvXnDnhl3Zil+oTB/0s= [email protected]"

    # allow sudo without password
    - name: add sudoers file for simone
      tags: always
      copy:
        src: sudoer_simone
        dest: /etc/sudoers.d/simone
        owner: root
        group: root
  • Add the sudoer_simone file inside the ./files dir.
simone ALL=(ALL) NOPASSWD: ALL

NOTE: we can remove the --ask-become-pass cmd line parameter by updating our ansivle.cfg file.

  • Update the ansible.cfg file to remove --ask-become-pass cmd line parameter.
# this file is read by ansible whenever we run it
[defaults]
inventory = inventory
private_key_file = ~/.ssh/raspberrypi
remote_user = simone

NOTE: There are a couple of more ways to use the authorized_keys module.

- name: Set authorized key taken from file
  ansible.posix.authorized_key:
    user: charlie
    state: present
    key: "{{ lookup('file', '/home/sid/.ssh/raspberrypi.pub') }}"

- name: Set authorized keys taken from url
  ansible.posix.authorized_key:
    user: charlie
    state: present
    key: https://github.com/sid.keys

2. Bootstrapping

  • We can now bootstrap our servers with package updates and setting up users before we configure the various servers, i.e., web_server, db_server and file_server.
  • This helps us setup servers with the default users and elevated permissions required on all the servers.
---
# ansible-playbook --ask-become-pass bootstrap.yml

# runs on all servers
- hosts: all
  become: true
  tasks:
    # `pre_tasks:` can be used with roles when we want to mandate some things to run before others
    - name: install updates (CentOS)
      tags: always
      dnf:
        update_only: true
        update_cache: true
      when: ansible_distribution = "CentOS"

    - name: install updates (Ubuntu)
      tags: always
      apt:
        upgrade: dist
        update_cache: true
      when: ansible_distribution == "Ubuntu"

- hosts: all
  become: true
  tasks:
    - name: create user simone
      tags: always
      user:
        name: simone
        groups: root

    - name: add ssh-key for simone
      tags: always
      authorized_keys:
        user: simone
        # ansible ssh public key
        key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQChspJkMu9vZ2BmL3X2R8hVBHzT1BXZklUTPwmd8vq5DrpX4SZ9wTymwPm0EoeS2Fp6KDyAAKY88p7m6SRSpC0/KJtBQK4JVjpVTnWI3DYATb2amkomJpicAsc2Hbb+EBkDgF7rznQo7VLmQ3nSNG72Qm1oL5/WU3fjroiIFXH09zf3Cv0FCdIuuwnfx6KIFA4OxT2a1Yh5XdEGCAR3ucmAjSNdsHvzjMsYjpiWbK9vMzmAp2P7Z9v/9xVVeCUzMick8JvOGBHpt3DwHP7i88/X5Ym/f1aZxF19LKdK12p1/P9qD5f0TkdTQI2nVcUUPPMghwqqs86kFmGL0rPBrAD/WjI1i53xcY+VxgRdcPUjZKbHbJE/wM8v8Gz7VvqmvYm601nHgKmeYekZ0DWJw5e1xucfYLgygnWDv24MSBCMsGIMFfRX0b+IOKH+YPoXWsKU9ZDyyBN4C38kHjgFHcJ8zQA0XlBMv7JilNxvIWHUKQlZlvXnDnhl3Zil+oTB/0s= [email protected]"
    # allow sudo without password
    - name: add sudoers file for simone
      tags: always
      copy:
        src: sudoer_simone
        dest: /etc/sudoers.d/simone
        owner: root
        group: root
        mode: 0440
# site.yml -> updating the repository index
# update_only option in the package manager line (dnf/apt) -> now only updates repo cache
apt:
- update_only: true
  update_cache: true
dnf:
- update_only: true
  update_cache: true
# changed_when is a new ansible module
+ changed_when: false
  • Once we configure the bootstrap.yml and site.yml files, we can then bootstrap our servers and run the site.yml playbook.
ansible-playbook --ask-become-pass bootstrap.yml # ``ask-become-pass required for new servers setup
ansible-playbook site.yml

Anisble Roles

  • Create Roles basically an Ansible PlayBook only
  • Add all the plays(tasks) in that particular role
  • For example, you can add some Bootstrapping scripts as Plays like an apt-get update
  • These types of roles can be used to set up a server which has nothing on it.
  • So, this can actually download all the latest packages required by the system.

We need to create a new dir called roles in the base repository and sub-folders within it according to the name of the roles we want.

  • Each of these sub-directories for roles will have another directory called tasks.
  • The tasks folder will contain the playbook for the tasks associated with that role. These "task books" are where we define the actual task associated with the specific servers.
  • We will name these "task books" as main.yml.
# create roles dir
mkdir -p roles && cd roles/
# create sub-dirs for each role
mkdir -p base workstations web_servers db_servers file_servers
# tasks dirs within each role sub-dir
mkdir -p base/tasks workstations/tasks web_servers/tasks db_servers/tasks file_servers/tasks
# creating the taskbooks
touch base/tasks/main.yml workstations/tasks/main.yml web_servers/tasks/main.yml db_servers/tasks/main.yml file_servers/tasks/main.yml

NOTE: Any role using files should also have a files/ dir to store the files for that role (within the role dir, alongside with the tasks dir)

Refer the site.yml and roles/ for detailed code.

Host Variables

  • These help us generalize our playbooks and have more control, plus it is easier to read and manage longer playbooks.
  • These variables co-relate to the variables defined in this section.
  • Create a new dir in the root folder called host_vars/.
  • Create files for each host here with their hostname or ip-addresses or dns-name (in the inventory file).
mkdir -p host_vars && cd host_vars
touch 172.16.250.132.yml 172.16.250.133.yml 172.16.250.134.yml 172.16.250.135.yml 172.16.250.248.yml
# 172.16.250.132.yml Ubuntu server
apache_package_name: apache2
apache_service: apache2
php_package_name: libapache2-mod-php
# 172.16.250.132.yml CentOS server
apache_package_name: httpd
apache_service: httpd
php_package_name: php
# ... for other servers as well

We can now use these variables within our roles for each "taskbook".

Ansible Handlers

  • A handler is triggered and any one of the plays in a playbook and taskbook can trigger that change.
  • Previous approach is not the best method to handle/register changes using ansible.
  • We can use handlers to restart a service if the registered variables register a change.
  • They are executed only once.
  • They notify ansible and execute at the very end.
  • Registers cannot handle multiple changes as efficiently as handlers do, hence it is best practice to use handlers.

We need to create a handlers dir in the role we need to register changes and notify ansible about it and create a file called main.yml inside it.

cd roles/web_service && mkdir -p handlers && cd handlers/
touch main.yml
# handlers/main.yml
- name: restart_apache
  service:
    name: "{{ apache_service }}"
    state: restarted
- registered: apache
+ notify: restart_apache # notify a play called `restart_apache`

- - name: restart httpd (CentOS)
-   tags: apache,centos,httpd
-   service:
-     name: "{{ apache_service }}"
-     state: restarted
-   when: apache.changed

Templates

_ Create ssh templates to configure the ssh service. The ssh config file is present at /etc/ssh/sshd_config in most of the linux distros.

  • The config may vary according to distros, but the location of the config file remains the same.
#since we want this config for all servers
cd roles/base
mkdir -p templates && cd templates
# copy default sshd_config in Jinja2 template
cp /etc/ssh/sshd_config sshd_config_ubuntu.j2
  • In order to copy this file from a server:
scp 172.16.250.248:/etc/sshd/sshd_config sshd_config_centos.js
# in case the above cmd says permission denied, ssh into the server and type:
chmod a+r /etc/ssh/sshd_config
# to revert the permissions:
chmod o-r /etc/ssh/sshd_config
  • Add the following line to the ubuntu and centos jinja2 templates: AllowUsers {{ ssh_users }}

  • Next, we need to update the host_vars files for each server.

# for ubuntu servers
+ ssh_users: "sid simone"
+ ssh_template_file: "sshd_config_ubuntu.js"
# for centos servers
+ ssh_users: "sid simone"
+ ssh_template_file: "sshd_config_centos.js"
  • Finally, we will add a play to the main.yml in the base role:
- name: generate ssh_config from template
  tags: ssh
  template:
    src: "{{ ssh_template_file }}"
    dest: /etc/ssh/sshd_config
    owner: root
    group: root
    mode: 0644
  notify: restart_sshd
# create handlers in base role
cd roles/base && mkdir -p handlers && cd handlers && touch main.yml
  • Add restart_sshd handler for the base role:
- name: restart_sshd
  service:
    name: sshd
    state: restarted

Task Iteration with Loops

  • [WIP]

Running playbooks in check mode

  • Ansible’s check mode allows you to execute a playbook without applying ANY ALTERATIONS to your systems.
  • You can use check mode to test playbooks before implementing them in a production environment.
  • Check mode offers a safe and practical approach to examine the functionality of your playbooks without risking unintended changes to your systems. Moreover, it is a valuable tool for troubleshooting playbooks that are not functioning as expected.
ansible-playbook --check playbook.yaml

Miscellaneous

Folder structure

Subdirectories:

mkdir -p defaults files handlers meta tasks templates tests vars

NOTE: anything that is not a Jinja2 template goes into the files folder.

Running ansible playbooks

Use the following command to run an asible playbook with custom (local) variables:

# key_state: absent/present
ansible-playbook import-ssh-key.yml -e @vars.yml -vvvv