So far, we have used Ansible to execute individual commands on all hosts in the inventory, one by one. Today, we will learn how to use playbooks to orchestrate the command execution. As a side note, we will also learn how to set up a local test environment using Vagrant.
Setting up a local test environment
To develop and debug Ansible playbooks, it can be very useful to have a local test environment. If you have a reasonably powerful machine (RAM will be the bottleneck), this can be easily done by spinning up virtual machines using Vagrant. Essentially, Vagrant is a tool to orchestrate virtual machines, based on configuration files which can be put under version control. We will not go into details on Vagrant in this post, but only describe briefly how the setup works.
First, we need to install Vagrant and VirtualBox. On Ubuntu, this is done using
sudo apt-get install virtualbox vagrant
Next, create a folder vagrant in your home directory, switch to it and create a SSH key pair that we will later use to access our virtual machines.
ssh-keygen -b 2048 -t rsa -f vagrant_key -P ""
This will create two files, the private key vagrant_key and the corresponding public key vagrant_key.pub.
Next, we need a configuration file for the virtual machines we want to bring up. This file is traditionally called Vagrantfile. In our case, the file looks as follows.
Vagrant.configure("2") do |config| config.ssh.private_key_path = ['~/.vagrant.d/insecure_private_key', '~/.keys/ansible_key'] config.vm.provision "file", source: "~/vagrant/vagrant_key.pub", destination: "~/.ssh/authorized_keys" config.vm.define "boxA" do |boxA| boxA.vm.box = "ubuntu/bionic64" boxA.vm.network "private_network", ip: "192.168.33.10" end config.vm.define "boxB" do |boxB| boxB.vm.box = "ubuntu/bionic64" boxB.vm.network "private_network", ip: "192.168.33.11" end end
We will not go into the details, but essentially this file instructs Vagrant to provision two machines, called boxA and boxB (the file, including some comments, can also be downloaded here). Both machines will be connected to a virtual network device and will be reachable from the host and from each other using the IP addresses 192.168.33.10 and 192.168.33.11 (using the VirtualBox networking machinery in the background, which I have described in a bit more detail in this post). On both files, we will install the public key just created, and we ask Vagrant to use the corresponding private key.
Now place this file in the newly created directory ~/vagrant and, from there, run
vagrant up
Vagrant will now start to spin up the two virtual machines. When you run this command for the first time, it needs to download the machine image which will take some time, but once the image is present locally, this usually only takes one or two minutes.
Our first playbook
Without further ado, here is our first playbook that we will use to explain the basic structure of an Ansible playbook.
--- # Our first play. We create an Ansible user on the host - name: Initial setup hosts: all become: yes tasks: - name: Create a user ansible_user on the host user: name: ansible_user state: present - name: Create a directory /home/ansible_user/.ssh file: path: /home/ansible_user/.ssh group: ansible_user owner: ansible_user mode: 0700 state: directory - name: Distribute ssh key copy: src: ~/.keys/ansible_key.pub dest: /home/ansible_user/.ssh/authorized_keys mode: 0700 owner: ansible_user group: ansible_user - name: Add newly created user to sudoers file lineinfile: path: /etc/sudoers state: present line: "ansible_user ALL = NOPASSWD: ALL"
Let us go through this line by line. First, recall from our previous posts that an Ansible playbook is a sequence of plays. Each play in turn refers to a group of hosts in the inventory.
A playbook is written in YAML-syntax that you probably know well if you have followed my series on Kubernetes (if not, you might want to take a look at the summary page on Wikipedia). Being a sequence of plays, it is a list. Our example contains only one play.
The first attribute of our play is called name. This is simply a human-readable name that ideally briefly summarizes what the play is doing. The next attribute hosts refers to a set of hosts in our inventory. In our case, we want to run the play for all nodes in the inventory. More precisely, this is a pattern which can specify individual hosts, groups of hosts or various combinations.
The second attribute is the become flag that we have already seen several times. We need this, as our vagrant user is not root and we need to do a sudo to execute most of our commands.
The next attribute, tasks, is itself a list and contains all tasks that make up the playbook. When Ansible executes a playbook, it will execute the plays in the order in which they are present in the playbook. For each play, it will go through the tasks and, for each task, it will loop through the corresponding hosts and execute this task on each host. So a task will have to be completed for each host before the next task starts. If an error occurs, this host will be removed from the rotation and the execution of the play will continue.
We have learned in an earlier post that a task is basically the execution of a module. Each task usually starts with a name. The next line specifies the module to execute, followed by a list of parameters for this module. Our first task executes the user module, passing the name of the user to be created as an argument, similar to what we did when executing commands ad-hoc. Our second task executes the file module, the third task the copy module and the last task the lineinfile module. If you have followed my previous posts, you will easily recognize what these tasks are doing – we create a new user, distribute the SSH key in ~/.keys/ansible_key.pub and add the user to the sudoers file on each host (of course, we could as well continue to use the vagrant user, but we perform the switch to a different user for the sake of demonstration). To run this example, you will have to create a SSH key pair called ansible_key and store it in the subdirectory .keys of your home directory, as in my last post.
Executing our playbook
How do we actually execute a playbook? The community edition of Ansible contains a command-line tool called ansible-playbook
that accepts one or more playbooks as arguments and executes them. So assuming that you have saved the above playbook in a file called myFirstPlaybook, you can run it as follows.
export ANSIBLE_HOST_KEY_CHECKING=False ansible-playbook -i hosts.ini -u vagrant --private-key ~/vagrant/vagrant_key myFirstPlaybook.yaml
The parameters are quite similar to the parameters of the ansible
command. We use -i to specify the location of an inventory file, -u to specify the user to use for the SSH connections and –private-key to specify the location of the private key file of this user.
When we run this playbook, Ansible will log a summary of the actions to the console. We will see that it starts to work on our play, and then, for each task, executes the respective action once for each host. When the script completes, we can verify that everything worked by using SSH to connect to the machines using our newly created user, for instance
ssh -i ~/.keys/ansible_key -o StrictHostKeyChecking=no ansible_user@192.168.33.10
Instead of executing our playbook manually after Vagrant has completed the setup of the machines, it is also possible to integrate Ansible with Vagrant. In fact, Ansible can be used in Vagrant as a Provisioner, meaning that we can specify an Ansible playbook that Vagrant will execute for each host immediately after is has been provisioned. To see this in action, copy the playbook to the Vagrant directory ~/vagrant and, in the Vagrantfile, add the three lines
config.vm.provision "ansible" do |ansible| ansible.playbook = "myFirstPlaybook.yaml" end
immediately after the line starting with config.vm.provision "file"
. When you now shut down the machines again using
vagrant destroy
from within the vagrant directory and then re-start using
vagrant destroy
you will see that the playbook gets executed once for each machine that is being provisioned. Behind the scenes, Vagrant will create an inventory in ~/vagrant/.vagrant/provisioners/ansible/inventory and will add the hosts there (when inspecting this file, you will find that Vagrant incorrectly adds the insecure default SSH key as a parameter to each host, but apparently this overridden by the settings in our Vagrantfile and does not cause any problems).
Of course there is much more that you can do with playbooks. As a first more complex example, I have created a playbook that
- Creates a new user ansible_user and adds it to the sudoer file
- Distributes a corresponding public SSH key
- Installs Docker
- Installs pip3 and the Python Docker library
- Pulls an NGINX container from the Docker hub
- Creates a HTML file and dynamically add the primary IP address of the host
- Starts the container and maps the HTTP port
To run this playbook, copy it to the vagrant directory as well along with the HTML file template, change the name of the playbook in the Vagrantfile to nginx.yaml and run vagrant as before.
Once this script completes, you can point your browser to 192.168.33.10 or 192.168.33.11 and see a custom NGINX welcome screen. This playbook uses some additional features that we will discuss in the next post, most notably variables and facts.
2 Comments