Subscribe to the feed

In the article How to use Ansible to configure Vim, I developed an Ansible playbook to configure an initial Vim environment using a few Vim plugins. In this current article, I continue building on the previous example by converting the playbook into an Ansible role.

Ansible roles allow you to develop reusable automation components by grouping and encapsulating related automation artifacts, like configuration files, templates, tasks, and handlers. Because roles isolate these components, it's easier to reuse them and share them with other people. You can also make your roles configurable by exposing variables that users can set when calling the role, allowing them to configure their system according to specific requirements.

[ You might also like: The four things you must be able to do in Vim ]

In this article, I convert the original playbook vim-config.yaml into a reusable role. At this time, I won't add any new functionality, but I'll further expand this example in the next article. You can find the original playbook and vimrc configuration file here.

1. Starting a new role

To create an Ansible role, it's enough to make a directory following the standard directory structure documented in the official documentation.

To make it easier and follow the standard, use the ansible-galaxy role init role_name command to create this directory for you. This command creates the required structure, including a few templates for documentation that you can update. Use it to initialize the vim role under the roles directory. First, create the roles directory and switch to it:

$ mkdir roles
$ cd roles

Then, use the command ansible-galaxy to initialize the role:

$ ansible-galaxy role init vim
- Role vim was created successfully

Now, verify the role directory structure:

$ tree vim
vim
├── defaults
│   └── main.yml
├── files
├── handlers
│   └── main.yml
├── meta
│   └── main.yml
├── README.md
├── tasks
│   └── main.yml
├── templates
├── tests
│   ├── inventory
│   └── test.yml
└── vars
    └── main.yml

8 directories, 8 files

While not required for the role to work, it's highly recommended to document your role by updating the files README.md and meta/main.yml. If your role depends on other roles to execute, it's important to document these dependencies in meta/main.yml, allowing Ansible to download them automatically if required.

Switch into the newly created directory:

$ cd vim

Your Vim role does not require any dependencies. Here's an example of a working meta configuration file. Update it with your name, company name, and a suitable license, if necessary:

$ vim meta/main.yml
galaxy_info:
  author: <YOUR NAME>
  description: Deploy and configure Vim with plugins
  company: <YOUR COMPANY>

  license: MIT

  min_ansible_version: 2.8

  platforms:
  - name: Fedora
    versions:
    - 33

  galaxy_tags: []

dependencies: []

The original file has additional comments, which I removed for brevity.

Next, define the tasks to execute.

2. Defining tasks

Generally speaking, your role will execute one or more tasks to configure the target system according to the role's requirements. In this case, you'll want to install and configure Vim. By default, when you execute a role, it looks for a file named main.yml in the tasks subdirectory and execute all the tasks listed within it. You can break the tasks into multiple files for more complex roles and call them from main.yml using the include_tasks or import_tasks modules.

For this role, include all required tasks in the tasks/main.yml file:

$ vim tasks/main.yml

---
# tasks file for vim
- name: Install required packages
  package:
    name: "{{ install_packages }}"
    state: present
  become: yes
  tags:
    - install_packages

- name: Ensure .vim/{autoload,bundle} directory exists
  file:
    path: "{{ item }}"
    state: directory
    recurse: no
    mode: 0750
  loop:
    - "{{ vim_dir }}"
    - "{{ vim_dir }}/autoload"
    - "{{ vim_dir }}/bundle"

- name: Ensure Pathogen is in place
  get_url:
    dest: "{{ vim_dir }}/autoload/pathogen.vim"
    url: https://tpo.pe/pathogen.vim

- name: Deploy plugins
  git:
    dest: "{{ vim_dir }}/bundle/{{ item.name }}"
    repo: "{{ item.url }}"
    clone: yes
    update: yes
    recursive: no
  loop: "{{ plugins }}"

- name: Ensure .vimrc config in place
  copy:
    src: vimrc
    dest: "{{ vimrc }}"
    backup: yes
    mode: 0640

Notice that, unlike the original playbook, you don't include the list of packages or plugins to install directly with the task definition. Instead, you're using the variables install_packages and plugins.

