Useful Ansible Stuff

Table of Contents

Header image with a pure black background and the words “useful ANSIBLE stuff” one word after the other in a vertical arrangement. The words “useful” and “stuff” are both a fluorescent green reminiscent of terminals and have a bit of an hour-glass curve to them. The word “Ansible” is stylized with a rainbow-esque outline. My anthropomorphic dark blue and light blue husky character peeks at the viewer from the top right corner of the image while blepping. Ansible community logo by Ansible licensed under CC BY-SA 4.0. Anthro Shammers artwork by Koidel Coyote.

Intro

I’ve been using Ansible and crafting custom roles and playbooks for a few years now. I’m by no means an expert and these aren’t necessarily wizardry or amazing, complex operations. In fact, if you know how to look for them, you can find out all of this information yourself by going through Ansible’s decent documentation for commands.

I wanted to make a quick “cheat sheet” of useful commands and configurations that I’m constantly using and occasionally need to look up for myself and I hope that this quick reference will be helpful for you as well. Feel free to expand the table of contents above and jump to the parts that interest you the most.

I’ll probably update this post later with new stuff that I find useful, so check back every now and then if you’ve found this helpful.

My Favorite Way to Install Ansible

Ansible is just a Python tool. While you can use your package manager(s) to install it, it might not be up-to-date. The better option is to use Python 3’s pip. It’s also a good idea to install Ansible within a Python venv or virtualenv to keep it from mucking up your system. Personally, I use virtualenvs with the help of virtualenvwrapper.

You can install virtualenv and virtualenvwrapper on your system-wide Python in Linux without causing too many problems:

$ pip3 install virtualenv virtualenvwrapper

Some Linux distributions and macOS won’t let you install to the system Python installation. For Linux distros that won’t, you can install Ansible utilizing pipx instead:

# Debian-based apt example
$ sudo apt install pipx

$ pipx install ansible

The same goes for macOS, but I personally opt to just install a Python version using Homebrew as well as virtualenv and virtualenvwrapper:

# Homebrew on macOS to install Python 3.12, virtualenv, and virtualenvwrapper
$ brew install python@3.12 virtualenv virtualenvwrapper

You may also be able to find virtualenv and virtualenvwrapper in your package manager for your system. If you can, then that tends to work well. virtualenvwrapper can also be installed via Homebrew on macOS. The hardest part about any of these install methods is just figuring out where it was put

Once you have virtualenvwrapper installed, you should locate the shell script it creates (this varies depending on the system and the installation method) and add a source to it in your ~/.bashrc or ~/.zshrc (if you’re using zsh).

# ~/.zshrc on macOS
# installed via: brew install virtualenvwrapper
# ...
source /opt/homebrew/bin/virtualenvwrapper.sh

Then we just create a virtualenv using the command mkvirtualenv, specifying the name and Python version we want:

$ mkvirtualenv -p python3.12 ansible

# This will auto-activate the environment
# In the case that it doesn't...
$ workon ansible

# Once in the environment...
(ansible) $ pip install ansible

# See where it is
(ansible) $ which ansible

# Check its version
(ansible) $ ansible --version

# When you're done...
(ansible) $ deactivate

$

Updating Ansible

If you used pip to install Ansible, you can activate whatever venv/virtualenv you have for it and upgrade from there:

$ workon ansible
(ansible) $ pip install --upgrade ansible

If you’re using pipx:

$ pipx install --upgrade ansible

Ansible Ad-Hoc Command

Ansible supports running “ad-hoc” commands using the various modules that Ansible provides. These are one-off things such as uninstalling a particular package on a particular host or maybe checking versions of software for vulnerabilities in your entire environment.

# Uninstalling a package quickly using apt on host0.example.com
# -i specifies inventory file
# -m specifies the module we want to use (apt)
# -a specifies what we want to handle with the module
# --become elevates privs, which is needed to do APT operations
$ ansible db0.example.com -i inventory.ini -m ansible.builtin.apt -a "name=hugo state=absent" --become

