Installation on Centos 8

sudo dnf makecache
sudo dnf install -y epel-releasesudo dnf makecache
sudo dnf install -y ansible

Resource: https://linuxhint.com/install_ansible_centos8


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() {
  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

Install Ansible on Red Hat, Fedora, or CentOS

dnf -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm
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:

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

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

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]
host.domain ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/id_rsa.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]
host.domain.com ansible_user=kali ansible_ssh_private_key_file=~/.ssh/id_rsa.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

Create new collection

Ansible Collections is a way of packaging and distributing playbooks, roles, modules, and plugins.

Here is a guide on how you can start creating an Ansible Collection:

Install Ansible: Make sure you have installed the latest version of Ansible. You can install Ansible using pip:

python3 -m pip install ansible

Create a Collection: Use the ansible-galaxy command-line tool which comes with Ansible, to create a new collection structure:

ansible-galaxy collection init namespace.collection_name

Replace namespace with your own namespace, and collection_name with your desired collection name. This command will create a new directory structure like this:

namespace/
└── collection_name/
    ├── docs/
    ├── galaxy.yml
    ├── plugins/
    │   ├── modules/
    │   ├── inventory/
    │   └── ...
    ├── README.md
    └── roles/
  • docs/: Local documentation for the collection.
  • galaxy.yml: The file contains all the metadata about the collection (author, support level, tags, etc.).
  • plugins/: Directory for Ansible plugins.
  • README.md: A README file containing information about the collection.
  • roles/: Directory for Ansible roles.

Move or Copy Your Roles and Playbooks: Now you can start moving your existing roles and playbooks into the new collection structure. Make sure you maintain the recommended structure of roles within Ansible.

Update Collection Metadata: Edit the galaxy.yml file to reflect the metadata of your collection. It is important because it provides information about the collection when publishing or sharing.

Build the Collection: Use the following command to build your collection:

ansible-galaxy collection build

This command will create a .tar.gz file which can be uploaded to Ansible Galaxy.

Publish the Collection: If you wish to share your collection, you can upload it to Ansible Galaxy with the following command:

ansible-galaxy collection publish ./namespace-collection_name-1.0.0.tar.gz

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:


Molecule

Create role

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

ROLE_NAME=l50.mysweetrole

molecule init role "${ROLE_NAME}"

Older versions of molecule:

molecule init role --role-name "${ROLE_NAME}"

Create test environment

molecule create

List test environments

molecule list

Run test

molecule converge

# Verbose output:
molecule -vvv converge

Destroy test environment

molecule destroy

Test format

There are a lot of ways to skin this cat. I appreciate the format that geerlingguy uses in his projects. It’s simple and gets the job done:

molecule
└── default
    ├── converge.yml
    └── molecule.yml

The verification piece happens in the post_tasks section of converge.yml:

post_tasks:
  - name: Test that package is installed
    ansible.builtin.stat:
      # Read from `vars/main.yml`:
      path: "{{ package_path }}"
    become: true

Resource: https://github.com/geerlingguy/ansible-role-postgresql

Fix unallowed value docker

To fix this error:

ERROR: Failed to pre-validate.
{'driver': [{'name': ['unallowed value docker']}]}

run this command:

pip3 install molecule-docker

Resource: https://github.com/ansible-community/molecule/issues/2891

Fix idempotency failures

Add the following to a block of code that’s failing the molecule idempotency check:

changed_when: false

Resource: https://stackoverflow.com/questions/64855374/how-to-delete-files-inside-a-directory-without-deleting-the-directory-with-ansib


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

"$(pwd)/hosts"
"${HOME}/.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:

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

Resources:

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:

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

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:

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:

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:

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

Resource:

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:

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: "{{ ansible_facts['ansible_user_id'] }}"
    usergroup: "{{ ansible_facts['ansible_user_id'] }}"
    sudo: true
    # port 5901
    vnc_num: 1
  - username: "someuser"
    usergroup: "someuser"
    sudo: true
    # port 5902
    vnc_num: 2

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

Resources:

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:

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 Precedence for roles

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

A full precedence list can be found here: https://docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html

Resources:

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

When bool is true or false

- name: Configure systemd auto-start service
  include: systemd.yaml
    when: var.setup_systemd is true
    #when: var.setup_systemd is false

Resource: https://stackoverflow.com/questions/51930287/ansible-condition-with-and-and-boolean-variable-not-working

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:


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:


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:

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:


Package Management

Install pip packages

roles/pipexample/tasks/main.yml:

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

roles/pipexample/vars/main.yml:

pip_packages:
  - package_name
  - another_package_name

Install gems

roles/gemexample/tasks/main.yml:

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

roles/gemexample/vars/main.yml:

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 some_script.sh
  args:
    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 } }

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

Get value for $HOME

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

Resource: https://stackoverflow.com/questions/43126400/ansible-host-how-to-get-the-value-for-home-variable

Query metadata

roles/ip/tasks/main.yml:

---
- name: Get aws public IP
  uri:
    method: GET
    url: "http://169.254.169.254/latest/meta-data/public-ipv4"
    return_content: yes
  register: uri_output

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

Output to a template

roles/ip/tasks/main.yml:

---
- name: Get aws public IP
  uri:
    method: GET
    url: "http://169.254.169.254/latest/meta-data/public-ipv4"
    return_content: yes
  register: uri_output

- name: Public IP to template
  template:
    src: ip.j2
    dest: "/tmp/pub_ip.txt"
  vars:
    public_ip: "{{ uri_output.content | regex_replace('\n','') }}"

ip.j2:

PUBLIC_IP={{ public_ip }}

Resources:

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
  lineinfile:
    path: /etc/zsh/zshrc
    line: export PATH=$PATH:/usr/share/metasploit-framework/tools/exploit
  when: ansible_distribution_release == "kali-rolling"

Resource: https://www.mydailytutorials.com/ansible-add-line-to-file/

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:


Download and extract zip

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

Resource: https://docs.ansible.com/ansible/2.5/modules/unarchive_module.html

Value to upper

{ { ansible_hostname|upper } }

Value to lower

{ { ansible_hostname|lower } }

Resource: https://serverfault.com/questions/677683/convert-value-of-an-ansible-variable-from-lower-case-to-upper-case


Download and install deb package

This particular example will install the Amazon SSM agent on a debian system.

- name: Download and install SSM Agent
  apt:
    deb: https://s3.{{ region }}.amazonaws.com/amazon-ssm-{{ region }}/latest/{{
      ansible_distribution|lower }}_amd64/amazon-ssm-agent.deb

Resource: https://stackoverflow.com/questions/22939775/ansible-and-wget


Append template to existing file

- blockinfile:
    insertafter: EOF
    path: ce_hostname.conf
    block: "{{ lookup('template', 'nokia_t1_port.j2') }}"
    marker: ""

Resource: https://stackoverflow.com/questions/63725020/ansible-template-append-to-file

Get lsb release

This is equivalent to the lsb_release -sc bash command:

- name: Add {{ ansible_distribution_release }} PPA repository for Mozilla Firefox
  ansible.builtin.apt_repository:
    repo: ppa:mozillateam/ppa
    codename: "{{ ansible_distribution_release }}"

Resource: https://www.shellhacks.com/ansible-lsb_release-variable/

Set the verbosity with env vars

export ANSIBLE_VERBOSITY=4
export MOLECULE_DEBUG=1

Profile slow tasks via molecule

Add the following to molecule/default/molecule.yml:

env:
  ANSIBLE_CALLBACK_PLUGINS: "${MOLECULE_SCENARIO_DIRECTORY}/callback_plugins"

For example:

provisioner:
  name: ansible
  playbooks:
    converge: ${MOLECULE_PLAYBOOK:-converge.yml}
  env:
    ANSIBLE_CALLBACK_PLUGINS: "${MOLECULE_SCENARIO_DIRECTORY}/callback_plugins"

Use Ansible with AWS SSM for Windows Instances

When using Ansible to manage Windows instances on AWS via Systems Manager (SSM), you might encounter some challenges. Here’s how to set it up correctly:

Inventory File (windows_aws_ec2.yaml)

plugin: aws_ec2
regions:
  - us-west-1
hostnames:
  - tag:Name
  - instance-id
filters:
  instance-id: i-1234567890abcdef0
  "tag:Name":
    - "*windows*"
  "tag:Environment":
    - "prod"
keyed_groups:
  - key: tags.Name
  - key: tags.Environment
compose:
  ansible_host: instance_id

Playbook File (windows_playbook.yaml)

---
- hosts: all
  gather_facts: false
  vars:
    ansible_connection: aws_ssm
    ansible_aws_ssm_bucket_name: your-bucket-name-here # Be sure to replace this with your bucket name
    ansible_aws_ssm_region: us-west-1
    ansible_shell_type: powershell
  tasks:
    - name: Wait for system to become reachable
      wait_for_connection:
        timeout: 60

    - name: Echo instance ID and name
      ansible.windows.win_shell: |
        Write-Output "Running on instance $env:COMPUTERNAME"
        Write-Output "Instance ID: {{ ansible_host }}"        
      register: echo_result

    - name: Display echo result
      debug:
        var: echo_result.stdout_lines

Key Points

  1. In the inventory file, use compose: ansible_host: instance_id to ensure SSM uses the correct instance ID format.
  2. In the playbook, set ansible_connection: aws_ssm to use SSM for connections.
  3. Specify the S3 bucket name and region for SSM operations.
  4. Use ansible_shell_type: powershell for Windows instances.
  5. Include a wait_for_connection task to ensure the system is reachable before running commands.
  6. Use ansible.windows.win_shell for running PowerShell commands on Windows instances.

Running the Playbook

Run the playbook with increased verbosity for debugging:

ansible-playbook -i ./windows_aws_ec2.yaml ./windows_playbook.yaml -vvv

Troubleshooting

If you encounter issues:

  1. Ensure the IAM role for your EC2 instance has necessary SSM permissions.

  2. Verify the SSM agent is installed and running on your Windows instance.

  3. Try bypassing host key checking:

    ansible-playbook -i ./windows_aws_ec2.yaml ./windows_playbook.yaml -vvv --ssh-extra-args="-o StrictHostKeyChecking=no"
    
  4. Test SSM connectivity manually:

    aws ssm start-session --target i-00abab71f33038568 --region us-west-1
    

Resource: Ansible AWS Guide