I haven’t used configuration management tools in a very long time because I didn’t know which tool to pick (Chef? Puppet? Ansible? Salt? cfengine?). Now I use Ansible and am very happy with it.

Here’s how you start – with an empty git repository:

$ mkdir my-infra
$ cd my-infra
$ git init .

Let’s start by making sure a couple of Ubuntu servers will have unattended upgrades enabled. First we tell Ansible what the servers are

$ vim hosts
$ cat hosts
debesis
ranka

Let’s make sure Ansible can ssh to those hosts:

$ ansible -i hosts all -m ping
ranka | SUCCESS => {
    "changed": false, 
    "ping": "pong"
}
debesis | SUCCESS => {
    "changed": false, 
    "ping": "pong"
}

I’ve an ~/.ssh/config that expands these nicknames to the full hostnames, and I’ve a private SSH key that allows me access without providing my password every time. The key is passphrase-protected and I use ssh-agent to avoid typing my passphrase every time. Notice a trend? I like convenience.

Speaking of convenience, it’s annoying to have to specify -i hosts every time (since the default inventory file is /etc/ansible/hosts and I never ever want that), so

$ vim ansible.cfg
$ cat ansible.cfg
[defaults]
inventory = hosts

Now ansible all -m ping should work. All this needs on the servers is SSH and Python (2 or 3).

We can do more useful ad-hoc stuff too, like apply any pending security updates:

$ ansible all -m apt -a 'update_cache=yes upgrade=dist' -u root
debesis | SUCCESS => {
    "changed": false, 
    "msg": "Reading package lists...\nBuilding dependency tree...\nReading state information...\n0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.\n", 
    "stderr": "", 
    "stdout": "Reading package lists...\nBuilding dependency tree...\nReading state information...\n0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.\n", 
    "stdout_lines": [
        "Reading package lists...", 
        "Building dependency tree...", 
        "Reading state information...", 
        "0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded."
    ]
}
ranka | SUCCESS => {
    "changed": false, 
    "msg": "Reading package lists...\nBuilding dependency tree...\nReading state information...\n0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.\n", 
    "stderr": "", 
    "stdout": "Reading package lists...\nBuilding dependency tree...\nReading state information...\n0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.\n", 
    "stdout_lines": [
        "Reading package lists...", 
        "Building dependency tree...", 
        "Reading state information...", 
        "0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded."
    ]
}

This runs apt-get update && apt-get dist-upgrade, so it needs root access. You can ssh directly as root (-u root), or you can use sudo (-b -K). I prefer ssh’ing as root because I don’t want to be typing my sudo password every time I run ansible. Let’s make this the default then:

$ vim ansible.cfg
$ cat ansible.cfg
[defaults]
inventory = hosts
remote_user = root

Now, back to unattended-upgrades. Let’s create a playbook:

$ vim setup.yml
$ cat setup.yml
---
- hosts: all
  tasks:
    - name: install unattended-upgrades
      apt: name=unattended-upgrades state=present

    - name: enable unattended-upgrades
      copy:
        dest: /etc/apt/apt.conf.d/50unattended-upgrades-local
        content: |
          APT::Periodic::Update-Package-Lists "1";
          APT::Periodic::Unattended-Upgrade "1";
          Unattended-Upgrade::Mail "root";
          Unattended-Upgrade::Remove-Unused-Dependencies "true";
          Unattended-Upgrade::Automatic-Reboot "true";
          Unattended-Upgrade::Allowed-Origins {
            "${distro_id}:${distro_codename}-security";
            "${distro_id}:${distro_codename}-updates";
          };

It does two things: install a package and set up a config file. Let’s see what it would do:

$ ansible-playbook setup.yml -CD

PLAY [all] *********************************************************************

TASK [setup] *******************************************************************
ok: [ranka]
ok: [debesis]

TASK [install unattended-upgrades] *********************************************
ok: [ranka]
ok: [debesis]

TASK [enable unattended-upgrades] **********************************************
changed: [ranka]
--- before
+++ after: /tmp/tmpcIWt6O
@@ -0,0 +1,9 @@
+APT::Periodic::Update-Package-Lists "1";
+APT::Periodic::Unattended-Upgrade "1";
+Unattended-Upgrade::Mail "root";
+Unattended-Upgrade::Remove-Unused-Dependencies "true";
+Unattended-Upgrade::Automatic-Reboot "true";
+Unattended-Upgrade::Allowed-Origins {
+  "${distro_id}:${distro_codename}-security";
+  "${distro_id}:${distro_codename}-updates";
+};

changed: [debesis]
--- before
+++ after: /tmp/tmpePUnG4
@@ -0,0 +1,9 @@
+APT::Periodic::Update-Package-Lists "1";
+APT::Periodic::Unattended-Upgrade "1";
+Unattended-Upgrade::Mail "root";
+Unattended-Upgrade::Remove-Unused-Dependencies "true";
+Unattended-Upgrade::Automatic-Reboot "true";
+Unattended-Upgrade::Allowed-Origins {
+  "${distro_id}:${distro_codename}-security";
+  "${distro_id}:${distro_codename}-updates";
+};


PLAY RECAP *********************************************************************
debesis                    : ok=3    changed=1    unreachable=0    failed=0   
ranka                      : ok=3    changed=1    unreachable=0    failed=0   

-CD stands for --check --diff, which tells Ansible not to make any changes, but show what it would do, with full unified diffs. Ansible’s killer feature IMHO. Probably the main reason I why switched from Fabric to Ansible.

Everything looks fine, so we can apply the changes – but let’s be cautious and only do it for only one machine for now

$ ansible-playbook setup.yml --limit debesis

PLAY [all] *********************************************************************

TASK [setup] *******************************************************************
ok: [debesis]

TASK [install unattended-upgrades] *********************************************
ok: [debesis]

TASK [enable unattended-upgrades] **********************************************
changed: [debesis]

PLAY RECAP *********************************************************************
debesis                    : ok=3    changed=1    unreachable=0    failed=0   

And we’re done!

$ git add .
$ git commit -m "Set up unattended-upgrades"

Ansible has great documentation, if you’d like to learn more.


Updated 2020-05-23 to remove the bit about requiring Python 2.x specifically. Python 3 works too nowadays, and Python 2 is EOL.