# I won't show you the output of this because it's very long
# Just trust that it'll give you a whole bunch of useful info if the package is present when ran!


# Run a shell command to get the version of Git installed
# We want to see if we're vulnerable to CVE-2024-32004 based on version
$ ansible all -i inventory.ini -m ansible.builtin.shell -a "git --version"

# output:
web0.example.com | CHANGED | rc=0 >>
git version 2.39.2

web1.example.com | CHANGED | rc=0 >>
git version 2.39.2

db0.example.com | CHANGED | rc=0 >>
git version 2.39.2

db1.example.com | CHANGED | rc=0 >>
git version 2.39.2

stats.example.com | CHANGED | rc=0 >>
git version 2.39.2

Excluding Systems

Occasionally, you may want to run a playbook against all your systems in a playbook except for a couple. Usually you’d do this if there’s some sort of mismatch between systems or if you want to do an ad-hoc kinda staggered deployment.

You can achieve this using the --limit flag and using exclamation points to exclude the systems you don’t want a playbook to run on.

# Run on all database servers defined in the playbook except these two
$ ansible-playbook -e environments/production -i environments/production/hosts database.yml --limit "!chi-db-01:!chi-db-07"

Vault Password Config

When you’re using Ansible vaults to store encrypted secrets, you can put your passphrase into a dot-file and have it automatically included as part of ansible.cfg.

Note: You’ll want to be careful NOT to add this to your Git repository. You might even just want to add this file to your .gitignore.

# Create a file in your home dir called .ansible_vault_pass
# This is a dot-file so it's hidden from the default ls commands
$ echo "mylongsillypassphrasethatssecure" > ~/.ansible_vault_pass
# Only owner can read and write, nobody else (but sudo maybe)
$ chmod 0600 ~/.ansible_vault_pass

# ansible.cfg
# Edit your config file for Ansible and add this line:
vault_password_file=~/.ansible_vault_pass

When you use ansible.cfg for your vault password file, you’ll need to use ansible-vault from the same directory you have ansible.cfg in.

Another option so you don’t have to be in the ansible.cfg directory? Environment variables.

$ echo "mylongsillypassphrasethatssecure" > ~/.ansible_vault_pass
$ chmod 0600 ~/.ansible_vault_pass

# Add to your ~/.bashrc or ~/.zshrc for automagic encrypt/decrypt anywhere
export ANSIBLE_VAULT_PASSWORD_FILE=~/.ansible_vault_pass

Arbitrary SSH Key Generation

Need some SSH keys but don’t actually care what they are? There’s a module for that. In this particular scenario, say you have a handful of developers who need to be able to authenticate with GitHub easily without much effort. We’ll have Ansible generate ed25519 keys for each user and update their SSH configs to automatically use the generated private key. Then all the developers have to do is add the public keys you get to their GitHub accounts.

Note that you may have to install community.crypto collection using ansible-galaxy if you don’t already have this module.

---
# You'd probably want to populate a list of users from another source
# But for simplicity, we'll define the list here
- name: Create list of users
  ansible.builtin.set_fact:
    users: [ steve, amanda, jessica, grace ]

# Generate ed25519 keys to use for GitHub for a list of users
# We use inventory_hostname to add the host to the comment for the key
- name: Generate SSH key pairs for each user
  community.crypto.openssh_keypair:
    path: "/home/{{ item }}/.ssh/gitkey"
    owner: "{{ item }}"
    type: ed25519
    comment: "{{ item }} - {{ inventory_hostname }} GitHub key"
  loop: "{{ users }}"
  register: keys

# Adding GitHub configuration to each user's .ssh/config
# We use "insertbefore: BOF" so it goes to the top of the file
- name: Add GitHub to SSH configs for each user
  ansible.builtin.blockinfile:
    path: "/home/{{ item }}/.ssh/config"
    append_newline: true
    insertbefore: BOF
    backup: true
    block: |
      Host github.com
        Hostname github.com
        User git
        IdentityFile ~/.ssh/gitkey      
  loop: "{{ users }}"
  
