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.
Example
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
pre_tasks:
- name: 'install python2'
raw: sudo apt-get -y install python-simplejson
vars:
UBUNTU_COMMON_DEPLOY_PASSWORD: "XXX"
ubuntu_common_deploy_user_name: devops
ubuntu_common_ssh_port: 22
tasks:
- name: Add deploy user
user:
name: "{{ ubuntu_common_deploy_user_name }}"
password: "{{ UBUNTU_COMMON_DEPLOY_PASSWORD }}"
shell: /bin/bash
- name: Add authorized keys for deploy user
authorized_key:
user: "{{ ubuntu_common_deploy_user_name }}"
key: "{{ lookup('file', '~/.ssh/ssh_id_rsa.pub') }}"
- name: Add deploy user to sudoers
lineinfile:
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
lineinfile:
dest: /etc/ssh/sshd_config
regexp: "^PasswordAuthentication"
line: "PasswordAuthentication no"
state: present
notify: restart ssh
- name: Disallow root SSH access
lineinfile:
dest: /etc/ssh/sshd_config
regexp: "^PermitRootLogin"
line: "PermitRootLogin no"
state: present
notify: restart ssh
- name: Allow SSH on fw
ufw:
rule: allow
port: "{{ item }}"
proto: tcp
with_items:
- "{{ ubuntu_common_ssh_port }}"
notify:
- restart ufw
handlers:
- 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:
[apps]
192.168.10.1
192.168.10.2
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.