This will encompass things that I find useful and end up looking up later when I haven’t coded in a while.

Build Control Node

I used an Ubuntu 20.04 instance for this. While I do provide installation instructions for Red Hat, everything is focused around Ubuntu. If you want to use another OS, you’ll just need to change the commands for installing and the username (ubuntu).

Create SSH key for management

ssh-keygen -b 4096 -f ~/.ssh/control_node -N ''

Copy SSH key to managed node(s)

If you want to use SCP, you could use the following function (don’t forget to change your management key from mgmt_key.pem to whatever you call it, or omit the -i ~/.ssh/mgmt_key.pem if you’re using passwords for SSH):

# Used to add the pub key to a managed node
add_pub_key() {
  scp -i ~/.ssh/mgmt_key.pem ~/.ssh/ ubuntu@$managed_node:
  ssh -i ~/.ssh/mgmt_key.pem ubuntu@$managed_node 'cat ~/ | tee -a ~/.ssh/authorized_keys && rm ~/'

add_pub_key "your_ip_address_goes_here"

Install Ansible on Control Node

sudo apt-get update && sudo apt-get install -y ansible

Install Ansible on Red Hat, Fedora, or CentOS

dnf -y install
dnf -y install  --enablerepo epel-playground ansible

Inventory File

Add any managed nodes to /etc/ansible/hosts. If you’re just managing one node, you could do something like this:

<managed node ip> ansible_user=ubuntu ansible_ssh_private_key_file=/home/ubuntu/.ssh/mgmt_key.pem

Config File

Your config file should at a minimum point to your inventory like so:


inventory      = /etc/ansible/hosts

Test connectivity to managed nodes

Run this command to ensure that you’re able to authenticate to your managed nodes:

ansible all -m ping

Resources: - generate SSH key - install Ansible on Ubuntu 20.04

Create role

This will generate the folders and files associated with a new role. Newer versions of molecule:

molecule init role <role name>

Older versions of molecule:

molecule init role --role-name <role name>

Install ansible lint

# Ubuntu
sudo apt-get upgrade -y
sudo apt-get install -y ansible-lint

# Mac OS
brew install ansible-lint



- name: Go to the folder and execute command
  command: chdir=/opt/tools/temp ls


Add apt repo

This will create docker.list in /etc/apt/sources.list.d/ and updates the apt repo to reflect this change:

- name: Configure docker apt repo
    repo: 'deb stretch stable'
    state: present
    filename: 'docker'
    update_cache: 'yes'
  become: true

Multi line string

This creates a variable called custom_vim_plugins and assigns it a multi-line string after the |

custom_vim_plugins : |
      name = 'hashivim/vim-terraform'
      merged = 0

Create a directory

- name: Creates directory
    path: /src/www
    state: directory


Find filename and assign to var

- name: "Find path to freddy jar"
    paths:"{{ ansible_env.HOME }}/burpExtensions/freddy_deserialization_bug_finder/"
    patterns: "*.jar"
    recurse: "yes"
    file_type: file  
  register: freddy_path


Running playbooks

Run locally

ansible-playbook site.yml -l localhost -vvv --user=<user> --ask-pass

Specify run locally in playbook

- name: A playbook
  hosts: localhost
  connection: local
  become: yes
    - role: your_role


Run against a particular group of servers

ansible-playbook site.yml -l web -vvv

ansible hosts file:

[web] ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/yourkeyfile.pem 


Set python interpreter for host

If your host has multiple versions of python and you want to specify a particular one to use:

[kali] ansible_user=kali ansible_ssh_private_key_file=~/.ssh/yourkeyfile.pem ansible_python_interpreter=/usr/bin/python3


Set python interpreter as an extra argument

ansible-playbook -vvv site.yml -e "ansible_python_interpreter=/usr/bin/python3"


Specify hosts file location

In this example, we will use a hosts file found in the same directory as site.yml:

ansible-playbook site.yml -i hosts

Test playbook without making changes

ansible-playbook site.yml -C -vvv

Best practices coding guide

Ansible Galaxy

Install galaxy roles to specific directory

Add this to your ansible.cfg:

roles_path = roles.galaxy:roles

This will install all ansible galaxy roles in a directory called roles.galaxy

You can keep local roles in the roles folder.



Install collections roles to specific directory

Add this to your ansible.cfg:

collections_paths = roles.collections:roles

This will install all ansible galaxy roles in a directory called roles.collections.

Your requirements.yml will probably look somewhat like this if you add in collections:

# Install Docker
    - name: geerlingguy.docker

    - name: community.docker

You can trigger installing any collections listed with:

ansible-galaxy collection install -r requirements.yml

Resources: - collections docs - best practices for collections - suggestions on how to set up a requirements.yml file

Run Tests

molecule --debug converge

Reset Test Environment

molecule destroy

Ignore ssh auth checking

There is a security sacrifice to keep in mind with doing this. That being said, add these lines to the ansible.cfg:

host_key_checking = False



Set env var for CI

This will cause Ansible to automatically to search for the password specified in the file specified for ANSIBLE_VAULT_PASSWORD_FILE.

export ANSIBLE_VAULT_PASSWORD_FILE=/path/to/vaultpass.txt

vaultpass.txt should simply contain the password:


Encrypt a file

ansible-vault encrypt file 

Decrypt a file

ansible-vault decrypt file


Decrypt a template, deploy and then delete it


- name: Decrypt template
  local_action: "shell {{ view_encrypted_file_cmd }} {{ role_path }}/templates/template.enc > {{ role_path }}/templates/template"
  changed_when: False

- name: Deploy template

- name: Remove decrypted template
  local_action: "file path={{ role_path }}/templates/template state=absent"
  changed_when: False


Hosts file priority

<current directory>/hosts


Host file vs. inventory

They are the same thing. If needed, you can define a different inventory file in the ansible.cfg file like so:

inventory = /etc/ansible/inventory

Show facts about a host

ansible $HOSTNAME -m ansible.builtin.setup

Show specific fact

ansible $HOSTNAME -m ansible.builtin.setup -a "filter=ansible_distribution"


Specify user and private key for host

If you are having connectivity issues and need to specify a username and private key to connect, you will need to do so in the ansible hosts file like so:

<hostname> ansible_user=<username to connect to target> ansible_ssh_private_key_file=/path/to/private/key/file

Test connectivity to all hosts

ansible -m ping all -vvv

Wait for init to finish

This can be used to wait for updates to install before kicking off your ansible code.

# Updating the cache immediately can conflict with cloud-init scripts and cause failures.
- name: Wait for /var/lib/dpkg/lock-frontend to be released
  shell: while lsof /var/lib/dpkg/lock-frontend; do sleep 10; done;

SSH retries

Add this to the ansible.cfg file:



Fix SSH UNREACHABLE Connection Error

Add the timeout field to the ansible.cfg file:

timeout = 25


List managed hosts

ansible all --list-hosts


Break up long command

We can use the YAML folding operator > to make this command:

- name: Use service account
  shell: gcloud auth activate-service-account "{{ sa_email }}" --key-file "{{ sa_creds_file }}"

Much more manageable to see what’s going on:

- name: Use service account
  shell: >
    gcloud auth activate-service-account "{{ sa_email }}"
    --key-file "{{ sa_creds_file }}"


Create empty variable

This particular example will set the users variable to whatever the value is for users or to an empty list:

users: "{{ users | default([]) }}"


Store output of command across multiple roles

- name: Some command to get users
  shell: wget
  # the output of running the script is stored in users
  register: users

- name: Store users as a fact to be used later
    users: "{{ users.stdout }}"
    cacheable: yes

Resources: - gave me the idea initially - made me aware of the cacheable parameter (very important)

Create fact with list of dictionaries

If you have a list of dictionaries from a variable that has some attributes that you want to use to create a fact, you can do the following:


  - username: 'admin'
    usergroup: 'admin'
    # port 5901 is 1
    vnc_num: 1

Next, we will add a random 8 character long password for each VNC user.


- name: Set vnc_users fact to be used later
    vnc_users: "{{ vnc_users | default([]) \
    + [ {'username': item.username, 'vnc_num': item.vnc_num, \
    'pass': lookup('password', '/dev/null chars=ascii_letters,digits,punctuation length=8')}] }}"
    # Make fact available to other roles
    cacheable: yes
  with_items: "{{ users }}"

As an added bonus, you can also see how to break a long command down over multiple lines above as well.

Resources: - where I got the example code from - how to generate a random password in ansible - what inspired my original idea - how to break a long fact down over multiple lines

String with delimiter to list

- name: Store users as a fact to be used later
    # Split on newlines
    users: "{{ users.stdout.split('\n') }}"
    cacheable: yes


Create users and groups from a list

This particular example will create users and groups with the same name:

- name: Create group for each user
    name: "{{ item }}"
    state: present
  loop: "{{ users }}"

- name: Create home directories for each user
    name: "{{ item }}"
    shell: /bin/bash
    groups: "{{ item }}"
    state: present
  loop: "{{ users }}


Remove trailing and leading quotes from var

{{ variable_name | regex_replace('\"|\"$', '') }}


Modify items in a list with multiple regexes

- name: For each user in the emails list, replace @ with _ and store in a fact called users
    users: "{{ emails | map('regex_replace', '@', '_') | list }}"
    cacheable: yes

- name: For each user in the users list, replace @ with _ and store in a fact called users
    users: "{{ users | map('regex_replace', '\\.', '_') | list }}"
    cacheable: yes

Resources: - how to regex when a . - how to use multiple regexs in a single play - gave me the initial idea

Merge Two lists into list of dictionaries

- name: Store usernames and emails together
    users_emails: "{{ users_emails | default([]) + [dict(username=item[0], email=item[1])] }}"
  loop: "{{ users | zip(emails) | list }}"


Get ansible facts and output to a file

ansible localhost -m ansible.builtin.setup | tee facts


Useful tidbits

Template path example:

src: "{{ ansible_env.HOME }}/roles/your_role/templates/"



Add this in whenever you need to figure out why your code isn’t working:

- debug:
    msg: "System {{ inventory_hostname }} has uuid {{ ansible_product_uuid }}"

Another example where you can specify a name:

- name: 'Get $PATH'
  debug: msg="{{ lookup('env','PATH') }} is an environment variable"

Another example where we end the playbook run after we print the debug message:

- debug:
    msg: "{{ ansible_distribution_release }}"
- meta: end_play

Resources: - how to end the play without throwing an error

Check for availability of role

ansible-doc community.docker


Run role as specific user

- name: Prepare VMs for cluster
  hosts: vm_host
    - role: dependencies
      become: true
    - role_to_run_with_non_elevated_privs
      ansible_become: false # equivalent of become:
      ansible_user: someuser

Resources: - explanation of ansible_become

Define variables for a specific role

For example, if you want to use the geerlingguy.docker role for a bunch of Kali systems and not have it affect your debian systems that you’re also provisioning, you need to trick it by changing some of the facts for the role.

You may not want those facts to be global for all roles, so you can set them for that particular one:

- hosts: all
  name: Prepare VMs for cluster
     - role: geerlingguy.docker
       become: yes
       docker_users: ['ansible']
       ansible_distribution: 'debian'
       ansible_distribution_release: 'buster'
       ansible_os_family: 'Debian'
     - role: provision_debian
       become: yes

You can also accomplish this using this syntax as well:

- hosts: all
  name: Prepare VMs for cluster
     - { role: geerlingguy.docker, become: yes, docker_users: ['ansible'], ansible_distribution: 'debian', ansible_distribution_release: 'buster', ansible_os_family: 'Debian'}

although I don’t think it’s as nice to read.

Resources: - nicer syntax - less nicer syntax

Change fact values for all hosts

If you need to change the facts for all hosts, you can do do something like this:

- name: Prepare VMs for cluster
  hosts: vm_host
    ansible_distribution: 'debian'
    ansible_distribution_release: 'buster'
    ansible_os_family: 'Debian'
    - { role: geerlingguy.docker, become: yes}

Change a line in a file

I also added the bit that’s used to restart the service to make the example complete.

- name: Disable Root Login
    line="PermitRootLogin no"
    - restart ssh
  - name: restart ssh


Create SSH key for a user on a managed node


- name: Generate SSH key
    path: "{{ ssh_key_path }}"
    type: rsa
    size: 4096
    state: present
    # Don't recreate if one is already
    force: no

- name: Set permissions for private key
    path: "{{ ssh_key_path }}"
    owner: "{{ user }}"
    group: "{{ user }}"
    mode: 0600

- name: Set permissions for public key
    path: "{{ ssh_key_path }}.pub"
    owner: "{{ user }}"
    group: "{{ user }}"
    mode: 0600


user: username
key_type: rsa
ssh_key_filename: "{{ user }}_ssh_{{ key_type }}"
ssh_key_path: "/home/{{ user }}/.ssh/{{ ssh_key_filename }}"

Resource: - how to create an openssh keypair

Include variables file based on OS info

Folder structure:

├── tasks
│   └── main.yml
└── vars
    ├── kali-rolling.yml
    ├── Debian-10.yml
    └── Ubuntu-20.yml

In main.yml:

- name: Include Kali specific variables
  include_vars: "{{ ansible_distribution_release }}.yml"
  when: ansible_distribution == 'Kali GNU/Linux'

- name: Include Distribution version specific variables
  include_vars: "{{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml"

Resources: - example playbook - how to use when for the distribution

Use variable with many values

One great use for this is if you have multiple users with multiple attributes:


  - username: 'ubuntu'
    usergroup: 'ubuntu'
  - username: 'user2'
    usergroup: 'user2'


 - name: Create .vnc dirs
     path: "/home/{{ item.username }}/.vnc"
     state: directory
     mode: 0755
     owner: "{{ item.username }}"
     group: "{{ item.usergroup | default(item.username) }}"
     with_items: "{{ vnc_users }}"


Create custom ansible module for role

If you have a problem that is not solveable through what Ansible offers you, STOP the StackOverflow hell and write your own module instead. A little bit of python goes a long way.

Creation and testing

First, grab the data structures you’ll be messing with in ansible and just write some python around them to figure out your logic. In my case, I wanted to add a new key to a list of dictionaries from a list.

list_of_dicts = [{"username": "bob", "usergroup": "bob", "session": 1}, {"username": "jim",
"usergroup": "jim", "session": 2}]
# uids
list_to_merge = ["1000", "1001"]

for a, b in zip(list_of_dicts, list_to_merge):
    a['uid'] = b

Great, it works! Now to make it into an ansible module for a role, create a folder under the role name called library:

mkdir your_role/library

Next, add your code to a python file with a descriptive name using your work in library/


from ansible.module_utils.basic import AnsibleModule
import json

def run_module():
    module_args = dict(
        list_of_dicts = dict(type='list', required=True),
        list_to_merge = dict(type='list', required=True)

    module = AnsibleModule(

    ld = module.params['list_of_dicts']
    l = module.params['list_to_merge']

    for a, b in zip(ld, l):
        a['uid'] = b

    module.exit_json(changed=False, result=ld)

def main():

if __name__ == '__main__':

To call the module from ansible, add the following to your_role/tasks/main.yml:

- name: Merge uids into vnc_users
    list_of_dicts: "{{ vnc_users }}"
    list_to_merge: "{{ uids }}"
  register: vnc_users_uid

# show output to show that it works
- debug: msg="{{ vnc_users_uid.result }}"

# Get the values from the username keys
- debug: msg="{{ item.username }}"
  with_items: "{{ vnc_users_uid.result }}"

Resources: - the solution I finally got to thanks to an unsung hero by the name of larsks - how to build a simple module - general guidance on developing modules

Install packages


  - curl
  - jq
  - tmux


- name: Install dependencies
    name: "{{ install_packages }}"
    state: present
    update_cache: yes
  environment: 'DEBIAN_FRONTEND: noninteractive'
  # If you have an OS specific task:
  when: ansible_distribution_release == "kali-rolling"


List to string

This will take a list (packages) and turn it into a space-delimited string:

"{{ packages | join (' ') }}"

One example of where this could be useful (cause I can’t get this environment variable to work with the apt module, and no one else on the internet seems to have either):

- name: Install dependencies
  # DO NOT put quotes around the {{ }}, or the command wont' work
  shell: DEBIAN_FRONTEND=noninteractive apt-get -y install {{ packages | join (' ') }}


Use a file in a role

If you need to copy a file for a role, you’ll need the following folders:

├── files
│   └── yourfile
├── tasks
│   └── main.yml

Put the file(s) in the files directory.

In tasks/main.yml, you’ll need to use the copy module to get the file into place on the managed node. For example:

- name: Create file
    src: yourfile # you don't need to point to the files directory, just put the file name here
    dest: "/root/yourfile"
    mode: 0755
    owner: root
    group: root

Use a template in a role

If you need a template for a role, you’ll need the following folders:

├── templates
│   └── yourtemplate.j2
├── tasks
│   └── main.yml

The template file needs to have a .j2 extension.

Here’s an example that sets up a systemd job to run VNC for multiple users (vncserver.j2):

Description=Remote desktop service (VNC)

ExecStartPre=/bin/sh -c '/usr/bin/vncserver -kill :{{ item.item.vnc_num }} > /dev/null 2>&1 || :'
ExecStart=/usr/bin/vncserver :{{ item.item.vnc_num }} {{ vnc_client_options }} {{ vnc_client_options_per_user | default() }}
ExecStop=/usr/bin/vncserver -kill :{{ item.item.vnc_num }}


You can then call it in your main.yml with the template module, like so:

- name: Update per-user systemd service files
    src: vncserver.j2
    dest: "/home/{{ item.item.username }}/.config/systemd/user/vncserver.service"
    mode: 0644
    owner: "{{ item.item.username }}"
    group: "{{ item.item.usergroup | default(item.item.username) }}"
    - vnc_ansible_managed_startup_scripts or not item.stat.exists
  with_items: "{{ checksystemd.results }}"

Resources: - how the template is called - example template file - how to transfer files to a managed node

Run a yml file at end of main

If you want to run a play or task after main.yml has finished, you can put that file into the tasks directory and then use the following to call it:

- name: Configure systemd auto-start service
  include: systemd.yml


Variable Precidence for roles

The first place to define variables with the least precidence will be in defaults/main.yml. These can be overwritten with (for example) variables defined in a task (taskname/vars/main.yml).

A full precidence list can be found here:

Resources: - official docs - a discussion that helped me to understand what was going on

Check if file exists

- name: Check if test_file exists
    path: $HOME/test_file
  register: test_file
- name: Report if test_file exists
    msg: "The file exists"
  when: test_file.stat.exists


Delete file if it exists

- name: Delete a file (or symlink) if it exists
    path: $HOME/test_file
    state: absent


Clone git repo

- name: Clone repo
    repo: "{{ repo_url }}"
    dest: "{{ clone_location }}"


Clone CloudCommit repo

- name: Generate temp creds to clone repo
  shell: |
    git config --global credential.helper '!aws codecommit credential-helper $@'
    git config --global credential.UseHttpPath true

- name: Clone repo
    repo: "{{ repo_url }}"
    dest: "{{ clone_location }}"

Coincidentally, the above also showcases how to run multiple bash commands and break them over multiple lines.


Manage containers with docker-compose

Start by installing the community.docker collection:

ansible-galaxy collection install community.docker

Next, create a role for the deployment, and put the following in it:

├── your-docker-compose-deployment
│   ├── docker-compose.yml
├── tasks
│   └── main.yml

In your-docker-compose-deployment, you’ll want to have the folder with your docker compose project.

In tasks/main.yml, you’ll want the following to install required dependencies and then run the deployment:

- name: Install docker sdk
    name: docker

- name: Install docker-compose
    name: docker-compose

- name: Create and start services

Note that pip will default to whatever interpreter you’ve specified.

Resources: - pip with ansible - docs for the docker_compose ansible stuff - how to use a collection in a playbook

Create password and output to template

This can be useful if you want to use .env for a docker-compose deployment, for example. env.j2:

POSTGRES_PASSWORD={{ password }}

Snippet to put into your role:

- name: Generate password
  become: true
  become_user: "{{ docker_user }}"
    src: env.j2
    dest: "{{ docker_compose_deployment_location }}/.env"
    password: "{{ lookup('password', '/dev/null chars=ascii_letters,digits,punctuation length=50') }}"

Resources: - creating passwords in ansible - how to specify a variable to put into a template


Loops and conditionals

This is an example taken from the nginx ansible role:

{% for path in nginx_logrotate_conf.paths %}
{{ path }}
{% endfor %}
{% for option in nginx_logrotate_conf.options %}
    {{ option }}
{% endfor %}
{% if ansible_facts['os_family'] == "Debian" %}
        if [ -f /var/run/ ]; then
            kill -USR1 `cat /var/run/`
{% else %}
        nginx -s reopen
{% endif %}

Resources: - example used - how to iterate through variables in a template

Use custom python function in template

If you need a custom python function for a template, you’ll want to use an ansible filter plugin.

You will have to expose this code to all roles by putting it in a filter_plugins folder like so:

├── ansible.cfg
├── filter_plugins
│   └──
├── hosts
├── roles

class FilterModule(object):
    def filters(self):
        return {
            'get_user': self.get_user

    def get_user(self, users, username, birthdate):
        for user in users:
            if username in user.values() and user['birthdate'] == birthdate:
                return user

You can call it in a template like so:

{{ list_of_users_in_a_dictionary | get_user('mikey','01/01/1988') }}

The first variable for the function, users, is defined to the left of the pipe. The second (username) and third (birthdate) are defined as you would normally define them for a function call: get_user('mikey','01/01/1988').

You can also assign the output to a variable and then use that:

{% set user = list_of_users_in_a_dictionary | get_user('mikey','01/01/1988') %}
{{ }}
{{ user.birthdate }}


Similar to modules, write the code first outside of ansible, and then integrate it.


Check the template that’s generated and make sure it’s working as you expect it to.

Resources: - how to create plugins - how to set a variable

Package Management

Install pip packages


- name: Install pip packages
    name: "{{ item }}"
    state: present
  loop: "{{ pip_packages }}"


  - package_name
  - another_package_name

Install gems


- name: Install gems
    name: "{{ item }}"
    user_install: no
  loop: "{{ gems }}"


  - gem_name
  - another_gem_name

Run bash script

- name: Run a bash script
  become: true
  # Run as the ubuntu user
  become_user: ubuntu
  shell: bash
    chdir: "/home/ubuntu"

Get current host’s IP address

This does not appear to work well on cloud - see Query metadata if you’re running on a cloud instance.

{{ ansible_ssh_host }}


Get value for $HOME

- debug: msg="{{ lookup('env','HOME') }}"


Query metadata


- name: Get aws public IP
    method: GET
    url: ""
    return_content: yes
  register: uri_output

- name: Print public IP
    msg: "Public IP: {{ uri_output.content | regex_replace('\n','') }}"

Output to a template


- name: Get aws public IP
    method: GET
    url: ""
    return_content: yes
  register: uri_output
- name: Public IP to template
    src: ip.j2
    dest: "/tmp/pub_ip.txt"
    public_ip: "{{ uri_output.content | regex_replace('\n','') }}"


PUBLIC_IP={{ public_ip }}

Resources: - initial idea - url module example - how to process the output from the uri module

Add line to end of file

This example will add the tools made available by MSF (such as pattern_create.rb) for exploit dev to the global $PATH:

- name: Add MSF tools to PATH
    path: /etc/zsh/zshrc
    line: export PATH=$PATH:/usr/share/metasploit-framework/tools/exploit
  when: ansible_distribution_release == "kali-rolling"


Get home dir

Get the env var for the currently connected user:

chdir: "{{ ansible_user_dir }}"

Another way (I’ve gotten errors around this before in certain situations like calling ansible from user-data):

chdir: "{{ ansible_env.HOME }}"

Note: both of these will return /root if you use become:.

Get user’s home directory via env (this will get the user that you connected with if you’re using become):

chdir: "{{ lookup('env','HOME') }}"

Debug line:

- debug:
    msg: "One way: {{ ansible_user_dir }} | Another way: {{ ansible_env.HOME }} | A final way: {{ lookup('env', 'HOME') }}"

Resources: - suggestion for ansible_user_dir

Download and extract zip

- name: Download and extract AWS CLI
    src: "{{ aws_download_server }}"
    dest: "{{ ansible_user_dir }}"
    remote_src: yes