Your may want to patch your Linux servers on a regular basis (e.g using “yum/dnf update”). As always, it’s obviously recommended to :
1) Patch the TEST systems
2) Check if there is no side effects
3) Wait few days or weeks
4) Patch the PROD systems
The problem here is that between step 1 and 4 above, a new version of the packages can be available on the public repository you use.
Consequently after step 4, you’ll have a more recent version of you packages on PROD than on TEST, which is a situation you probably want to avoid.
In my previous post, I explained how to update Linux OS with Ansible by patching all packages or installing security fixes only.
In this one, I’ll show you how to keep your different environments consistent in term of packages version, even when the patching is delayed by several weeks.
Inventory
We will use the following simple inventory for this demo :
[test] 54.93.103.197 # srvtest1 35.156.114.202 # srvtest2 [prod] 54.93.245.3 # srvprod1 3.67.8.87 # srvprod1
The VMs are running in AWS and are deployed with Terraform (have a look to my other post if you want to know how to do it).
Both host groups have a variable “gv_env” defined :
$ tree inventories/group_vars/ inventories/group_vars/ ├── prod.yml └── test.yml 0 directories, 2 files
$ cat inventories/group_vars/* gv_env: prod gv_env: test
$ ansible-inventory -i inventories/hosts --graph --vars @all: |--@prod: | |--3.67.8.87 | | |--{gv_env = prod} | |--54.93.245.3 | | |--{gv_env = prod} | |--{gv_env = prod} |--@test: | |--35.156.114.202 | | |--{gv_env = test} | |--54.93.103.197 | | |--{gv_env = test} | |--{gv_env = test} |--@ungrouped:
Playbook
--- - name: Patch TEST or PROD servers hosts: all gather_facts: true roles: - os_update ...
Role
--- - include_tasks: test.yml when: - ev_env_to_patch == "test" - inventory_hostname in groups['test'] - include_tasks: prod.yml when: - ev_env_to_patch == "prod" - inventory_hostname in groups['prod'] ...
Task – test.yml
The first one is to list all packages that will be modified. Thus, the user can double-check what will be installed before moving on, or cancel the process if desired :
--- - name: Get packages that can be upgraded on TEST servers become: true ansible.builtin.yum: list: updates state: latest update_cache: yes register: reg_yum_output_all - name: List packages that will be upgraded on TEST servers ansible.builtin.debug: msg: "{{ reg_yum_output_all.results | map(attribute='name') | list }}" - name: Request user confirmation ansible.builtin.pause: prompt: | The packages listed above will be upgraded. Do you want to continue ? -> Press RETURN to continue. -> Press Ctrl+c and then "a" to abort.
Next step is to start the update :
- name: Upgrade packages on TEST servers become: true ansible.builtin.yum: name: '*' state: latest update_cache: yes update_only: no register: reg_upgrade_ok when: - reg_yum_output_all is defined - name: Print errors if upgrade failed ansible.builtin.debug: msg: "Packages upgrade failed" when: reg_upgrade_ok is not defined
If the Kernel has been updated, it’s strongly recommended to reboot the server. To check if a reboot is required, we can use the command “needs-restarting” provided here by the package dnf-utils :
- name: Install yum-utils on TEST servers become: true ansible.builtin.yum: name: 'yum-utils' state: latest update_cache: yes - name: Check if a reboot is required become: true command: needs-restarting -r register: reg_reboot_required ignore_errors: true failed_when: false changed_when: reg_reboot_required.rc != 0 notify: - Reboot server
As you probably noticed, the actions described so far are nearly the same as in my previous post.
Once the server is back the important step is to save in a text file the packages version freshly installed :
- name: Genereate the list of the upgraded packages on TEST servers become: true shell: "rpm -qa > /tmp/packages-set.txt" args: warn: false
You might have imported GPG public keys into your system to verify the signature of a package before installing it. If this is the case, the keys are also added to the RPM database. Therefore it’s required to remove them from the file :
- name: Delete the fake RPM packages gpg-pubkey from the list become: true ansible.builtin.lineinfile: path: /tmp/packages-set.txt regexp: 'gpg-pubkey-.*' state: absent
Once the file is ready, we fetch it from the Ansible Control Node :
- name: Fetch the file from the Ansible Control Node become: true ansible.builtin.fetch: src: /tmp/packages-set.txt dest: /tmp/ flat: yes
Now we need a directory on the PROD servers where the file will be copied to :
- name: Create directory to store the file on PROD servers become: true ansible.builtin.file: path: /etc/dbi state: directory mode: '0777' delegate_to: "{{ item }}" loop: "{{ groups['prod'] }}"
I used “delegate_to” to make this task executed by the PROD servers.
And finally, we copy the file to the PROD servers :
- name: Copy the file from the Ansible Control node to the PROD servers ansible.builtin.copy: src: /tmp/packages-set.txt dest: /etc/dbi force: true delegate_to: "{{ item }}" loop: "{{ groups['prod'] }}" ...
Everything is done on the TEST servers : patching, reboot, file containing packages version transferred to the PROD servers.
Let’s see how to patch the PROD servers…
Task – prod.yml
First of all we need to ensure that the file “packages-set.txt” is still present on the servers :
--- - name: Check if the packages list file (/etc/dbi/packages-set.txt) is present on PROD servers ansible.builtin.stat: path: /etc/dbi/packages-set.txt register: reg_file_status
As the goal is to use it for the patching, we want the playbook to fail if it is not there :
- name: Fail when file /etc/dbi/packages-set.txt does not exist ansible.builtin.fail: msg: /etc/dbi/packages-set.txt does not exist when: reg_file_status.stat.exists == false
And now… the (easy) trick :
- name: Upgrade PROD servers to the same packages version as TEST servers become: true shell: "cat /etc/dbi/packages-set.txt | xargs yum update-to -y"
- name: Check if a reboot is required become: true command: needs-restarting -r register: reg_reboot_required ignore_errors: true failed_when: false changed_when: reg_reboot_required.rc != 0 notify: - Reboot server
Run the playbook on the TEST servers
To patch the TEST servers, we only need to execute the playbook with the extra-var “ev_env_to_patch=test” :
$ ansible-playbook playbooks/os_update.yml -e "ev_env_to_patch=test" PLAY [Patch TEST or PROD servers] ******************************************************************************************************************************************************************************* TASK [Gathering Facts] *********************************************************************************************************************************************************************************************************************** ok: [35.156.114.202] ok: [3.67.8.87] ok: [54.93.245.3] ok: [54.93.103.197] TASK [os_update : include_tasks] ************************************************************************************************************************************************************************************************************* skipping: [54.93.245.3] skipping: [3.67.8.87] included: /ansible/roles/os_update/tasks/test.yml for 35.156.114.202, 54.93.103.197 TASK [os_update : Get packages that can be upgraded on TEST servers] ************************************************************************************************************************************************************************* ok: [54.93.103.197] ok: [35.156.114.202] TASK [os_update : List packages that will be upgraded on TEST servers] *********************************************************************************************************************************************************************** ok: [54.93.103.197] => { "changed": false, "msg": [ "bash", "bind-export-libs", "bind-libs-lite", "bind-license", "binutils", "btrfs-progs", "ca-certificates", "cloud-init", "cronie-anacron", "cronie", "curl", "cyrus-sasl-lib", "device-mapper-libs", "device-mapper", "dhclient", "dhcp-common", "dhcp-libs", "dmidecode", "firewalld-filesystem", "gettext-libs", "gettext", "glib2", "glibc-common", "glibc", "grub2-common", "grub2-pc-modules", "grub2-pc", "grub2-tools-extra", "grub2-tools-minimal", "grub2-tools", "grub2", "iproute", "kbd-legacy", "kbd-misc", "kbd", "kernel-tools-libs", "kernel-tools", "kpartx", "krb5-libs", "libblkid", "libcurl", "libgudev1", "libmount", "libsmartcols", "libuuid", "libwebp", "libxml2-python", "libxml2", "linux-firmware", "nspr", "nss-softokn-freebl", "nss-softokn", "nss-sysinit", "nss-tools", "nss-util", "nss", "openldap", "openssh-clients", "openssh-server", "openssh", "openssl-libs", "openssl", "oraclelinux-release-el7", "pciutils-libs", "pciutils", "python-firewall", "python-perf", "rpm-build-libs", "rpm-libs", "rpm-python", "rpm", "rsyslog", "selinux-policy-targeted", "selinux-policy", "sudo", "systemd-libs", "systemd-sysv", "systemd", "tzdata", "util-linux", "virt-what" ] } ok: [35.156.114.202] => { "changed": false, "msg": [ "bash", "bind-export-libs", "bind-libs-lite", "bind-license", "binutils", "btrfs-progs", "ca-certificates", "cloud-init", "cronie-anacron", "cronie", "curl", "cyrus-sasl-lib", "device-mapper-libs", "device-mapper", "dhclient", "dhcp-common", "dhcp-libs", "dmidecode", "firewalld-filesystem", "gettext-libs", "gettext", "glib2", "glibc-common", "glibc", "grub2-common", "grub2-pc-modules", "grub2-pc", "grub2-tools-extra", "grub2-tools-minimal", "grub2-tools", "grub2", "iproute", "kbd-legacy", "kbd-misc", "kbd", "kernel-tools-libs", "kernel-tools", "kpartx", "krb5-libs", "libblkid", "libcurl", "libgudev1", "libmount", "libsmartcols", "libuuid", "libwebp", "libxml2-python", "libxml2", "linux-firmware", "nspr", "nss-softokn-freebl", "nss-softokn", "nss-sysinit", "nss-tools", "nss-util", "nss", "openldap", "openssh-clients", "openssh-server", "openssh", "openssl-libs", "openssl", "oraclelinux-release-el7", "pciutils-libs", "pciutils", "python-firewall", "python-perf", "rpm-build-libs", "rpm-libs", "rpm-python", "rpm", "rsyslog", "selinux-policy-targeted", "selinux-policy", "sudo", "systemd-libs", "systemd-sysv", "systemd", "tzdata", "util-linux", "virt-what" ] } TASK [os_update : Request user confirmation] ************************************************************************************************************************************************************************************************* [os_update : Request user confirmation] The packages listed above will be upgraded. Do you want to continue ? -> Press RETURN to continue. -> Press Ctrl+c and then "a" to abort. : ok: [54.93.103.197] TASK [os_update : Upgrade packages on TEST servers] ****************************************************************************************************************************************************************************************** changed: [35.156.114.202] changed: [54.93.103.197] TASK [os_update : Print errors if upgrade failed] ******************************************************************************************************************************************************************************************** skipping: [54.93.103.197] skipping: [35.156.114.202] TASK [os_update : Install yum-utils on TEST servers] ***************************************************************************************************************************************************************************************** ok: [54.93.103.197] ok: [35.156.114.202] TASK [os_update : Check if a reboot is required] ********************************************************************************************************************************************************************************************* changed: [54.93.103.197] changed: [35.156.114.202] TASK [os_update : Genereate the list of the upgraded packages on TEST servers] *************************************************************************************************************************************************************** changed: [35.156.114.202] changed: [54.93.103.197] TASK [os_update : Delete the fake RPM packages gpg-pubkey from the list] ********************************************************************************************************************************************************************* changed: [35.156.114.202] changed: [54.93.103.197] TASK [os_update : Fetch the file from the Ansible Control Node] ********************************************************************************************************************************************************** changed: [35.156.114.202] changed: [54.93.103.197] TASK [os_update : Create directory to store the file on PROD servers] ************************************************************************************************************************************************************************ ok: [54.93.103.197 -> 54.93.245.3] => (item=54.93.245.3) ok: [35.156.114.202 -> 54.93.245.3] => (item=54.93.245.3) ok: [35.156.114.202 -> 3.67.8.87] => (item=3.67.8.87) ok: [54.93.103.197 -> 3.67.8.87] => (item=3.67.8.87) TASK [os_update : Copy the file from the Ansible Control node to the PROD servers] *********************************************************************************************************************************************************** ok: [54.93.103.197 -> 54.93.245.3] => (item=54.93.245.3) changed: [35.156.114.202 -> 54.93.245.3] => (item=54.93.245.3) ok: [35.156.114.202 -> 3.67.8.87] => (item=3.67.8.87) changed: [54.93.103.197 -> 3.67.8.87] => (item=3.67.8.87) TASK [os_update : include_tasks] ************************************************************************************************************************************************************************************************************* skipping: [54.93.103.197] skipping: [35.156.114.202] skipping: [54.93.245.3] skipping: [3.67.8.87] RUNNING HANDLER [os_update : Reboot server] ************************************************************************************************************************************************************************************************** changed: [54.93.103.197] changed: [35.156.114.202] PLAY RECAP *********************************************************************************************************************************************************************************************************************************** 3.67.8.87 : ok=1 changed=0 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0 35.156.114.202 : ok=13 changed=7 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0 54.93.103.197 : ok=14 changed=7 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0 54.93.245.3 : ok=1 changed=0 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0 $
Run the playbook on the PROD servers
To patch the PROD servers, we only need to execute the playbook with the extra-var “ev_env_to_patch=prod” :
$ ansible-playbook playbooks/os_update.yml -e "ev_env_to_patch=prod" PLAY [Patch TEST or PROD servers] ******************************************************************************************************************************************************************************* TASK [Gathering Facts] *********************************************************************************************************************************************************************************************************************** ok: [54.93.245.3] ok: [3.67.8.87] ok: [35.156.114.202] ok: [54.93.103.197] TASK [os_update : include_tasks] ************************************************************************************************************************************************************************************************************* skipping: [35.156.114.202] skipping: [54.93.103.197] skipping: [54.93.245.3] skipping: [3.67.8.87] TASK [os_update : include_tasks] ************************************************************************************************************************************************************************************************************* skipping: [54.93.103.197] skipping: [35.156.114.202] included: /ansible/roles/os_update/tasks/prod.yml for 54.93.245.3, 3.67.8.87 TASK [os_update : Check if the packages list file (/etc/dbi/packages-set.txt) is present on PROD servers] ************************************************************************************************************************************ ok: [3.67.8.87] ok: [54.93.245.3] TASK [os_update : Fail when file /etc/dbi/packages-set.txt does not exist] ******************************************************************************************************************************************************************* skipping: [54.93.245.3] skipping: [3.67.8.87] TASK [os_update : Upgrade PROD servers to the same packages version as TEST servers] ********************************************************************************************************************************************************* changed: [54.93.245.3] changed: [3.67.8.87] TASK [os_update : Check if a reboot is required] ********************************************************************************************************************************************************************************************* changed: [54.93.245.3] changed: [3.67.8.87] RUNNING HANDLER [os_update : Reboot server] ************************************************************************************************************************************************************************************************** changed: [3.67.8.87] changed: [54.93.245.3] PLAY RECAP *********************************************************************************************************************************************************************************************************************************** 3.67.8.87 : ok=6 changed=3 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0 35.156.114.202 : ok=1 changed=0 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0 54.93.103.197 : ok=1 changed=0 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0 54.93.245.3 : ok=6 changed=3 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0 $
Easy, isn’t ? Happy patching !
gdeleon23
10.07.2023Hi!!
Fantastic blog, congratulations!
Only one question, you dont have control of the package versión or i am missing something?
Thank you!