Ansible Notes

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. 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() {
  managed_node=$1
  scp -i ~/.ssh/mgmt_key.pem ~/.ssh/control_node.pub ubuntu@$managed_node:
  ssh -i ~/.ssh/mgmt_key.pem ubuntu@$managed_node 'cat ~/control_node.pub | tee -a ~/.ssh/authorized_keys && rm ~/control_node.pub'
}

add_pub_key "your_ip_address_goes_here"

Install Ansible on Control Node

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

Inventory File

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

[dev_machines]
<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:

[defaults]

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:
https://www.digitalocean.com/community/tutorials/how-to-set-up-ssh-keys-on-ubuntu-20-04 - generate SSH key
https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-ansible-on-ubuntu-20-04 - 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

Resource: https://zoomadmin.com/HowToInstall/UbuntuPackage/ansible-lint

CWD

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

Resource: https://stackoverflow.com/questions/19369931/ansible-how-to-change-active-directory-in-ansible-playbook

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
  apt_repository:
    repo: 'deb https://download.docker.com/linux/debian 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 : |
      [[custom_plugins]]
      name = 'hashivim/vim-terraform'
      merged = 0

Create a directory

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

Resource:
https://stackoverflow.com/questions/22844905/how-to-create-a-directory-using-ansible

Find filename and assign to var

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

Resource:
https://stackoverflow.com/questions/41565614/ansible-how-to-register-output-from-find-module-and-use-in-other

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
  roles:
    - role: your_role

Resource: https://www.middlewareinventory.com/blog/run-ansible-playbook-locally/

Run against a particular group of servers

ansible-playbook site.yml -l web -vvv

ansible hosts file:

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

Resource:
https://stackoverflow.com/questions/22129422/running-an-ansible-playbook-on-a-particular-group-of-servers

Set python interpreter for host

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

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

Resource: https://stackoverflow.com/questions/59380824/how-to-choose-a-python-interpreter-for-ansible-playbook

Set python interpreter as an extra argument

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

Resource: https://github.com/cisagov/kali-packer/blob/develop/src/packer.json

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

https://docs.ansible.com/ansible/2.5/user_guide/playbooks_best_practices.html

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.

Resource: https://stackoverflow.com/questions/22201306/ansible-galaxy-roles-install-in-to-a-specific-directory

Collections

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
  roles:
    - name: geerlingguy.docker

  collections:
    - name: community.docker
      source: https://galaxy.ansible.com

You can trigger installing any collections listed with:

ansible-galaxy collection install -r requirements.yml

Resources:
https://docs.ansible.com/ansible/latest/reference_appendices/config.html#collections-paths - collections docs
https://www.jeffgeerling.com/blog/2020/ansible-best-practices-using-project-local-collections-and-roles - best practices for collections
https://stackoverflow.com/questions/25230376/how-to-automatically-install-ansible-galaxy-roles - 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:

[defaults]
host_key_checking = False

Resource: https://stackoverflow.com/questions/32297456/how-to-ignore-ansible-ssh-authenticity-checking

Vault

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:

r3allyCompl3xp@ssw0rd!thatislongandrandomandallthegoodthings

Encrypt a file

ansible-vault encrypt file 

Decrypt a file

ansible-vault decrypt file

Resources:
https://docs.ansible.com/ansible/latest/user_guide/vault.html#encrypt-string-for-use-in-yaml
https://docs.ansible.com/ansible/latest/user_guide/playbooks_vault.html

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
  template:
    src=templates/template
    dest=/home/user/file

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

Resource: https://stackoverflow.com/questions/37682928/ansible-un-vault-and-template-a-file

Hosts file priority

<current directory>/hosts
/home/<user>/.ansible/hosts
/etc/ansible/hosts

Resource:
https://www.quora.com/What-is-the-difference-between-host-and-inventory-file-in-ansible

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:

[defaults]
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"

Resource: https://docs.ansible.com/ansible/latest/user_guide/playbooks_vars_facts.html

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:

[ssh_connection]
retries=2

Resource: https://stackoverflow.com/questions/40340761/is-it-possible-to-have-ansible-retry-on-connection-failure

Fix SSH UNREACHABLE Connection Error

Add the timeout field to the ansible.cfg file:

[defaults]
timeout = 25

Resource: https://stackoverflow.com/questions/41885326/ansible-playbook-setup-gather-facts-ssh-unreachable-connection-timed-out-dur

List managed hosts

ansible all --list-hosts

Resource: https://www.tecmint.com/install-and-configure-an-ansible-control-node/

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 }}"

Resource: https://stackoverflow.com/questions/53098493/how-to-nicely-split-on-multiple-lines-long-conditionals-with-or-on-ansible

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([]) }}"

Resource: https://serverfault.com/questions/805576/how-to-assign-an-empty-value-to-a-variable-in-ansible