By defining variables instead of hard coding the values, you make your roles more reusable and easier to maintain. Now, define values for these variables in two different ways. Start with the plugins variable, covered next.

3. Defining default variables

When you're developing an Ansible role, you might want to allow role users to provide values to customize how the role performs its tasks. These variables make your role more reusable, allowing users to modify the outcome based on their specific requirements.

For this example, the plugins variable allows the users to specify which plugins they want to install with Vim, making the role flexible for their needs. It's recommended to define a default value for it in the defaults/main.yml file to ensure that the roles execute successfully even if the user does not provide a value to this variable.

This file defines variables with a very low precedence which means Ansible will only use them in case the value wasn't defined anywhere else.

Now, define the default value for the plugins variable like this:

$ vim defaults/main.yml

---
# defaults file for vim
plugins:
  - name: vim-airline
    url: https://github.com/vim-airline/vim-airline
  - name: nerdtree
    url: https://github.com/preservim/nerdtree
  - name: fzf-vim
    url: https://github.com/junegunn/fzf.vim
  - name: vim-gitgutter
    url: https://github.com/airblade/vim-gitgutter
  - name: vim-fugitive
    url: https://github.com/tpope/vim-fugitive
  - name: vim-floaterm
    url: https://github.com/voldikss/vim-floaterm

In this case, you're defining the default value using the same values from the original playbook, which means that if you call the role without providing a value for this variable, it will behave exactly like the original playbook, installing these six plugins.

Define the internal variables.

4. Defining role variables

Another class of variables is role variables or internal variables. By defining these variables in a separate file from the tasks, you make your role easier to maintain. You can reuse these variables in many places, and it's easier to update them in a central place. However, you don't want to make it too easy for users to override them by setting them in general locations such as the playbook or the inventory.

The variables install_packages, which defines a list of required packages to install, and vimrc, which specifies the location of Vim's configuration file, are good examples of internal variables. Define them in vars/main.yml. This file defines variables with higher precedence that are not easily overridden. Users can still provide values if necessary by explicitly setting them when calling the role, but in this case, you can assume they know what they're doing.

$ vim vars/main.yml

---
# vars file for vim
vim_dir: "{{ ansible_env.HOME }}/.vim"
vimrc: "{{ ansible_env.HOME }}/.vimrc"
install_packages:
  - vim-enhanced
  - git
  - powerline-fonts
  - fzf

For more details on how Ansible variables precedence works, consult Understanding variable precedence in the documentation.

5. Copying files

The last step to create this role is to copy the file vimrc to the files directory. By default, when using the copy module as a role task, it will look for files to copy in the files subdirectory. Define the vimrc file like this:

$ vim files/vimrc

execute pathogen#infect()
syntax on
filetype plugin indent on

colo darkblue

" Configuration vim Airline
set laststatus=2

let g:airline#extensions#tabline#enabled=1
let g:airline_powerline_fonts=1

" Configuration NERDTree
map <F5> :NERDTreeToggle<CR>

" Configuration floaterm
let g:floaterm_keymap_toggle = '<F12>'
let g:floaterm_width = 0.9
let g:floaterm_height = 0.9

" Configuration Vim.FZF
let g:fzf_preview_window = 'right:50%'
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6  }  }

Save and close the file to complete your role. Now, it's time to define the playbook to use the role.

6. Calling the role from a playbook

Now that your role is complete, you can call it from your playbooks. By default, Ansible looks for roles in the roles subdirectory relative to the playbook file or the system directory /etc/ansible/roles. You can also use the Ansible configuration roles_path to define alternative role locations.

For this example, create a playbook in the same directory where you created the roles directory. Switch to it:

$ cd ../..
$ ls
roles

Create the playbook vim-config.yaml, similar to the original playbook but this time, instead of defining the tasks, use the module import_role to import your new vim role into the playbook:

$ vim vim-config.yaml

- name: Config Vim with plugins
  hosts: localhost
  gather_facts: yes
  become: no

  tasks:
    - name: Configure Vim using role
      import_role:
        name: vim

