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.