Ansible : Playbook to Perform System Configuration Tasks


In Ansible changing a system’s configuration with isn’t much more difficult than provisioning a new system.

For this, we will need the following facts for the new host:

  • ntp_servers
  • dns_servers
  • dns_search

We’ll also need to have a couple of templates to provision the following files:

  • /etc/logrotate.d/syslog
  • /etc/ntp.conf
  • /etc/ntp/step-tickers
  • /etc/resolv.conf


Let’s create the playbook to configure the system. Perform the following steps:

  1. Create a ~/playbooks/config.yml playbook with the following content:
    - name: Configure system
      hosts: all
      - include: networking.handlers.yml
      - include: ntp-client.handlers.yml
      - include: networking.tasks.yml
      - include: ntp-client.tasks.yml
      - include: logrotate.tasks.yml
  2. Create a ~/playbooks/networking.handlers.yml file with the following content:
      - name: reset-sysctl
        action: command /sbin/sysctl -p
  3. Now, create a ~/playbooks/ntp-client.handlers.yml file with the following content:
      - name: restart-ntpd
        action: service name=ntpd state=restarted enabled=yes
  4. Create a ~/playbooks/networking.tasks.yml file with the following content:
      - name: Set the hostname
        action: hostname name={{ inventory_hostname }}
      - name: Deploy sysctl template to disable ipv6
        action: template src=templates/etc/sysctl.d/ipv6.conf.el7 dest=/etc/sysctl.d/ipv6.conf
        notify: reset-sysctl
      - name: 'Detect if ::1 is in /etc/hosts'
        action: shell /bin/egrep '^\s*::1.*$' /etc/hosts
        register: hosts_lo_ipv6
        failed_when: false
        always_run: yes
      - name: 'Remove ::1 from /etc/hosts'
        action: lineinfile dest=/etc/hosts regexp='^\s*::1.*$' state=absent
        when: hosts_lo_ipv6.rc == 0
      - name: Configure DNS
        action: template src=templates/etc/resolv.conf.el7 dest=/etc/resolv.conf
  5. Next, create a ~/playbooks/ntp-client.tasks.yml file with the following content:
      - name: "Install ntpd (if it's not installed already)"
        action: yum name=ntp state=present
        notify: restart-ntpd
      - name: Configure the ntp daemon
        action: template src=templates/etc/ntp.conf.el7 dest=/etc/ntp.conf
        notify: restart-ntpd
      - name: Configure the step-tickers
        action: template src=templates/etc/ntp/step-tickers.el7 dest=/etc/ntp/step-tickers
        notify: restart-ntpd
  6. Create a ~/playbooks/logrotate.tasks.yml file with the following content:
      - name: Configure logrotate for rsyslog
        action: template src=templates/etc/logrotate.d/syslog.el7 dest=/etc/logrotate.d/syslog