Store output of command across multiple roles

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

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

Resources:
https://serverfault.com/questions/795026/ansible-run-global-variable - gave me the idea initially
https://docs.ansible.com/ansible/latest/collections/ansible/builtin/set_fact_module.html - 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:

vars/main.yml:

users:
  - 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.

tasks/main.yml:

- name: Set vnc_users fact to be used later
  set_fact:
    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:
https://github.com/sdarwin/Ansible-VNC/blob/master/vars/Debian-10.yml - where I got the example code from
https://docs.ansible.com/ansible/devel/collections/ansible/builtin/password_lookup.html - how to generate a random password in ansible
https://stackoverflow.com/questions/38143647/set-fact-with-dynamic-key-name-in-ansible - what inspired my original idea
https://stackoverflow.com/questions/57776903/how-to-split-multiple-filters-into-multiple-lines-with-ansible - 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
  set_fact: 
    # Split on newlines
    users: "{{ users.stdout.split('\n') }}"
    cacheable: yes

Resource: https://gist.github.com/VerosK/9853931

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
  group:
    name: "{{ item }}"
    state: present
  loop: "{{ users }}"

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

Resource: https://stackoverflow.com/questions/62358569/ansible-create-users-and-group

Remove trailing and leading quotes from var

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

Resource: https://firxworx.com/blog/it-devops/sysadmin/using-ansibles-regex_replace-filter-to-strip-leading-and-trailing-slashes-from-strings/

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
  set_fact:
    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
  set_fact:
    users: "{{ users | map('regex_replace', '\\.', '_') | list }}"
    cacheable: yes

Resources:
https://stackoverflow.com/questions/39545195/match-literals-with-regex-replace-ansible-filter - how to regex when a .
https://stackoverflow.com/questions/60461281/replace-multiple-patterns-with-multiple-values-in-ansible - how to use multiple regexs in a single play
https://www.reddit.com/r/ansible/comments/91wko8/modify_each_element_in_a_list_varible/ - gave me the initial idea

Merge Two lists into list of dictionaries

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

Resource: https://stackoverflow.com/questions/51898227/ansible-how-to-combine-two-separate-lists-into-a-list-of-dictionaries

Get ansible facts and output to a file

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

Resource: https://docs.ansible.com/ansible/latest/user_guide/playbooks_vars_facts.html

Useful tidbits

Reference ansible directory:

"{{ ansible_env.HOME }}"

Template path example:

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

Resource: https://www.middlewareinventory.com/blog/ansible-template-module-example/

Debug

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:
https://docs.ansible.com/ansible/2.4/debug_module.html
https://stackoverflow.com/questions/36451793/how-do-i-exit-ansible-play-without-error-on-a-condition - how to end the play without throwing an error

Check for availability of role

ansible-doc community.docker

Resource: https://www.ansible.com/blog/hands-on-with-ansible-collections

Run role as specific user

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

Resources:
https://stackoverflow.com/questions/39183100/define-become-yes-per-role-with-ansible
https://docs.ansible.com/ansible/latest/user_guide/become.html - 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
  roles:
     - 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
  roles:
     - { 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:
https://stackoverflow.com/questions/42883909/passing-variables-to-ansible-roles - nicer syntax
https://stackoverflow.com/questions/39183100/define-become-yes-per-role-with-ansible - 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
  vars:
    ansible_distribution: 'debian'
    ansible_distribution_release: 'buster'
    ansible_os_family: 'Debian'
  roles:
    - { 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
  lineinfile:
    dest=/etc/ssh/sshd_config
    regexp='^PermitRootLogin'
    line="PermitRootLogin no"
    state=present
    backup=yes
  notify:
    - restart ssh
    
handlers:
  - name: restart ssh
    service:
      name=sshd
      state=restarted

Resource: https://devops4solutions.medium.com/setup-ssh-key-and-initial-user-using-ansible-playbook-61eabbb0dba4

Create SSH key for a user on a managed node

roles/ssh/tasks/main.yml:

---
- name: Generate SSH key
  openssh_keypair:
    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
  file:
    path: "{{ ssh_key_path }}"
    owner: "{{ user }}"
    group: "{{ user }}"
    mode: 0600

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

roles/ssh/vars/main.yml:

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

Resources:
https://docs.ansible.com/ansible/latest/collections/community/crypto/openssh_keypair_module.html - how to create an openssh keypair

Include variables file based on OS info

Folder structure:

role_name
├── 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:
https://github.com/sdarwin/Ansible-VNC/blob/master/tasks/main.yml - example playbook
https://raymii.org/s/tutorials/Ansible_-_Only_if_on_specific_distribution_or_distribution_version.html - 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:

vars/main.yml:

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

tasks/main.yml:

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

Resource: https://github.com/sdarwin/Ansible-VNC/blob/master/vars/Debian-10.yml

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.

test.py:

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
print(list_of_dicts)

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 test.py:
library/merge_list_of_dicts_w_list.py:

#!/usr/bin/python3

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(
        argument_spec=module_args,
        supports_check_mode=True
    )

    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():
    run_module()

if __name__ == '__main__':
    main()

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

- name: Merge uids into vnc_users
  merge_list_of_dicts_w_list:
    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 }}"

