If you’d like to enable unattended updates in Ubuntu and also configure a time for reboots to occur then the following should help you.
Now, manual steps can be found on the Ubuntu website but below I’ve drafted up some Ansible code for those of us that want to manage things using a Configuration as Code (CaS) model.
Our playbook consists of a small number of components:
- Inventory file (usually found at: /etc/ansible/hosts )
- Variables file (/vars/main.yml)
- Playbook (unattended_upgrades.yml)
.
├── unattended_upgrades.yml
├── vars
│ └── main.yml
Inventory
First off, you’ll want to build out your Ansible inventory so that you have three groups:
- patch_group_a
- patch_group_b
- patch_group_c
For example:
[patch_group_a]
server1
server3
[patch_group_b]
server2
server4
[patch_group_c]
server5
Variables
The playbook assumes that you have a file under the ‘vars’ directory
You can change the group_names and timings by editing this file, which comes next:
---
reboot_offset: 15m
install_time: "{%- if 'patch_group_a' in group_names -%}
01:30
{%- elif 'patch_group_b' in group_names -%}
02:30
{%- elif 'patch_group_c' in group_names -%}
03:30
{% else %}
03:30
{%- endif -%}"
reboot_time: "{%- if 'patch_group_a' in group_names -%}
02:00
{%- elif 'patch_group_b' in group_names -%}
03:00
{%- elif 'patch_group_c' in group_names -%}
04:00
{% else %}
04:00
{%- endif -%}"
You can easily add or remove groups in the template or change the respective values for reboot_time.
Playbook
The main playbook does the following:
- Installs: update-notifier-common
- Creates: unattended upgrades configuration file
- Enables: automated reboots
Note that the goal here is configuring an already existing service, namely, ‘unattended-upgrades‘. Specifically, we’re controlling the reboots.
What does the actual work of installing the updates is the unattended-upgrades script which gets invoked when the ‘apt-daily-upgrade.service‘ runs.
The timing for the execution of ‘apt-daily-upgrade.service‘ is controlled via a systemd timer named ‘apt-daily-upgrade.timer‘.
See the reference section at the end for full documentation.
Reboots
While this is sufficient to enable automatic reboots, it might not be exactly what you have in mind because the schedule by which the updates are installed is controlled separately via the apt-daily-upgrade.timer.
The configuration file can be found at:
[Unit]
Description=Daily apt upgrade and clean activities
After=apt-daily.timer
[Timer]
OnCalendar=*-*-* 6:00
RandomizedDelaySec=60m
Persistent=true
[Install]
WantedBy=timers.target
As one can see there’s quite a bit of scope for customising when it will run and, ideally, you’ll probably want it to run as close to your specified reboot window as possible.
To achieve this using Ansible the vars file contains some jinja2 templating which allows us to dynamically set the value of variables based on whether a host is a member of an inventory group or not.
In our implementation here we’re reducing the randomized delay from 60 minutes down to 15 minutes, and setting a gap between install and reboot of 30 minutes. Given the offset value this means a worst-case scenario of a host having 15 minutes to install all updates before the reboot (and a best-case of 45 minutes). That’s fine for me but you may want to increase both values based on the needs of your environment.
Our final playbook will now look like this:
- name: Configure automated updates (Ubuntu)
hosts: all
become: true
gather_facts: false
vars_files:
- vars/main.yml
tasks:
## https://help.ubuntu.com/community/AutomaticSecurityUpdates
- name: Install apt prerequisites for automated reboots
ansible.builtin.apt:
name: update-notifier-common
state: present
- name: Create unattended upgrades configuration file
ansible.builtin.blockinfile:
dest: /etc/apt/apt.conf.d/20auto-upgrades
block: |
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
marker: "// {mark} ANSIBLE MANAGED BLOCK - unattended_upgrades settings"
create: true
mode: "0644"
owner: root
group: root
register: unattended_upgrades_config_set
- name: Enable automated reboots
ansible.builtin.blockinfile:
dest: /etc/apt/apt.conf.d/50unattended-upgrades
block: |
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "{{ reboot_time }}";
marker: "// {mark} ANSIBLE MANAGED BLOCK - unattended_upgrades settings"
create: true
mode: "0644"
owner: root
group: root
register: unattended_upgrades_settings_set
- name: Dpkg reconfigure
ansible.builtin.command:
cmd: dpkg-reconfigure -f noninteractive unattended-upgrades
register: dpkg_reconfigure_unattended_upgrades
when:
- unattended_upgrades_config_set.changed or
unattended_upgrades_settings_set.changed
- name: Configure updates installation timing
ansible.builtin.lineinfile:
path: /lib/systemd/system/apt-daily-upgrade.timer
regexp: '^OnCalendar'
line: OnCalendar=*-*-* {{ install_time }}
- name: Configure updates installation timing offset
ansible.builtin.lineinfile:
path: '/lib/systemd/system/apt-daily-upgrade.timer'
regexp: '^RandomizedDelaySec'
line: 'RandomizedDelaySec={{ reboot_offset}}'
Validation
Naturally, the best way to validate changes aside from checking the idempotency of the Ansible playbook is to see whether things work as expected. However, there are also some additional checks that you can run.
Firstly, you can verify whether the timer that triggers the service has picked up the correct schedule.
Let’s verify the config contents:
grep -E OnCalendar /lib/systemd/system/apt-daily-upgrade.timer
Which should hopefully match what the playbook has configured. For example:
OnCalendar=*-*-* 01:30
Then we’ll check the actual value being used by the Systemd timer unit:
systemctl status apt-daily-upgrade.timer
● apt-daily-upgrade.timer - Daily apt upgrade and clean activities
Loaded: loaded (/lib/systemd/system/apt-daily-upgrade.timer; enabled; preset: enabled)
Active: active (waiting) since Fri 2023-10-20 22:22:53 BST; 17h ago
Until: Fri 2023-10-20 22:22:53 BST; 17h ago
Trigger: Sun 2023-10-22 01:37:11 BST; 9h left
Triggers: ● apt-daily-upgrade.service
Note how the Trigger value matches up with our configured value, taking into account the randomized offset of up to 15 minutes (in this case our actual offset is about 8 minutes).
Now, if we travel forward in time to the date of the trigger and query the logs for the service:
grep -E apt-daily-upgrade.service /var/log/syslog
Whose output should show something like this:
2023-10-22T01:37:11.887415+01:00 localhost systemd[1]: Starting apt-daily-upgrade.service - Daily apt upgrade and clean activities...
2023-10-22T01:37:54.120174+01:00 localhost systemd[1]: apt-daily-upgrade.service: Deactivated successfully.
2023-10-22T01:37:54.121916+01:00 localhost systemd[1]: Finished apt-daily-upgrade.service - Daily apt upgrade and clean activities.
2023-10-22T01:37:54.123701+01:00 localhost systemd[1]: apt-daily-upgrade.service: Consumed 9.982s CPU time.
Another approach for validating the remaining settings is to use the apt-config dump command:
apt-config dump APT::Periodic::Unattended-Upgrade && \
apt-config dump APT::Periodic::Update-Package-Lists && \
apt-config dump Unattended-Upgrade::Automatic-Reboot && \
apt-config dump Unattended-Upgrade::Automatic-Reboot-Time
which should hopefully give us the results that we expect:
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::Update-Package-Lists "1";
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "02:00";
A note on update types
By default the unattended-upgrades package is configured to install only security updates. However, you can easily change this by uncommenting what you want from the configuration file. Here’s the top of the file and the settings that you want:
// Automatically upgrade packages from these (origin:archive) pairs
//
// Note that in Ubuntu security updates may pull in new dependencies
// from non-security sources (e.g. chromium). By allowing the release
// pocket these get automatically pulled in.
Unattended-Upgrade::Allowed-Origins {
"${distro_id}:${distro_codename}";
"${distro_id}:${distro_codename}-security";
// Extended Security Maintenance; doesn't necessarily exist for
// every release and this system may not have it installed, but if
// available, the policy for updates is such that unattended-upgrades
// should also install from here by default.
"${distro_id}ESMApps:${distro_codename}-apps-security";
"${distro_id}ESM:${distro_codename}-infra-security";
// "${distro_id}:${distro_codename}-updates";
// "${distro_id}:${distro_codename}-proposed";
// "${distro_id}:${distro_codename}-backports";
};
If you want to update applications in the same way as if you’d run an apt-upgrade then you’ll probably want to uncomment the line highlighted above.
Special considerations and issues
- Please note that the comment-syntax for the Ubuntu unattended config files uses double-forward slashes (e.g. ‘//’) and therefore using hashes for comments will cause a failure (e.g. ‘#’). As a result, the markers for the Ansible Blockinfile module must use the double-forward slash method.
- Since this all relies on unattended-upgrades functioning you’ll want to make sure everything is already working by checking your logs under:
/var/log/unattended-upgrades/
References
https://help.ubuntu.com/community/AutomaticSecurityUpdates
https://manpages.ubuntu.com/manpages/lunar/en/man8/unattended-upgrade.8.html