Over the years I’ve had to manage an increasing number of servers, and recently I was tasked with an entire organisation’s servers of which I knew nothing about. The institutional knowledge had been lost - but the clock needs to keep ticking over.

After making sure everything was good and nothing would fall over, I decided to do some research into the best tool for the job. I came across Ansible, invested some time learning it, and have never looked back.

Why Ansible

There are a couple of configuration management / servers-as-code tools out there. One of the biggest reasons I like Ansible is that it is agentless. Nothing needs to live on any servers for it to do its thing. Generally, the less moving parts the better.

What agents have that Ansible doesn’t is continuous checking by default. Normally things like Salt will report back at intervals. This is easy enough to mimic with Ansible through cron, Ansible Tower or another CI/CD tool.


A few examples go a long way. I use Ansible for any task involving provisioning of servers, deployment of software, or even updating this blog.

For securing of servers, I followed guides like My First Five Minutes On A Server as well as Linode’s guide and codified the important bits. The below is an excerpt from the securing script for an Ubuntu server:


- hosts: all
  become: yes
  gather_facts: no
  # We do these pre tasks to make sure Ansible runs nicely on the server
    - name: 'install python2'
      raw: sudo apt-get -y install python-simplejson

    ubuntu_common_deploy_user_name: devops
    ubuntu_common_ssh_port: 22


  - name: Add deploy user
      name: "{{ ubuntu_common_deploy_user_name }}"
      password: "{{ UBUNTU_COMMON_DEPLOY_PASSWORD }}"
      shell: /bin/bash

  - name: Add authorized keys for deploy user
      user: "{{ ubuntu_common_deploy_user_name }}"
      key: "{{ lookup('file', '~/.ssh/ssh_id_rsa.pub') }}"

  - name: Add deploy user to sudoers
      dest: /etc/sudoers
      regexp: "{{ ubuntu_common_deploy_user_name }} ALL"
      line: "{{ ubuntu_common_deploy_user_name }} ALL=(ALL) NOPASSWD: ALL"
      state: present

  - name: Install aptitude
    apt: name=aptitude state=present update_cache=yes

  - name: Update APT package cache
    apt: update_cache=yes cache_valid_time=3600

  - name: Upgrade APT to the latest packages
    apt: upgrade=safe
  - name: Disallow password authentication
      dest: /etc/ssh/sshd_config
      regexp: "^PasswordAuthentication"
      line: "PasswordAuthentication no"
      state: present
    notify: restart ssh

  - name: Disallow root SSH access
      dest: /etc/ssh/sshd_config
      regexp: "^PermitRootLogin"
      line: "PermitRootLogin no"
      state: present
    notify: restart ssh

  - name: Allow SSH on fw
      rule: allow
      port: "{{ item }}"
      proto: tcp
    - "{{ ubuntu_common_ssh_port }}"
    - restart ufw

  - name: restart ssh
    service: name=ssh state=restarted

  - name: restart ufw
    ufw: state=restarted

This does most of what you would look for:

  • Adds a user
  • Adds keys for that user
  • Disables password login
  • Adds user to sudoers
  • Upgrades apt packages
  • Turns off password auth and disallows root access
  • Turns on the firewall and lets only SSH through

Handlers issue commands on completion of the task, so when you change ssh config, sshd gets restarted. Neat.

To use this playbook is just as easy:

ansible-playbook -i your.hosts playbook.yaml

This will run the playbook against all your servers as defined in your.hosts. Example:


You can group servers and have group specific variables, roles and more. That goes a little deeper than this short blog post though.


Hopefully from the above you can see it’s all pretty straightforward. Things can get hairy (as they always can with software) but I’m using one playbook Ansible for a whole host of tasks across projects. For the bigger projects, the structure is more complex with roles, group variables, and many playbooks. Ansible can scale to what you need.

Ansible playbooks are (should be) idempotent - you run it N times and always the same result happens. If you have a clean server or a server already prepped/deployed to, running Ansible playbooks will not change the server at all. Deploying any number of app servers, for example, is now a matter of adding those servers to a hosts file and letting Ansible do its magic. At the end of the run, all servers will look and act the same. The dream. This can even be automated, letting servers automatically scale, added to a load balancer, and more when load gets too high.

I’d highly recommend reading Ansible for DevOps - it’s been the most valuable investment into my software engineering education in the recent past.