Resource:
https://stackoverflow.com/questions/47018363/ansible-updating-values-in-a-list-of-dictionaries - the solution I finally got to thanks to an unsung hero by the name of larsks.
https://docs.ansible.com/ansible/2.3/dev_guide/developing_modules_general.html - how to build a simple module
https://docs.ansible.com/ansible/2.4/dev_guide/developing_modules_general.html - general guidance on developing modules

Install packages

vars/main.yml:

install_packages:
  - curl
  - jq
  - tmux

tasks/main.yml:

- name: Install dependencies
  apt:
    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"

Resource: https://github.com/sdarwin/Ansible-VNC/blob/master/tasks/main.yml

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 (' ') }}

Resource: https://stackoverflow.com/questions/47244834/how-to-join-a-list-of-strings-in-ansible

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
  copy:
    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):

[Unit]
Description=Remote desktop service (VNC)
After=syslog.target network.target

[Service]
Type=forking
PAMName=login
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 }}

[Install]
WantedBy=default.target

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

- name: Update per-user systemd service files
  template:
    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) }}"
  when:
    - vnc_ansible_managed_startup_scripts or not item.stat.exists
  with_items: "{{ checksystemd.results }}"

Resources:
https://github.com/sdarwin/Ansible-VNC/blob/master/tasks/systemd.yml - how the template is called
https://github.com/sdarwin/Ansible-VNC/blob/master/templates/vncserver.j2 - example template file

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

Resource: https://github.com/sdarwin/Ansible-VNC/blob/master/tasks/main.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: https://docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html

Resources:
https://docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html - official docs
https://stackoverflow.com/questions/29127560/whats-the-difference-between-defaults-and-vars-in-an-ansible-role - a discussion that helped me to understand what was going on

Check if file exists

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

Resource: https://phoenixnap.com/kb/ansible-check-if-file-exists

Delete file if it exists

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

Resource: https://www.toptechskills.com/ansible-tutorials-courses/ansible-file-module-tutorial-examples/

Clone git repo

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

Resource: https://docs.ansible.com/ansible/2.3/git_module.html

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
  git:
    repo: "{{ repo_url }}"
    dest: "{{ clone_location }}"

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

Resource: https://stackoverflow.com/questions/51196854/how-to-generate-https-git-credentials-for-aws-codecommit-using-terraform

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
  pip:
    name: docker

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

- name: Create and start services
  docker_compose:

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

Resources:
https://docs.ansible.com/ansible/2.4/pip_module.html - pip with ansible
https://docs.ansible.com/ansible/latest/collections/community/docker/docker_compose_module.html#ansible-collections-community-docker-docker-compose-module - docs for the docker_compose ansible stuff
https://thenewstack.io/tutorial-use-ansible-collections-to-help-configure-and-manage-more-complex-systems/ - 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 }}"
  template:
    src: env.j2
    dest: "{{ docker_compose_deployment_location }}/.env"
  vars:
    password: "{{ lookup('password', '/dev/null chars=ascii_letters,digits,punctuation length=50') }}"

Resources: https://docs.ansible.com/ansible/devel/collections/ansible/builtin/password_lookup.html - creating passwords in ansible
https://www.mydailytutorials.com/ansible-template-module-examples/ - how to specify a variable to put into a template

Templates

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 %}
    postrotate
{% if ansible_facts['os_family'] == "Debian" %}
        if [ -f /var/run/nginx.pid ]; then
            kill -USR1 `cat /var/run/nginx.pid`
        fi
{% else %}
        nginx -s reopen
{% endif %}
    endscript
}

Resources:
https://github.com/nginxinc/ansible-role-nginx/blob/main/templates/logrotate/nginx.j2 - example used
https://stackoverflow.com/questions/26989492/ansible-loop-through-group-vars-in-template - 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:

/etc/ansible/
├── ansible.cfg
├── filter_plugins
│   └── get_user.py
├── hosts
├── roles

get_user.py:

#!/usr/bin/python3
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.info }}
{{ user.birthdate }}

Development

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

Debugging

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

Resources:
https://www.dasblinkenlichten.com/creating-ansible-filter-plugins/ - how to create plugins
https://stackoverflow.com/questions/3727045/set-variable-in-jinja - how to set a variable

Run bash script

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

Get current host's IP address

{{ ansible_ssh_host }}

Resource: https://stackoverflow.com/questions/39819378/ansible-get-current-target-hosts-ip-address