Tutorials
Configuration with Ansible¶
Use Ansible to launch a Linux and a Windows instance with the openstack.cloud collection, find them with a dynamic inventory, and configure a web server on each: over SSH on Linux, and over WinRM on Windows.
Well-Architected
Pillar: Operational excellence. Configuration lives in playbooks you can rerun, review and keep in version control, so every instance is set up the same way.
Also touches: Security and sovereignty (encrypted WinRM, a vaulted password, access limited to your IP) and Reliability (instances you can rebuild and reconfigure from code).
What you'll build¶
| Resource | Name | Details |
|---|---|---|
| Key pair | ansible-key |
Your SSH public key, for Linux |
| Security group | ansible-access |
SSH (22), WinRM over HTTPS (5986) and HTTP (80) from your IP only |
| Linux instance | ansible-web-01 |
Ubuntu, v1.c1r2, configured with nginx |
| Windows instance | ansible-win-01 |
Windows Server, v1.c2r8, configured with IIS |
Four playbooks and files do the work:
| File | What it does |
|---|---|
provision.yml |
Creates the key pair, security group and both instances |
openstack.yml |
A dynamic inventory: asks Breqwatr Cloud which instances exist |
configure.yml |
Installs and configures the web servers |
destroy.yml |
Removes everything provision.yml created |
Before you start¶
- Completed: Launch your first instances. This tutorial uses the Windows console password reset from 4.1.
- Quota for 2 instances, 3 vCPUs, 10 GB of memory, 100 GB of storage and 2 floating IPs.
- Time: about 60 minutes.
- The code: every playbook and file in this tutorial is also in
ansible-configuration/in learn-projects, the course's code repository, at releasev1.2.0.
Ansible runs on macOS and Linux. On Windows, it runs inside WSL, Microsoft's Linux environment for Windows, and you follow the Linux steps there.
Step 1: Install Ansible¶
Ansible goes into the virtual environment from Install the OpenStack CLI, next to the CLI. The ansible package includes the openstack.cloud, ansible.windows and community.windows collections; pywinrm lets it talk to Windows over WinRM.
Ansible doesn't yet run on Python 3.15, which is why Install the OpenStack CLI uses Python 3.14. If ansible --version fails with an error about dataclasses, the environment was made with a newer Python: recreate it with Python 3.14.
- Install WSL with Ubuntu, as in Microsoft's Install WSL, and open the Ubuntu terminal.
- Inside Ubuntu, follow the Linux steps in Install the OpenStack CLI, and put your
clouds.yamlin~/.config/openstack/inside Ubuntu. -
With the environment active, install Ansible:
Create a folder called ansible-configuration for the files below, and an SSH key for this tutorial in it:
Step 2: Set your values¶
vars.yml holds every value that's yours. Use openstack image list and openstack network list --external for the names.
cloud: <cloud-name>
prefix: ansible
my_ip: "<your-ip>/32"
network: default
external_network: <external-network>
linux_image: <linux-image>
windows_image: <windows-image>
public_key_file: "{{ playbook_dir }}/ansible-key.pub"
Step 3: Write the provisioning playbook¶
Windows needs a WinRM listener on HTTPS, which Breqwatr's Windows images don't set up on their own. First-boot user data creates one with a self-signed certificate and opens port 5986 in the Windows firewall. Save it as winrm-https.ps1:
#ps1_sysnative
$cert = New-SelfSignedCertificate -DnsName $env:COMPUTERNAME -CertStoreLocation Cert:\LocalMachine\My
New-Item -Path WSMan:\localhost\Listener -Transport HTTPS -Address * -CertificateThumbPrint $cert.Thumbprint -Force
New-NetFirewallRule -DisplayName "WinRM over HTTPS" -Direction Inbound -Protocol TCP -LocalPort 5986 -Action Allow
provision.yml creates everything with the openstack.cloud collection. Each instance boots from a volume deleted with it (terminate_volume), gets a floating IP, and carries os metadata that the inventory uses to group it.
- name: Provision instances on Breqwatr Cloud
hosts: localhost
gather_facts: false
vars_files:
- vars.yml
tasks:
- name: Add the SSH public key
openstack.cloud.keypair:
cloud: "{{ cloud }}"
name: "{{ prefix }}-key"
public_key: "{{ lookup('file', public_key_file) }}"
- name: Create the security group
openstack.cloud.security_group:
cloud: "{{ cloud }}"
name: "{{ prefix }}-access"
- name: Allow SSH, WinRM over HTTPS and HTTP from your IP address
openstack.cloud.security_group_rule:
cloud: "{{ cloud }}"
security_group: "{{ prefix }}-access"
protocol: tcp
port_range_min: "{{ item }}"
port_range_max: "{{ item }}"
remote_ip_prefix: "{{ my_ip }}"
loop: [22, 5986, 80]
- name: Launch the Linux instance
openstack.cloud.server:
cloud: "{{ cloud }}"
name: "{{ prefix }}-web-01"
image: "{{ linux_image }}"
flavor: v1.c1r2
boot_from_volume: true
volume_size: 20
terminate_volume: true
key_name: "{{ prefix }}-key"
network: "{{ network }}"
security_groups: [default, "{{ prefix }}-access"]
floating_ip_pools: ["{{ external_network }}"]
metadata:
os: linux
wait: true
timeout: 900
- name: Launch the Windows instance
openstack.cloud.server:
cloud: "{{ cloud }}"
name: "{{ prefix }}-win-01"
image: "{{ windows_image }}"
flavor: v1.c2r8
boot_from_volume: true
volume_size: 80
terminate_volume: true
network: "{{ network }}"
security_groups: [default, "{{ prefix }}-access"]
floating_ip_pools: ["{{ external_network }}"]
userdata: "{{ lookup('file', 'winrm-https.ps1') }}"
metadata:
os: windows
wait: true
timeout: 900
Run it:
The Windows instance takes several minutes to build, which is why both tasks wait up to 900 seconds. The playbook is safe to run again: it leaves alone what already exists.
Step 4: Find the instances¶
openstack.yml is an inventory that asks Breqwatr Cloud for the instances in your project and groups them by their os metadata, as os_linux and os_windows. Each host's address is its floating IP.
plugin: openstack.cloud.openstack
only_clouds:
- <cloud-name>
expand_hostvars: false
fail_on_errors: true
keyed_groups:
- key: openstack.metadata.os
prefix: os
The graph shows ansible-web-01 under @os_linux and ansible-win-01 under @os_windows. It also lists any other instances in the project; the playbooks below only touch the two os_ groups.
Step 5: Tell Ansible how to connect¶
Connection settings for each group go in group_vars.
group_vars/os_linux/vars.yml connects over SSH with your key:
ansible_user: ubuntu
ansible_ssh_private_key_file: "{{ playbook_dir }}/ansible-key"
ansible_ssh_common_args: "-o StrictHostKeyChecking=accept-new"
group_vars/os_windows/vars.yml connects over WinRM on HTTPS. The certificate is self-signed, so Ansible doesn't validate it; the connection is still encrypted.
ansible_connection: winrm
ansible_port: 5986
ansible_winrm_scheme: https
ansible_winrm_transport: ntlm
ansible_winrm_server_cert_validation: ignore
ansible_user: Administrator
ansible_password: "{{ vault_windows_password }}"
Windows needs its Administrator password set before Ansible can sign in. Set it on the console as in 4.1: open the console (openstack --os-cloud <cloud-name> console url show ansible-win-01), choose Send CtrlAltDel, and set a new password when asked. Then store it, encrypted, with Ansible Vault:
Choose a vault password when asked. In the editor that opens, add one line with the Administrator password, then save:
Check that Ansible can reach both instances:
ansible -i openstack.yml os_linux -m ping
ansible -i openstack.yml os_windows -m ansible.windows.win_ping --ask-vault-pass
Each instance answers pong.
Step 6: Configure the web servers¶
configure.yml installs nginx on Linux and IIS on Windows, publishes a page on each, and opens HTTP in the Windows firewall:
- name: Configure Linux web servers
hosts: os_linux
become: true
tasks:
- name: Install nginx
ansible.builtin.apt:
name: nginx
update_cache: true
- name: Publish a page
ansible.builtin.copy:
content: "Configured by Ansible on {{ inventory_hostname }}\n"
dest: /var/www/html/index.html
- name: Configure Windows web servers
hosts: os_windows
tasks:
- name: Install IIS
ansible.windows.win_feature:
name: Web-Server
include_management_tools: true
- name: Publish a page
ansible.windows.win_copy:
content: "Configured by Ansible on {{ inventory_hostname }}\r\n"
dest: C:\inetpub\wwwroot\index.html
- name: Allow HTTP through the Windows firewall
community.windows.win_firewall_rule:
name: HTTP in
localport: 80
protocol: tcp
direction: in
action: allow
Run it a second time. Every task reports ok instead of changed: Ansible changes only what differs from the playbook.
Check it worked¶
Open each instance's floating IP in a browser, or:
Each returns "Configured by Ansible on" followed by the instance's name.
Clean up¶
destroy.yml removes the instances with their root volumes and floating IPs, then the security group and the key pair:
- name: Remove everything provision.yml created
hosts: localhost
gather_facts: false
vars_files:
- vars.yml
tasks:
- name: Delete the instances, with their root volumes and floating IPs
openstack.cloud.server:
cloud: "{{ cloud }}"
name: "{{ item }}"
state: absent
delete_ips: true
wait: true
loop: ["{{ prefix }}-web-01", "{{ prefix }}-win-01"]
- name: Delete the security group
openstack.cloud.security_group:
cloud: "{{ cloud }}"
name: "{{ prefix }}-access"
state: absent
- name: Delete the key pair
openstack.cloud.keypair:
cloud: "{{ cloud }}"
name: "{{ prefix }}-key"
state: absent
If something goes wrong¶
| Message | What to check |
|---|---|
ansible --version fails with an error about dataclasses |
Your Python is newer than Ansible supports. Recreate the environment with Python 3.14 |
| "A worker was found in a dead state" on macOS | Run export no_proxy='*' in the terminal, then rerun the command. It stops Python looking up macOS proxy settings in Ansible's worker processes |
| "Timeout waiting for the server to come up" | The Windows build took longer than timeout. Wait until openstack server list shows it ACTIVE, then rerun provision.yml |
| WinRM: "the specified credentials were rejected" | The vault holds a different password from the one set on the console, or the password hasn't been set yet |
| SSH or WinRM can't connect | Check your IP address in vars.yml is still current, and that the Windows first boot has finished |
What you learned¶
- The
openstack.cloudcollection builds resources through the same APIs as the CLI and OpenTofu; see GUI, API and infrastructure as code. - A dynamic inventory finds instances by what they are, not by hard-coded addresses.
- One playbook configures Linux over SSH and Windows over WinRM, and running it again changes nothing that's already right.
- Secrets such as the Windows password go in Ansible Vault, not in plain text; see Security and sovereignty.
Next¶
- A three-tier web app: build a whole application from code, behind a load balancer.
- openstack.cloud collection: every module and the inventory plugin.
- Windows remote management: WinRM options in Ansible.