Provisioning Windows Machines via Ansible

While I'm not new to Configuration Management (CM), necessarily, I will admit that I'm slightly outdated (hey, things change fast in the tech world, okay?). Prior to this, my exposure to CM has been limited to Chef or Puppet.

Recently, I was tasked with vetting out the various CM tools out there, and began weighing the pros and cons of each. As such, I was introduced to Ansible.

Ansible wasn't entirely new to me. I had heard the name previously, but was under the false impression that it was still under-developed, had a small userbase, and immature syntax.

I've never been more wrong.

Ansible now blows both Chef and Puppet out of the water. As far as sheer userbase, contributors, and stars go on Github, Ansible has more than 5x that of either Puppet or Chef. Needless to say, I've switched.

Getting Started

I began following this (https://medium.com/the-sysadmin/managing-windows-machines-with-ansible-60395445069f) guide in order to get up and going. While it's mostly correct, I found there were a few additional configurations that I stumbled through, which would have been helpful to know up front. I'm going to take from this guide, and add to it.

My instructions will teach you how to manage machines remotely using Basic Authentication (local Windows accounts). If you're looking to use an AD account, you'll need to look elsewhere on how to set up Kerberos authentication.

Ansible has supported Windows machines for some time now. The server itself still requires a Linux/Unix box, but people have discovered ways to jimmy-rig it to work on a Windows machine via Cygwin or the now-native Bash support in Windows 10. I opted to stick with the fully-supported Linux system. My Ansible server is running Ubuntu 17.10.

Prepping your Windows machine

In order for Ansible to be able to communicate with your Windows boxes, WinRM (Remote Management) needs to be enabled and configured. Lucky for us, the Ansible team has provided a quick and easy way to do that.

Open a command prompt as an Administrator, and run the following command:

@powershell -NoProfile -ExecutionPolicy Bypass -Command "iex ((new-object net.webclient).DownloadString('https://github.com/ansible/ansible/raw/devel/examples/scripts/ConfigureRemotingForAnsible.ps1'))"

Ansible Server

Again, remember that Linux is the only fully-supported option currently for Ansible Server. There are hacks to get it up and running on Windows, but I won't be going over that.

On your Linux box, create the following folder structure. This can be created anywhere on the machine. I opted to place it in my "home" directory. Note the "hosts" is a file (without an extension), not a folder.

windows/  
├── group_vars/
│   └── windows.yml
└── hosts

Within the hosts file, type the following:

# file: hosts
[windows]
192.168.198.129  
192.168.198.130  
192.168.198.131  
...

Obviously, you'll need to replace these addresses with the specific IPs for your Windows machines.

Under the group_vars directory, within the windows.yml file, enter the following information:

ansible_user: ansible  
ansible_password: Bertha123!  
ansible_port: 5985  
anssible_connection: winrm  
ansible_winrm_transport: basic  
ansible_winrm_server_cert_validation: ignore  

Feel free to change the username and password to your liking. Then, set up this user (using the same password) as a local administrator on each of your Windows servers.

Linux Ping

I ran into an issue where my Windows machines couldn't ping the Ansible server. I ran the following command on each machine for them to become visible:

netsh firewall set icmpsetting 8 enable

Final WinRM Configuration

At this point, many guides leave you and say, "That's it! You're all set!" I didn't have such luck. I still was unable to manage any of my Windows machines from Ansible and kept receiving an error.

WinRM by default doesn't allow basic (read: username and password) authentication. Run the following commands:

winrm quickconfig. Type "y" and "enter" following the prompt.

Set-Item -Path WSMan:\localhost\Service\Auth\Basic -Value $true

Set-Item -Path WSMan:\localhost\Service\AllowUnencrypted -Value $true

This probably goes without saying, but the above commands are fairly big security flaws. It's a much better practice to set up Certificate-based Authentication or Kerberos over HTTPS port 5986. For my purposes, all I cared about was getting it running so I could initially test my Playbooks. After that, I went back and tightened things up.

All set!

At this point, you're ready to go and your Windows machines should now be able to communicate with the Ansible server via WinRM.

To test, run the following from Ansible server:

ansible windows -i hosts -m win_ping

This command runs a ping command against all of the servers listed under the "windows" group within your hosts file. Additional groups can be set up here for more granular control over servers (i.e. a "databases" group, or a "web servers" group)

If you receive something similar to the following, in green letters, you're golden:

192.168.1.10 | SUCCESS => {  
    "changed": false,
    "ping": "pong"
}

Running a playbook

The above command simply runs a single task against all of your servers. What if you want to run a more advanced series of commands (i.e. a playbook) against the servers? You'll use this command:

ansible-playbook windows -i hosts playbook-iis-setup.yml

Below is a sample playbook I've used in the past, that provisions a webserver running IIS. It installs the IIS Windows Server role, creates a directory structure, copies down latest code, and changes firewall ports. It's pretty slick.

# file: playbook-iis-setup.yml
---
- hosts: windows
  vars:
    ansible_site_path: "c:\\inetpub\\wwwroot\\ansibletest"
    staging_path: "c:\\deploy"
    ansible_test_staging_path: "{{ staging_path }}\\ansible-test-site-{{ ansible_date_time.year }}{{ ansible_date_time.month }}{{ ansible_date_time.day }}"
  tasks:
  - name: install-iis
    win_feature:
      name: "Web-Server"
      state: present
      restart: no
      include_sub_features: yes
      include_management_tools: no
  - name: create staging path
    win_file: path={{ staging_path }} state=directory

  - name: default-website-index
    win_copy:
      src: files/index.html
      dest: "C:\\inetpub\\wwwroot\\index.html"

  - name: create new website's directory
    win_file: path={{ ansible_site_path }} state=directory
  - name: create new website
    win_iis_website:
      name: "Ansible Test Site"
      state: started
      port: 8080
      physical_path: "{{ ansible_site_path }}"
  - name: Open site's port on firewall
    win_firewall_rule:
      name: mysite8080
      enable: yes
      state: present
      localport: 8080
      action: Allow
      direction: In
      protocol: Tcp
      force: true
    tags: firewall

  - name: create deploy staging path
    win_file: path={{ ansible_test_staging_path }} state=directory
  - name: get code to deploy staging path
    win_copy:
      src: files/site.zip
      dest: "{{ ansible_test_staging_path }}"
  - name: unzip code to site path
    win_unzip:
      src: "{{ ansible_test_staging_path }}\\site.zip"
      dest: "{{ ansible_site_path }}"
      creates: "{{ ansible_site_path }}\\index.html"
    tags: unzip

In all, it's pretty straightforward and you should be able to take it from here. I just wanted to correct and share a few mistakes from the previous guide I followed to save you some hair-pulling.