LowEndBox - Cheap VPS, Hosting and Dedicated Server Deals

Managing Your LowEndEmpire With Ansible, Part 2

In the previous tutorial, we walked through setting up Ansible on both the control (master) node and on target nodes.  Now let’s look at using Ansible capabilities.

Writing Playbooks

One cool thing about Ansible is that hosts can have various roles, and you can layer these roles. So for example you could have roles like this:

  • a “common” role that contains tasks to be run on all hosts
  • a “backup-client” role for hosts that are backed up
  • a “db” role for hosts that operate as database servers

etc. In this tutorial, we’ll have a “common” role that we intend to run on all hosts. That we’ll create another role called ‘db’ for database servers.

In /ansible, create the following playbook file called db.yml:

- hosts: db

    - common

Note that we are using YAML, which is very fussy about syntax, paticular spaces, so if you get an error, make sure it’s as show above.


Ansible comes with a wide variety of modules, which are packages that can be used to issue commands on target hosts. These cover a ton of common tasks. Some examples:

  • the ‘apt’ module can be used to install and remove packages using apt on Debian. There are also ‘yum’, ‘pacman’ and other package manager modules.
  • modules such as ‘user’, ‘group’, etc. can add/remove users and groups
  • locale_gen, timezone, and other modules can be used for system configuration
  • the ‘postgresql*’ set of modules and the ‘mysql*’ modules can be used to add/remove databases, add/remove users, etc.

Etcetera. So if you want to make sure that a certain package is available on your server, you don’t need to code dpkg, apt-get, etc. commands. You can just use the relevant Ansible module.

If something you need is not covered by a stock module, there are also modules for modifying files, making sure a line is present in a file, etc. which allow for configurations not covered by the Ansible distributed modules. And of course you can always copy a script from your control node and execute it.

See the docs for a full list of all modules and capabilities.

Creating a Playbook

Create the following directory structure:

mkdir -p /ansible/roles/common/tasks

Now we’ll create the actual tasks we’re going to execute for the ‘common’ role. In /ansible/roles/common/tasks, create the file main.yml as follows. You do not need to include the comments – they are explanatory text for purposes of this tutorial.

The ‘name’ portion of each task is whatever you choose to call that task. The following line contains the module name followed by a colon, and then specific arguments and options for that module.


  # this task will run dpkg-reconfigure locales and ensure that
  # en_US.UTF-8 is present. Modify for your locale.

  - name: locale generation
    locale_gen: name=en_US.UTF-8 state=present

  # this task will run apt-get update

  - name: apt-get update
    apt: update_cache=yes

  # this task will run apt-get upgrade

  - name: apt-get upgrade
    apt: upgrade=dist

  # this task will install some packages we want on all hosts

  - name: basic packages
    apt: name=bsd-mailx,bzip2,cron,dnsutils,gpg,git,man,sqlite,unzip,vim,wget,whois,zip state=latest

  # this task will set the localtime to US/Pacific. Modify to taste.

  - name: set timezone
      name: US/Pacific

  # this task will make sure that cron is enable in systemd

  - name: cron enable
    service: name=cron enabled=yes state=started

  # this task ensures that root's .bash_profile exists
  - name: make sure /root/.bash_profile exists
    path: /root/.bash_profile
    state: touch

  # this task ensures that 'set -o vi' is in root's .bash_profile

  - name: set -o vi for root
    path: /root/.bash_profile
    state: present
    regexp: '^set -o vi'
    line: set -o vi

Then run the ansible-playbook file against db.yml:

root@master:/ansible# ansible-playbook db.yml

PLAY [db] **********************************************************************

TASK [Gathering Facts] *********************************************************
ok: [target.example.com]

TASK [common : locale generation] **********************************************
ok: [target.example.com]

TASK [common : apt-get update] *************************************************
changed: [target.example.com]

TASK [common : apt-get upgrade] ************************************************
ok: [target.example.com]

TASK [common : basic packages] *************************************************
ok: [target.example.com]

TASK [common : set timezone to US/Pacific] *************************************
ok: [target.example.com]

TASK [common : cron enable] ****************************************************
ok: [target.example.com]

TASK [common : make sure /root/.bash_profile exists] ***************************
changed: [target.example.com]

TASK [common : set -o vi for root] *********************************************
changed: [target.example.com]

PLAY RECAP *********************************************************************
target.example.com : ok=9 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

Ansible’s Power

Now you may say to yourself “so you’ve set the locale and timezone and installed some packages – so what?” But the key points here are:

  • You didn’t have to do it manually.
  • Because it’s automated, it’s done identically every single time.
  • You can do this as easily on one host as on a thousand hosts.
  • Ansible allows you to set roles that baseline your environment. So every single host you use will be setup exactly as you wish.
  • You can run these commands every night to make sure no host drifts from the configuration you want. Think of these as “policies” that enforce how you want each server to be configured.

Adding a Database Role

Modify your db.yml to add a db-server role:

- hosts: db

    - common
    - db-server


mkdir -p /ansible/roles/db-server/tasks