This is it for the playbook. Now we need to create the templates:

  1. First, create a ~/playbooks/templates/etc/sysctl.d/ipv6.conf.el7 file with the following content:
    # {{ ansible_managed }}
    net.ipv6.conf.all.disable_ipv6 = 1
    net.ipv6.conf.default.disable_ipv6 = 1
    net.ipv6.conf.lo.disable_ipv6 = 1
  2. Then, create a ~/playbooks/templates/etc/resolv.conf.el7 file with the following content:
    # {{ ansible_managed }}
    search {{ dns_search|join(' ') }}
    {% for dns in dns_servers %}
    nameserver {{ dns }}
    {% endfor %}
  3. Create a ~/playbooks/templates/etc/ntp.conf.el7 file with the following content:
    # {{ ansible_managed }}
    driftfile /var/lib/ntp/drift
    restrict default nomodify notrap nopeer noquery
    restrict ::1
    {% for ntp in ntp_servers %}
    server {{ ntp }} iburst
    {% endfor %}
    includefile /etc/ntp/crypto/pw
    keys /etc/ntp/keys
    disable monitor
  4. Next, create a ~/playbooks/templates/etc/ntp/step-tickers.el7 file with the following content:
    # {{ ansible_managed }}
    {% for ntp in ntp_servers %}
    {{ ntp }}
    {% endfor %}
  5. Create a ~/playbooks/templates/etc/logrotate.d/syslog.el7 file with the following content:
    # {{ ansible_managed }}
        rotate 365
            /bin/kill -HUP `cat /var/run/ 2> /dev/null` 2> /dev/null || true
  6. Then, deploy the playbook to a newly created host by executing the following command:
    ~]# ansible-playbook --limit newhost ~/playbooks/config.yml
    PLAY [Configure system] **************************************
    GATHERING FACTS **********************************************
    ok: [newhost]
    TASK: [Set the hostname] *************************************
    skipping: [newhost]
    ok: [newhost]
    TASK: [Deploy sysctl template to disable ipv6] ***************
    changed: [newhost]
    TASK: [Detect if ::1 is in /etc/hosts] ***********************
    changed: [newhost]
    TASK: [Remove ::1 from /etc/hosts] ***************************
    changed: [newhost]
    TASK: [Configure DNS] ****************************************
    changed: [newhost]
    TASK: [Install ntpd (if it's not installed already)] *********
    ok: [newhost]
    TASK: [Configure the ntp daemon] *****************************
    changed: [newhost]
    TASK: [Configure the step-tickers] ***************************
    changed: [newhost]
    TASK: [Configure logrotate for rsyslog] **********************
    changed: [newhost]
    NOTIFIED: [reset-sysctl] *************************************
    skipping: [newhost]
    ok: [newhost]
    NOTIFIED: [restart-ntpd] *************************************
    changed: [newhost]
    PLAY RECAP ***************************************************
    newhost            : ok=9  changed=8  unreachable=0  failed=0  

Ansible is packed with lots of power tools. Two that are worth mentioning here and are lifesavers for debugging your playbooks are --check and --diff.

The ansible-playbook --check tool allows you to run your playbook on a system without actually changing anything. Why is this important, you ask? Well, the output of the playbook will list which actions of the playbook will actually change anything on the target system.

An important point to remember is that not all modules support this, but Ansible will tell you when it’s not supported by a module.

The shell module is one such module that doesn’t support the dry run, and it will not execute unless you specify the always_run: yes directive. Be careful with this directive as if the action would change anything, this directive will cause this change to be applied, even when specifying --check.

I added the 'Detect if ::1 is in /etc/hosts' action to the networking.tasks.yml file with the always_run: yes directive. This specific action just checks whether the line is present. The ergep returns code 0 if it finds a match and 1 if it doesn’t. It registers the result of the shell action to a variable (hosts_lo_ipv6).

This variable contains everything about the result of the action; in this case, it contains the values for stdoutstder,r, and also (but not limited to) the result code, which we need for the next task in the playbook ('Remove ::1 from /etc/hosts') to decide on. This way, we can introduce a manual form of idempotency into the playbook for modules that cannot handle idempotency due to whatever restrictions.

The ansible-playbook --diff --check tool does the exact same work as discussed here. However, it comes with an added bonus: it shows you what exactly will be changed in the form of a diff -u between what it actually is and what it’s supposed to be. Of course, once again, the module has to support it.

As you can see in this tutorial, Ansible allows us to create reusable code by creating separate task and handler yml files. This way, you could create other playbooks referring to these files, without having to reinvent the wheel.

This becomes particularly practical once you start using roles to deploy your playbooks.

Roles allow you to group playbooks and have them deployed according to the needs (that is, roles) of your server.

For instance, a “lamp” role would deploy Linux, Apache, MariaDB, and PHP to a system using the playbooks included in the role. Roles can define dependencies. These dependencies are other roles, and thus, the “lamp” role could be broken down into three more roles that may be more useful as separate roles: Linux, Dbserver, and ApachePHP.

This is a breakdown of the directory/file structure that you’ll need to use for certain roles:

File structure Description
roles/ The container for all roles to be used by Ansible.
roles/<role> This is the container for your role.
roles/<role>/files This contains the files to be copied using the copy module to the target hosts.
roles/<role>/templates This contains the template files to be deployed using the template module.
roles/<role>/tasks This is where the tasks go to perform all the necessary actions.
roles/<role>/tasks/main.yml This playbook is automatically added to the play when this role is applied to a system.
roles/<role>/handlers This is the location of your role handlers.
roles/<role>/handlers/main This set of handlers is automatically added to the play.
roles/<role>/vars This location holds all the variables for your role.
roles/<role>/vars/main.yml This set of variables is automatically applied to the play.
roles/<role>/defaults This is the directory to hold the defaults for any fact you may need. The facts/variables defined in this way have the lowest priority, meaning that your inventory will win in the event that a fact is defined in both.
role/<role>/defaults/main.yml This set of defaults is automatically added to the play.
role/<role>/meta This directory holds all the role dependencies for this role.
role/<role>/meta/main.yml This set of dependencies is automatically added to the play.

In order to address the roles created in this way, you just need to create a playbook containing the following:

- name: Deploy LAMP servers
  hosts: lamp
  - linux
  - DBserver
  - Apache-PHP

Alternatively, you could create a role lamp that has Linux, DBserver, and ApachePHP as the dependencies in the meta/main.yml file by creating it with the following contents:

  - { role: linux }
  - { role: DBserver, db_type: mariadb }
  - { role: Apache-PHP }


Please enter your comment!
Please enter your name here