You can also include the role in the playbook using the module include_role. I'll discuss the differences between these two modules in a separate article. If you can't wait, check the documentation.

Finally, execute the playbook.

8. Execute the playbook

Execute the playbook using the ansible-playbook command with the -K parameter and type your sudo password to allow Ansible to install system packages.

Note: Backup any existing .vimrc configuration file before running this playbook.

$ ansible-playbook -K vim-config.yaml
BECOME password: 
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

PLAY [Config Vim with plugins] ***********************************************

TASK [Gathering Facts] *******************************************************
ok: [localhost]

TASK [vim : Install required packages] ***************************************
changed: [localhost]

TASK [Ensure .vim/{autoload,bundle} directory exists] ************************
changed: [localhost] => (item=/home/ricardo/.vim)
changed: [localhost] => (item=/home/ricardo/.vim/autoload)
changed: [localhost] => (item=/home/ricardo/.vim/bundle)

TASK [vim : Ensure Pathogen is in place] *************************************
changed: [localhost]

TASK [vim : Deploy plugins] **************************************************
changed: [localhost] => (item={'name': 'vim-airline', 'url': 'https://github.com/vim-airline/vim-airline'})
changed: [localhost] => (item={'name': 'nerdtree', 'url': 'https://github.com/preservim/nerdtree'})
changed: [localhost] => (item={'name': 'fzf-vim', 'url': 'https://github.com/junegunn/fzf.vim'})
changed: [localhost] => (item={'name': 'vim-gitgutter', 'url': 'https://github.com/airblade/vim-gitgutter'})
changed: [localhost] => (item={'name': 'vim-fugitive', 'url': 'https://github.com/tpope/vim-fugitive'})
changed: [localhost] => (item={'name': 'vim-floaterm', 'url': 'https://github.com/voldikss/vim-floaterm'})

TASK [Ensure .vimrc config in place] *****************************************
changed: [localhost]

PLAY RECAP *******************************************************************
localhost                  : ok=6    changed=5    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

This playbook runs and executes all tasks in the localhost. If you want to configure a remote system, create an inventory file with the desired systems and update the playbook hosts list.

[ Looking for more on system automation? Get started with The Automated Enterprise, a free book from Red Hat. ] 

Wrap up

Now you have a role that installs and configures Vim that you can reuse and share. In the next article in this series, I'll improve this role by adding a template file to make the configuration even more flexible.

You can also use Molecule to test your roles using containers or virtual machines. If you want to know more about that tool, read my article Developing and Testing Ansible Roles with Molecule and Podman - Part 1 in the official Ansible blog.

For more information about Ansible, consult the official documentation.


About the author

Ricardo Gerardi is Technical Community Advocate for Enable Sysadmin and Enable Architect. He was previously a senior consultant at Red Hat Canada, where he specialized in IT automation with Ansible and OpenShift. 

He has been a Linux and open source enthusiast and contributor for over 20 years. He is currently interested in hacking stuff using the Go programming language, and he's the author of Powerful Command-Line Applications in Go: Build Fast and Maintainable Tools. Ricardo also writes regularly about Linux, Vim, and command line tools for Opensource.com and Enable Sysadmin community publications.

Ricardo enjoys spending time with his daughters, reading science fiction books, and playing video games.

Read full bio
UI_Icon-Red_Hat-Close-A-Black-RGB

Browse by channel

automation icon

Automation

The latest on IT automation for tech, teams, and environments

AI icon

Artificial intelligence

Updates on the platforms that free customers to run AI workloads anywhere

open hybrid cloud icon

Open hybrid cloud

Explore how we build a more flexible future with hybrid cloud

security icon

Security

The latest on how we reduce risks across environments and technologies

edge icon

Edge computing

Updates on the platforms that simplify operations at the edge

Infrastructure icon

Infrastructure

The latest on the world’s leading enterprise Linux platform

application development icon

Applications

Inside our solutions to the toughest application challenges

Original series icon

Original shows

Entertaining stories from the makers and leaders in enterprise tech