And create a main.yml there with these tasks:


  # make sure that postgres is installed

  - name: postgres package
    apt: name=postgresql-11,python-ipaddress state=latest

  # make sure postgresql is configured to start on boot

  - name: postgres enable
    service: name=postgresql enabled=yes state=started

  # enable md5 (password) connections locally

    - name: modify pg_hba.conf to allow md5 connections
        dest: /etc/postgresql/11/main/pg_hba.conf
        contype: local
        users: all
        databases: all
        method: md5
        state: present

Now run

ansible-playbook db.yml

You’ll see Ansible walk through all the ‘common’ tasks, and then:

TASK [db-server : postgres packages] *******************************************
changed: [target.example.com]

TASK [db-server : postgres enable] *********************************************
ok: [target.example.com]

TASK [db-server : modify pg_hba.conf to allow md5 connections] *****************
changed: [target.example.com]

Other PostgreSQL modules allow us to create databases, setup users, grant permissions, etc.

Copying Files and Using Templates

Ansible allows the easy distribution of files, either for configuration or application purposes. It also comes with a powerful templating package called Jinja2 that can modify these templates so that they are customized properly for each system.

Let’s add a mail-server role. We’ll use postfix.

Modify db.yml again:

- hosts: db

    - common
    - db-server
    - mail-server

Then execute:

mkdir -p /ansible/roles/mail-server/tasks

And edit /ansible/roles/mail-server/tasks/main.yml:


  # ensure postfix packages are installed

  - name: postfix package package
    apt: name=postfix state=latest

  # template /etc/mailname

  - name: /etc/mailname
    template: src=/ansible/src/mailname.j2 dest=/etc/mailname owner=root group=0 mode=0644

  # copy /etc/postfix/main.cf

  - name: postfix main.cf
    template: src=/ansible/src/main.cf.j2 dest=/etc/postfix/main.cf owner=root group=0 mode=0644

  # make sure postfix is configured to start on boot and restart it
  # in case main.cf is changed

  - name: postfix enable and restart
    service: name=postfix enabled=yes state=restarted

  # set the root: alias in /etc/aliases

  - name: root alias in /etc/aliases
      path: /etc/aliases
      state: present
      regexp: '^root:'
      line: 'root: someone@somewhere.com'

  # run newaliases

  - name: newaliases
    command: /usr/bin/newaliases

Now let’s create our templates. In /ansible/src/mailname.j2, enter this text:

{{ ansible_host }}

This is a Jinja2 template (hence the .j2 ending). Text in between the double braces will be replaced with variables. Ansible supports many different variables. In this case we are using ‘ansible_host’ which will be replaced with ‘target.example.com’.

Here is /ansible/src/main.cf.j2, our postfix configuration:

smtpd_banner = $myhostname ESMTP $mail_name (Debian/GNU)
biff = no
append_dot_mydomain = no
readme_directory = no
compatibility_level = 2
smtpd_tls_session_cache_database = btree:${data_directory}/smtpd_scache
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_unauth_destination
myhostname = {{ ansible_node_name }}
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
myorigin = /etc/mailname
mydestination = $myhostname, localhost.example.com , localhost
relayhost =
mynetworks = [::ffff:]/104 [::1]/128
mailbox_size_limit = 0
recipient_delimiter = +
inet_interfaces = all
inet_protocols = all

Of course many Postfix variables could be set – this is a tutorial on Ansible not Postfix.

Now run:

ansible-playbook db.yml

And after the ‘common’ and ‘db-server’ sections, you’ll see the ‘mail-server’ tasks run:

TASK [mail-server : postfix package package] ***********************************
changed: [target.example.com]

TASK [mail-server : /etc/mailname] *********************************************
changed: [target.example.com]

TASK [mail-server : postfix main.cf] *******************************************
changed: [target.example.com]

TASK [mail-server : postfix enable and restart] ********************************
changed: [target.example.com]

TASK [mail-server : root alias in /etc/aliases] ********************************
changed: [target.example.com]

TASK [mail-server : newaliases] ************************************************
changed: [target.example.com]

And if we look on the system, we see that the proper template substitutions have been made:

root@master:/ansible# cat /etc/mailname

Wrap Up

Although we’ve only scratched the surface of Ansible’s capabilities, hopefully you can see the tremendous power and flexibility of the product.  With Ansible playbooks, you can both setup new systems quickly and enforce policies on a continuous basis.




  1. Arthur:

    Great work!
    PS.: The link to the previous tutorial is broken.

    December 7, 2020 @ 6:37 pm | Reply

Leave a Reply

Some notes on commenting on LowEndBox:

  • Do not use LowEndBox for support issues. Go to your hosting provider and issue a ticket there. Coming here saying "my VPS is down, what do I do?!" will only have your comments removed.
  • Akismet is used for spam detection. Some comments may be held temporarily for manual approval.
  • Use <pre>...</pre> to quote the output from your terminal/console, or consider using a pastebin service.

Your email address will not be published. Required fields are marked *