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.