Managing Software Versions on Windows with Ansible
Recently I’ve been working on automating a huge Windows infrastructure with Ansible to reduce the workload on our team during deploys and upgrades. We need to quickly deploy multiple versions of our supporting software, some only available in ZIP format, without resorting to manual configuration.
First, a few caveats. I know that for a lot of Windows software an EXE or MSI
installer with win_package
would be preferable to this method. If you can
make win_package
behave, more power to you and I suggest you use it over this
method whenever possible. Personally, I’ve had nothing but trouble getting my
WinRM session to pass on the correct set of permissions to actually get a MSI to
install, and necessity is the mother of invention. Second, this method is really
handy for software that is only available in ZIP format. It will allow you to
install to standard and predictable paths without the risk of “forgetting” what
version is installed. However, it does make you entirely reliant on nobody
removing the flag file you will be creating. With that said, let’s get to making
things work.
To start we’re going to need some default values for our role to target. Work has had me working with Tomcat so we’ll be using that as our example.
defaults/main.yml
---
tomcat_root_dir: C:\Tomcat
tomcat_version: 8.0.35
tomcat_version_file_path: '{{ tomcat_root_dir }}\tomcat-{{ tomcat_version }}.version'
tomcat_old_version_file_path: '{{ tomcat_root_dir }}\*.version'
By specifying the version number in our flag file’s name we have a simple way to
check the installed version later with the win_stat
module. We’re also going
define a really simple regex to clean up any old version files after our install
has finished successfully.
Jumping into our main task file we’re going to abuse Ansible’s registered
variables when we check for the current .version
file.
tasks/main.yml
---
- name: Check for the Tomcat version.
win_stat:
path: '{{ tomcat_version_file_path }}'
register: tomcat_version_file
The advantage of this method is that it allows us to trigger certain tasks
only when changing versions, like unzipping the new package, versus every
time Ansible runs, like writing out configuration files. Depending on how
complex the installation is this could be when
clauses applied to individual
tasks or to an entire playbook with include_tasks
. One thing to keep an eye
out for is that the exists
boolean is not a child element of your registered
variable but is under stat
instead.
- name: Stop the Tomcat service.
when: not tomcat_version_file.stat.exists
win_service:
name: '{{ tomcat_service_name }}'
state: stopped
- name: Run the Tomcat install.
when: not tomcat_version_file.stat.exists
include_tasks: install.yml
The last step is to clean up any old version files when we finish an update.
So far I haven’t been able to get win_file
to work with a regex pattern to
remove the old files so I’ve resorted to using PowerShell directly with the
win_shell
module. The trick is to create the new version file as the very last
step of the role. This way you are sure that all your tasks have run successfully
and the version file acts like a database transaction. If anywhere along the way
the role stalls or fails you can re-run the role and it stays idempotent.
- name: Remove old Tomcat version files.
when: not tomcat_version_file.stat.exists
win_shell: >
Remove-Item {{ tomcat_old_version_file_path }}
- name: Create new Tomcat version file.
win_file:
path: '{{ tomcat_version_file_path }}'
state: touch
There we have it! Ansible support for Windows is still a little shaky but with a little creativity we can make it a useful tool for managing your Microsoft infrastructure.