After we ran ad-hoc commands and created a monolith playbook, we will increase our level of automation. We will separate our code much better with introducing modular, reusable file structures called roles. Ansible roles will load variables, handlers and tasks automatically for us based on a defined directory and file structure.
Though Ansible is very flexible and we can use it in a lot of ways, there is a really powerful structure that will allow us to bring out the maximum of our automation.
Let’s create a new directory in our Ansible project home and call it roles
. It must be in the project directory where our inventory
and the playbooks
directories reside.
Now we will use a new tool called ansible-galaxy
to create a skeleton of the role. The reference for using the command can be found here.
$ ansible-galaxy init common
- Role common was created successfully
The command will create the proper directory hierarchy for us in the newly created common
role. The directory structure looks like the following:
$ tree common
common
├── defaults
│ └── main.yml
├── files
├── handlers
│ └── main.yml
├── meta
│ └── main.yml
├── README.md
├── tasks
│ └── main.yml
├── templates
├── tests
│ ├── inventory
│ └── test.yml
└── vars
└── main.yml
A role is technically a collection of the above files and directories.
By default Ansible will look in each directory within a role for a main.yml
file for relevant content (also main.yaml
and main
):
tasks/main.yml
– the main list of tasks that the role executes.handlers/main.yml
– handlers, which may be used within or outside this role.library/my_module.py
– modules, which may be used within this role (see Embedding modules and plugins in roles for more information).defaults/main.yml
– default variables for the role (see Using Variables for more information). These variables have the lowest priority of any variables available, and can be easily overridden by any other variable, including inventory variables.vars/main.yml
– other variables for the role (see Using Variables for more information).files/main.yml
– files that the role deploys.templates/main.yml
– templates that the role deploys.meta/main.yml
– metadata for the role, including role dependencies and optional Galaxy metadata such as platforms supported.
Let’s take our play-base-config.yml
playbook to the next level with separating the tasks, variables and handlers! Let’s see where are we now! We have a playbook with target host information on the top of it followed by tasks and variables. We have created a skeleton of our new common
role. We call it “common” because we want to run this code on every test server we deploy.
Now we will open our play-base-config.yml
playbook in VSCode and start cutting it up to pieces.
The host information and the connection meta data will remain in the playbook.
---
- name: Basic OS and user setup
hosts: all
become: true
become_user: root
This information is followed by the task definition below it.
Let’s copy everything from below the tasks:
keyword into our new role’s tasks/main.yml
file!
Well, now we can separate the variables from the code. There are two places in our role for these variables: the vars
directory and the defaults
directory. The defaults
has the lowest precedence, so we will put here our data.
Reference: https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_variables.html
Let’s open the defaults/main.yml
file as well in our editor and start moving the variables there with taking care of the YAML data types. We name our variables carefully, putting the role name in it and its function. Months later we will thank it for us. It is even better idea to comment our code and variables. Memories fade, but the comments and documentation are everlasting.
Let’s edit the defaults/main.yml
file now!
---
# defaults file for common
common_packages:
- vim
- tmux
- python3
- python3-apt
- sshpass
- git
- wget
- curl
- zsh
common_git_repo_url: https://github.com/tmolnar0831/dotfiles.git
common_git_checkout_dest: /home/tmolnar/stuff/dotfiles
common_git_checkout_version: 4bfbaa2917844a46ab936bddba5125af16c10bca
common_root_copy_files:
- { src: /home/tmolnar/stuff/dotfiles/.vimrc, dest: /root/.vimrc }
- { src: /home/tmolnar/stuff/dotfiles/.tmux.conf, dest: /root/.tmux.conf }
- { src: /home/tmolnar/stuff/dotfiles/.zshrc, dest: /root/.zshrc }
common_user_copy_files:
- { src: /home/tmolnar/stuff/dotfiles/.vimrc, dest: /home/tmolnar/.vimrc }
- { src: /home/tmolnar/stuff/dotfiles/.tmux.conf, dest: /home/tmolnar/.tmux.conf }
- { src: /home/tmolnar/stuff/dotfiles/.zshrc, dest: /home/tmolnar/.zshrc }
Yes, it could be much nicer with creating variables for the users and directories as well, but it will be fine for now. We are just practicing though. We start with a working code base and iteratively build upon it.
As the next step we have to copy our tasks to the tasks/main.yml
file. We have to take care of referring to the variables we have created in our variables file. In YAML and Jinja2 when we start a token with {{
we must quote or double quote it like "{{
!
Here is the tasks/main.yml
file with the referenced variables from the defaults/main.yml
.
---
# tasks file for common
- name: Install the basic packages
ansible.builtin.apt:
name: "{{ common_packages }}"
state: present
- name: Check out my dotfiles repository from Github
ansible.builtin.git:
repo: "{{ common_git_repo_url }}"
dest: "{{ common_git_checkout_dest }}"
version: "{{ common_git_checkout_version }}"
become_user: tmolnar
- name: Copy the config files to the root user
ansible.builtin.copy:
src: '{{ item.src }}'
dest: '{{ item.dest }}'
owner: root
group: root
mode: '0754'
remote_src: true
loop: "{{ common_root_copy_files }}"
- name: Copy the config files to my home
ansible.builtin.copy:
src: '{{ item.src }}'
dest: '{{ item.dest }}'
owner: tmolnar
group: tmolnar
mode: '0754'
remote_src: true
loop: "{{ common_user_copy_files }}"
become_user: tmolnar
- name: Change the user shell to zsh
ansible.builtin.user:
name: tmolnar
shell: /usr/bin/zsh
We are almost there! We have to use this role in the playbook instead of listing these tasks. Our playbook will look like this:
---
- name: Basic OS and user setup
hosts: vmware
become: true
become_user: root
roles:
- role: common
Instead of adding the tasks directly here, we just included the new role in the playbook file.
There is one more tweak we have to do: we moved our playbooks into the playbooks
directory. As playbooks will look for roles relative to their place, they will not find our role out of the box. A configuration file adding our roles
directory in the roles_path
will solve this issue.
Let’s create an ansible.cfg
file in the project directory with something similar content:
[defaults]
roles_path = /home/tmolnar/stuff/projects/ansible_lab/roles
Done!
With running the code we can make sure that our playbook finds the role, the tasks see the variables and everything works well.
$ ansible-playbook -i inventory playbooks/play-base-config.yml --become --ask-become-pass --limit vmware --check -vvv
Using the -vvv
option gives us a super verbose output in which we can check the passed variables too.
If you have anything to share then please visit my Tom’s IT Cafe Discord Server!
This text is invaluable. When can I find out more?
LikeLike
Hmm, maybe here? 😁
LikeLike