- name: Print pub keys to stdout
  ansible.builtin.debug:
    msg: "{{ item['public_key'] }}"
  loop: "{{ keys['results'] }}"

Ad-Hoc KISS(es)

If you’ve created a bunch of files (labeled something like delete_me_00, delete_me_01, etc. etc.) in a place on all your systems and realized you don’t want them anymore and don’t want your environment to be full of snow flakes, you can utilize Ansible ad-hoc commands to delete those files.

If you’re someone who likes to follow “best practices” with Ansible, you might be thinking, “I’ll just add a task to my role that does this.” That’s cool and all, but you’re making a one-off task that you’re only realistically going to run once. Why waste the time and energy to do that?

“Ah, I’ll just craft an Ansible command using the file module!” That’s a better idea, but if you have a bunch of files that are named similarly in a directory, what do you do?

“I’ll use loops in the ad-hoc commands!”

Pump the brakes. Ansible’s built-in looping doesn’t work with Ansible ad-hoc commands. To do something similar, you’d need to do some shell scripting to loop through instead.

Or… you can apply the KISS principle (Keep It Simple, Stupid!). Ansible’s shell module lets you run shell commands exactly as you would at the command line. It’s pretty powerful (and dangerous!).

# Use the usual shell globbing in an Ansible ad-hoc command
$ ansible all -i inventory.ini -m ansible.builtin.shell -a "rm -rf /tmp/delete_me_*"

# Output:
web0.example.com | CHANGED | rc=0 >>

web1.example.com | CHANGED | rc=0 >>

db0.example.com | CHANGED | rc=0 >>

db1.example.com | CHANGED | rc=0 >>

stats.example.com | CHANGED | rc=0 >>

Avoid the Red Waves

You should have a staging environment of some sort to test your Ansible deployment before going to production unless you’re doing something simpler and less mission-critical (like a silly little blog). Even with a staging environment, it’s a good idea to double-check your work and make sure things are looking good before attempting to deploy. If you’re the kind of person who’s been going full YOLO and just deploying, waiting for Ansible to bleed all over your screen in red letters, fixing the issues, and retrying… it doesn’t have to be that way.

# Use --syntax-check to check for issues before you deploy!
$ ansible-playbook -i inventory.ini --syntax-check my-playbook.yml

Lint Traps

Also a useful tool for any kind of language you write is a linting tool. Ansible provides ansible-lint which will scour over your configurations and tell you whether your configs are following best practices or not.

ansible-lint can be installed via pip. It’s recommended that you use venv/virtualenv/conda environments when possible to keep from mucking up your system Python installation.

# Switch to ansible virtualenv
# (this is how I install and use ansible!)
$ workon ansible

# Install ansible-lint
$ pip install ansible-lint

# Run ansible-lint in the root of your Ansible configurations
$ ansible-lint

Note that you can also just use ansible-lint instead of doing a --syntax-check at all, but it doesn’t hurt to combine these. If you’re putting your Ansible configurations into a Git repository (you should be…), you can have a hook that runs ansible-lint against your branches before allowing merge to main.

Ignoring Linting Issues

Linting is not a bullet-proof solution and sometimes it doesn’t know exactly what you want. In these kinds of cases, you can tell ansible-lint to ignore certain tasks it sees as issues:

# ansible-lint will usually complain about this
# update: false will keep it from pulling new stuff
# adding the tag "skip_ansible_lint" tells it to buzz off
- name: Grab git repository if doesn't exist
  ansible.builtin.git:
    repo: git@github.com:johnsmith123/personal-site.git
    dest: /home/jsmith/personal-site
    update: false
  tags: skip_ansible_lint

Conclusion

That’s it for now. Again, I suggest you check back every now and then as I add more content to this and do the inevitable corrections.

Take care.