I recently wanted to test Ansible playbooks locally, but since I am on a Windows environment, this proved a little difficult. It is possible to install Ansible in WSL, but I found that this did not quite fit my need, since I wanted a control node where I can tweak and run the Ansible scripts from, but also needed some target machines to apply the changes to. This gave me the idea to use another tool I am very fond of, Vagrant, to create several Linux machines to create my Ansible development environment.
Ansible is a popular IT automation framework, allowing the creation of scripts to automate the installation of software and packages, lifecycle management such as patching and upgrades and other complex tasks.
Vagrant is a tool for creation and management of virtual machines, also using scripts to automate the provisioning process.
Prerequisites
- Virtual Box (or similar Hypervisor).
- Vagrant.
- Visual Studio Code (or another text editor).
Getting Started
- Download and install Visual Studio Code
- Download and install Oracle Virtual Box (or your preferred Hypervisor). Please note, if you are already using Hyper-V, you can run Virtual Box side-by-side, as long as you have enabled the Windows Hypervisor Platform feature.
- Download and install Vagrant from the official Downloads page, or you can use your favourite package manager.
- Make sure Vagrant was successfully installed by running the following from the command-line.
vagrant --version
Next we will look at the creation of a Vagrantfile
to start our Virtual Machine automation.
Vagrantfile
- In your working directory, create a new file and name it
Vagrantfile
. Open this file in your text editor of choice, I will be using Visual Studio Code.
- Each time we provision a new Virtual Machine, we do not want to perform a fresh installation of the operating system, as this is too time consuming. Vagrant has the concept of a box, which is a base image to start provisioning from. You can create your own boxes using a tool like Packer, but Vagrant also provides a catalogue of publicly available boxes.
- Firstly, we need to tell Vagrant which version of configuration to use:
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure("2") do |config|
# Content goes here
end
- We can add the instruction on which box to use. I have selected the Centos Stream 8 image available on the Vagrant Box catalogue.
config.vm.box = "centos/stream8"
- Configure the provider, in this case Virtual Box, with the required settings for our virtual machine(s).
config.vm.provider :virtualbox do |v|
v.memory = 1024
v.cpus = 1
v.linked_clone = true
end
- It is possible to instruct Vagrant to repeat the instruction multiple times, to create more than one virtual machine.
# Define three VMs with static private IP addresses.
boxes = [
{ :name => "ansible-1", :ip => "192.168.33.71" },
{ :name => "node1", :ip => "192.168.33.72" },
{ :name => "node2", :ip => "192.168.33.73" }
]
# Provision each of the VMs.
boxes.each do |bx|
config.vm.define bx[:name] do |config|
config.vm.hostname = bx[:name]
config.vm.network :private_network, ip: bx[:ip]
end
end
- Vagrant generates a unique SSH key on the host for communication with each virtual machine. You can instruct Vagrant to use the same SSH key for each VM.
config.ssh.insert_key = false
- We can use the provision instruction to copy files and run scripts.
- We add a condition for the machine named
ansible-1
, which will function as the Ansible control node, to allow SSH communication to the other nodes by reusing the SSH key generated by Vagrant on the host machine.
- Create a script to perform
dnf update
and install the Ansible components, requiring sudo
permissions.
- Use pip to install
ansible-navigator
.
$script = <<-'SCRIPT'
dnf -y update
dnf -y install python3 python3-pip podman ansible-core
SCRIPT
$script2 = <<-'SCRIPT'
python3 -m pip install ansible-navigator --user
SCRIPT
if bx[:name] == "ansible-1"
config.vm.provision "file", source: "~/.vagrant.d/insecure_private_key", destination: "~/.ssh/id_rsa"
config.vm.provision :shell, inline: "chmod 600 ~/.ssh/id_rsa", privileged: false
config.vm.provision :shell, inline: $script, privileged: true
config.vm.provision :shell, inline: $script2, privileged: false
end
- For configuration of ansible-navigator, we create a
.ansible-navigator.yml
settings file in the working directory which contains the Vagrantfile
. The settings file must be added to the home directory of the Ansible control node. This identifies an inventory file, with some required Ansible configuration.
- Finally we create a
hosts
inventory file, also in the working directory, with basic variables matching our Vagrantfile
, and default SSH configuration.
- Putting it all together, the working directory should contain:
.ansible-navigator.yml
---
ansible-navigator:
ansible:
inventories:
- /home/vagrant/hosts
hosts
[all:vars]
ansible_user=vagrant
ansible_port=22
[control]
ansible-1 ansible_host=192.168.33.71
[web]
node1 ansible_host=192.168.33.72
node2 ansible_host=192.168.33.73
Vagrantfile
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure("2") do |config|
config.vm.box = "centos/stream8"
# Do not generate a new SSH key for each VM
config.ssh.insert_key = false
config.vm.provider :virtualbox do |v|
v.memory = 1024
v.cpus = 1
v.linked_clone = true
end
# Define three VMs with static private IP addresses.
boxes = [
{ :name => "ansible-1", :ip => "192.168.33.71" },
{ :name => "node1", :ip => "192.168.33.72" },
{ :name => "node2", :ip => "192.168.33.73" }
]
# Install ansible & dependencies
$script = <<-'SCRIPT'
dnf -y update
dnf -y install python3 python3-pip podman ansible-core
SCRIPT
# Install ansible-navigator
$script2 = <<-'SCRIPT'
python3 -m pip install ansible-navigator --user
SCRIPT
# Provision each of the VMs.
boxes.each do |bx|
config.vm.define bx[:name] do |config|
config.vm.hostname = bx[:name]
config.vm.network :private_network, ip: bx[:ip]
# Copy SSH key to primary node and run scripts
if bx[:name] == "ansible-1"
config.vm.provision "file", source: "~/.vagrant.d/insecure_private_key", destination: "~/.ssh/id_rsa"
config.vm.provision :shell, inline: "chmod 600 ~/.ssh/id_rsa", privileged: false
config.vm.provision :shell, inline: $script, privileged: true
config.vm.provision "file", source: "./.ansible-navigator.yml", destination: "~/.ansible-navigator.yml"
config.vm.provision :shell, inline: $script2, privileged: false
config.vm.provision "file", source: "./hosts", destination: "~/hosts"
end
end
end
end
- These three files are also available on GitHub.
- From an Administrative command-line, we can now instruct Vagrant to create and provision the three CentOS virtual machines:
vagrant up
- Once completed, you can verify access connecting to the virtual machines using SSH.
vagrant ssh ansible-1
If you are using Visual Studio Code, install the Remote - SSH extension. This will allow you to develop against the remote Linux environment from the host machine. Add a new SSH target in the Remote Explorer for ansible-1
with the IP address specified in the Vagrantfile
, which is 192.168.33.71
. Also specify the identity_file
, named insecure_private_key
, which is located at C:\Users\<your username>\.vagrant.d\
. The default user for Vagrant boxes is (almost) always vagrant
.
ssh -i c:\Users\<your username>\.vagrant.d\insecure_private_key vagrant@192.168.33.71
If the SSH connection is successful, VS Code Server will be downloaded and installed on the target machine, allowing you to use your local VS Code to work against the remote instance. Click on File > Open Folder
and select /home/vagrant
. You will see the hosts
file and .ansible-navigator.yml
file we copied during the Vagrant provisioning in the directory.
- After connecting to the
ansible-1
via SSH, create a new folder learn-ansible
.
- Create a file and call it
install-nginx.yml
, this will be our Ansible playbook. A playbook is nothing more than a YML file consisting of repeatable plays, each with a set of tasks that is run from top to bottom.
mkdir learn-ansible
cd learn-ansible/
touch install-nginx.yml
- In the new file we'll create our first play.
- We give the play a
name
, specify which hosts
it will apply to (specifying node1 which we defined in the hosts
file) and use the become
instruction to tell Ansible that this will require administrative privileges.
---
- name: nginx is installed
hosts: node1
become: yes
- A play will contain one or more tasks to execute.
- First, let's add the task to install the NGINX web server. We
name
the task, and instruct ansible to use the dnf
module to install the nginx
package.
---
- name: nginx is installed
hosts: node1
become: yes
tasks:
- name: latest nginx version is installed
dnf:
name: nginx
state: latest
- We also want to make sure the NGINX service is started after installation.
- We add a second task, using the
service
module to make sure that NGINX is enabled
and started
.
---
- name: nginx is installed
hosts: node1
become: yes
tasks:
- name: latest nginx version is installed
dnf:
name: nginx
state: latest
- name: nginx is started
service:
name: nginx
enabled: true
state: started
- We can now use the command-line tool,
ansible-navigator
, to apply the playbook. Ansible Navigator is a text-based user interface to help with the creation and management of Ansible content.
- Ansible Navigator will use the
.ansible-navigator.yml
file to detect settings, in which we have defined the hosts
file with our SSH configuration and virtual machine inventory.
- Apply the playbook.
ansible-navigator run install-nginx.yml
- You can interact with Ansible Navigator to drill down into tasks. Notice that shows that 2 changes where made.
- Once the playbook has finished running, you can access
node1
on 192.168.33.72
in your browser, which will show the default NGINX web server landing page.
- Because we specified
node1
in hosts
, NGINX was not installed on node2
. Change the value from node1
to web
in the playbook, to apply it to all servers specified in the hosts
file under the web
group.
- Run the playbook again to apply the changes to
node2
. You can use the --mode
parameter to use stdout
instead of the default interactive
output.
ansible-navigator run install-nginx.yml --mode stdout
- The output is printed to the console instead. Note that
node1
was not changed since it already had NGINX installed.
You can destroy the Vagrant machines with a simple command, which will stop and delete all the virtual machines defined in the Vagrantfile
.
vagrant destroy -f
In this blog post we created three Linux virtual machines by using the virtual machine automation tool Vagrant. We also installed ansible and ansible-navigator. We created a simple Ansible playbook using the Visual Studio Code Remote SSH extension. We used ansible-navigator to apply the playbook, installing the NGINX web server on our target nodes. This is only a small taste of what you can do with Ansible, start learning more today!
Also check out how you can use Ansible on IBM Cloud.
If you are using Vagrant on Linux, there is also an Ansible Provisioner available to trigger your playbooks directly from vagrant. Read more in the Vagrant Guide from Ansible.