Getting started with Ansible playbooks: more steps towards DevOps

Ansible playbooks are YAML files with target host/group information, command execution and some loops and logic. A playbook is a blueprint of an operation on our managed nodes. Playbooks are the first step towards managing infrastructure as code.

Playbooks can be run by using the ansible-playbook command line tool. They are interpreted sequentially in the order we write the plays and tasks in them, thus they are processed top-down.

There must be minimum two things in a play:

  • A target host/group definition
  • At least one task to run

Remember, we have a Debian server in our vmware group in the inventory file.

[vmware]
debserver ansible_host=192.168.122.129  ansible_connection=ssh  ansible_become_method=su

I added the Ansible control node to the inventory as well, so the file contains the following lines:

# Ansible control host
ansible ansible_host=localhost ansible_connection=local

# Practice lab machines
[vmware]
debserver ansible_host=192.168.122.129  ansible_connection=ssh  ansible_become_method=su

We will create an Ansible playbook that operates on the vmware group. Later we will separate the target nodes by function into different groups.

The first playbook will do basic operations on the Debian server, like

  • Install a bunch of tools and libraries
  • Check out configuration from a Github repository
  • Move the configuration files into their final places

Creating a basic Ansible playbook

Let’s open the Ansible playbook documentation in the browser for reference, and start writing our small, idempotent playbook for automating the initial configuration of our nodes!

We will create a file called play-base-config.yml in a new directory called plabyooks, and open it in an editor. The first lines of our playbook is the name of the first play and the targeted hosts/groups. It is followed by the instruction to elevate user privileges on the managed host and use the root user for this.

---
- name: Basic OS and user setup
  hosts: vmware
  become: true
  become_user: root

This is the first mandatory element in a playbook followed by at least one task definition.

Let’s install the necessary libraries and tools we need! Using the tasks keyword we will define a task that uses the ansible.builtin.apt module for installing software.

---
- name: Basic OS and user setup
  hosts: vmware
  become: true
  become_user: root

  tasks:
    - name: Install the basic packages
      ansible.builtin.apt:
        name:
          - vim
          - tmux
          - python3
          - python3-apt
          - sshpass
          - git
          - wget
          - curl
          - zsh
        state: present

The apt module accepts a list of package names, so we use a YAML list to define what do we want to install. The indentation in YAML is very important, we must take care of it. The state keyword instructs the task to make sure the packages are present on the managed nodes.

Now we can dry run our playbook with using -C (or –check) option that runs the playbook in check mode. It means that nothing destructive operation will run on the target hosts if the used modules support it.

The ansible.builtin.apt module supports the check mode according to its documentation.

Let’s check the syntax of our playbook and try to run it in check mode!

$ ansible-playbook -i inventory playbooks/play-base-config.yml --syntax-check      

playbook: playbooks/play-base-config.yml

$ ansible-playbook -i inventory playbooks/play-base-config.yml --become --ask-become-pass --check
BECOME password: 

PLAY [Basic OS and user setup] *****************************************************************************************************************************

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

TASK [Install the basic packages] **************************************************************************************************************************
changed: [debserver]

PLAY RECAP *************************************************************************************************************************************************
debserver                  : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

The playbook ran our play on the vmware group and tried to execute our task. Obviously it did not install any software because we ran it in check mode, but the output at the end convinced us that our playbook works and valid. The summary at the end lists the managed hosts and the final results of the task execution.

debserver                  : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

The output explains itself.

Now we are ready to run the command without the –check option and install the software on the Debian server. At the first time it will make sure to install the listed tools in the task, then we can re-run the playbook anytime we want and it will not do anything.

PLAY RECAP *************************************************************************************************************************************************
debserver                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Let’s touch another important topic here before we grow our playbook into a mammoth: linting.

Ansible and YAML lint

A linter helps to avoid structural and logical errors in our code. YAML thus Ansible code are very sensitive to indentation. Linting helps to keep the best practices while we write our infrastructure as code.

We can install ansible-lint and yaml-lint with the package manager or with Python pip.

Let’s use pip now!

$ pip install ansible-lint

After we installer our linter, let’s check our code!

$ ansible-lint playbooks/play-base-config.yml

Passed with production profile: 0 failure(s), 0 warning(s) on 1 files.

If we did everything right, then our code passed ansible-lint.

We will try to keep our code tidy to pass the linter every time.

Extending our playbook to configure our servers

Now we have a working playbook and a linter that keeps us on the track.

Until this point we worked in the terminal and we used a simple text editor for managing our files, but as our environment grows we will need something better. It is the topic of a lot of debate, but Visual Studio Code is a very great and versatile tool. VSCodium is another good option.

The automatic linting and syntax checking will help us a lot in the future. As the code base grows the integrated source control management is another benefit.

We have already installed

  • vim
  • tmux
  • zsh

with our playbook, let’s configure them! I keep my personal configuration files in my Github repository, so I want to simply clone / check out the repo and use a defined version of the files as my configuration.

We have to extend our playbook to clone the repo, take the files and copy them into their place. After the configuration we want to change the default shell of our user to zsh.

The final playbook will look like this:

---
- name: Basic OS and user setup
  hosts: vmware
  become: true
  become_user: root

  tasks:
    - name: Install the basic packages
      ansible.builtin.apt:
        name:
          - vim
          - tmux
          - python3
          - python3-apt
          - sshpass
          - git
          - wget
          - curl
          - zsh
        state: present

    - name: Check out my dotfiles repository from Github
      ansible.builtin.git:
        repo: https://github.com/tmolnar0831/dotfiles.git
        dest: /home/tmolnar/stuff/dotfiles
        version: 4bfbaa2917844a46ab936bddba5125af16c10bca
      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:
        - { 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 }

    - 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:
        - { 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 }
      become_user: tmolnar

    - name: Change the user shell to zsh
      ansible.builtin.user:
        name: tmolnar
        shell: /usr/bin/zsh

Unfortunately the check mode will not work here, as we have sequential steps to download code from Github and use it in the next steps.

If we use the playbook above the code will run without errors, it is idempotent and fast. There are a lot of hardwired items in it though. It is neither beautiful nor useful.

We want to write good, secure and modular Ansible code, so in the next round we will split up a playbook into roles, variables and defaults.

My playbooks can be found on my Github repository.

If you have anything to share then please visit my Tom’s IT Cafe Discord Server!

Leave a comment