Files
oam/knowledge base/ansible.md
2025-03-16 11:59:19 +01:00

60 KiB
Raw Blame History

Ansible

  1. TL;DR
  2. Configuration
    1. Performance tuning
  3. Inventories
    1. AWS
    2. Patterns
  4. Templating
    1. Tests
    2. Loops
  5. Validation
    1. Assertions
  6. Asynchronous actions
    1. Run tasks in parallel
  7. Error handling
    1. Using blocks
  8. Output formatting
  9. Handlers
  10. Roles
    1. Get roles
    2. Assign roles
    3. Role dependencies
  11. Create custom filter plugins
  12. Execution environments
    1. Build execution environments
  13. Ansible Navigator
    1. Navigator configuration files
  14. Secrets management
    1. Ansible Vault
  15. Best practices
  16. Troubleshooting
    1. ERROR: Ansible could not initialize the preferred locale: unsupported locale setting
    2. Print all known variables
    3. Force notified handlers to run at a specific point
    4. Time tasks execution
    5. Run specific tasks even in check mode
    6. Dry-run only specific tasks
    7. Set up recursive permissions on a directory so that directories are set to 755 and files to 644
    8. Only run a task when another has a specific result
    9. Define when a task changed or failed
    10. Set environment variables for a play, role or task
    11. Set variables to the value of environment variables
    12. Check if a list contains an item and fail otherwise
    13. Define different values for true/false/null
    14. Force a task or play to use a specific Python interpreter
    15. Provide a template file content inline
    16. Python breaks in OS X
    17. Load files' content into variables
    18. Only run a task when explicitly requested
    19. Using AWS' SSM with Ansible fails with error Failed to create temporary directory
    20. Future feature annotations is not defined
    21. Boolean variables given from the CLI are treated as strings
  17. Further readings
    1. Sources

TL;DR

Setup
# Install.
pip3 install --user 'ansible'
brew install 'ansible' 'sshpass'         # darwin
sudo pamac install 'ansible' 'sshpass'   # manjaro linux

# Generate example configuration files with entries disabled.
ansible-config init --disabled > 'ansible.cfg'
ansible-config init --disabled -t 'all' > ~/'.ansible.cfg'

# Show the current configuration.
ansible-config dump
Usage
# List hosts.
ansible-inventory -i 'inventory' --list
ansible-playbook -i 'inventory' 'playbook.yml' --list-hosts
ansible -i 'inventory' all --list-hosts

# Check the syntax of a playbook.
# This will *not* execute the plays inside it.
ansible-playbook 'path/to/playbook.yml' --syntax-check

# Execute playbooks.
ansible-playbook 'path/to/playbook.yml' -i 'hosts.list'
ansible-playbook … -i 'host1,host2,hostN,' -l 'hosts,list'
ansible-playbook … -i 'host1,host2,other,' -l 'hosts-pattern' --step
ansible-playbook … -e 'someKey=someValue someOtherKey=someOtherValue' -e 'extraKey=extraValue'
ansible-playbook … -e '{ "boolean_value_requires_json_format": true, "some_list": [ true, "someString" ] }'

# Show what changes (with details) a play would apply to the local machine.
ansible-playbook 'path/to/playbook.yml' -i 'localhost,' -c 'local' -vvC

# Only execute tasks with specific tags.
ansible-playbook 'path/to/playbook.yml' --tags 'configuration,packages'
ansible-playbook -i 'localhost,' -c 'local' -Dvvv 'playbook.yml' -t 'container_registry' --ask-vault-pass

# Avoid executing tasks with specific tags.
ansible-playbook 'path/to/playbook.yml' --skip-tags 'system,user'

# Check what tasks will be executed.
ansible-playbook 'path/to/playbook.yml' --list-tasks
ansible-playbook … --list-tasks --tags 'configuration,packages'
ansible-playbook … --list-tasks --skip-tags 'system,user'

# Debug playbooks.
ANSIBLE_ENABLE_TASK_DEBUGGER=True ansible-playbook …

# Record how much time tasks take.
ANSIBLE_CALLBACKS_ENABLED='profile_tasks' ansible-playbook …

# Encrypt data using Vault.
ansible-vault encrypt_string --name 'command_output' 'somethingNobodyShouldKnow'
ansible-vault encrypt '.ssh/id_rsa' --vault-password-file 'password_file.txt'
ANSIBLE_VAULT_PASSWORD_FILE='password_file.txt' ansible-vault encrypt --output 'ssh.key' '.ssh/id_rsa'

# Print out decoded contents of files encrypted with Vault.
ansible-vault view 'ssh.key.pub'
ansible-vault view 'ssh.key.pub' --vault-password-file 'password_file.txt'

# Edit decoded contents of files encrypted with Vault.
ANSIBLE_VAULT_PASSWORD='abracadabra' ansible-vault edit 'ssh.key.pub'
ansible-vault edit 'ssh.key.pub' --vault-password-file 'password_file.txt'

# Decrypt files encrypted with Vault.
ansible-vault decrypt 'ssh.key'
ansible-vault decrypt --output '.ssh/id_rsa' --vault-password-file 'password_file.txt' 'ssh.key'

# List roles installed from Galaxy.
ansible-galaxy list

# Install roles from Galaxy.
ansible-galaxy install 'namespace.role'
ansible-galaxy install --roles-path 'path/to/ansible/roles' 'namespace.role'
ansible-galaxy install 'namespace.role,v1.0.0'
ansible-galaxy install 'git+https://github.com/namespace/role.git,commit-hash'
ansible-galaxy install -r 'requirements.yml'

# Create new roles.
ansible-galaxy init 'role_name'
ansible-galaxy role init --type 'container' --init-path 'path/to/role' 'name'

# Remove roles installed from Galaxy.
ansible-galaxy remove 'namespace.role'
Real world use cases
# Show hosts' ansible facts.
ansible -i 'path/to/hosts/file' -m 'setup' all
ansible -i 'host1,hostN,' -m 'setup' 'host1' -u 'remote-user'
ansible -i 'localhost,' -c 'local' -km 'setup' 'localhost'

# Execute locally using Ansible from the virtual environment in the current directory.
venv/bin/python3ansible -i 'localhost ansible_python_interpreter=venv/bin/python3,' -c 'local' \
  -m 'ansible.builtin.copy' -a 'src=/tmp/src' -a 'dest=/tmp/dest' 'localhost'

# Check the Vault password file is correct.
diff 'path/to/plain/file' <(ansible-vault view --vault-password-file 'password_file.txt' 'path/to/vault/encrypted/file')

# Use AWS SSM for connections.
ansible-playbook 'playbook.yaml' -DCvvv \
  -e 'ansible_aws_ssm_plugin=/usr/local/sessionmanagerplugin/bin/session-manager-plugin ansible_connection=aws_ssm' \
  -e 'ansible_aws_ssm_bucket_name=ssm-bucket ansible_aws_ssm_region=eu-west-1' \
  -e 'ansible_remote_tmp=/tmp/.ansible-\${USER}/tmp' \
  -i 'i-0123456789abcdef0,'

Galaxy collections and roles worth a check:

ID Type Description
sivel.toiletwater collection Extra filters, mostly

UIs:

UI Static inventories Dynamic inventories
AWX
Rundeck ?
Semaphore
Zuul ? ?

Configuration

Ansible can be configured using INI files named ansible.cfg, environment variables, command-line options, playbook keywords, and variables.

The ansible-config utility allows to see all the configuration settings available, their defaults, how to set them and where their current value comes from.

Ansible will process the following list and use the first file found; all the other files are ignored even if existing:

  1. the ANSIBLE_CONFIG environment variable;
  2. the ansible.cfg file in the current directory;
  3. the ~/.ansible.cfg file in the user's home directory;
  4. the /etc/ansible/ansible.cfg file.

Generate a fully commented-out example of the ansible.cfg file:

ansible-config init --disabled > 'ansible.cfg'

# Includes existing plugins.
ansible-config init --disabled -t all > 'ansible.cfg'

Performance tuning

Refer the following:

Suggestions:

  • Optimize fact gathering:

    • Disable fact gathering when not used.

    • Consider using smart fact gathering:

      [defaults]
      gathering = smart
      fact_caching = jsonfile
      fact_caching_connection = /tmp/ansible/facts.json  ; /tmp/ansible to use the directory and have a file per host
      fact_caching_timeout = 86400
      
    • Only gather subsets of facts:

      - name: Play with selected facts
        gather_facts: true
        gather_subset:
          - '!all'
          - '!min'
          - system
      

      Refer the setup module for more information, and the setup module source code for available keys.

  • Consider increasing the number of forks when dealing with lots of managed hosts:

    [defaults]
    forks = 25
    
  • Set independent tasks as async.

  • Optimize SSH connections:

    • Prefer key-based authentication if used:

      [ssh_connection]
      ssh_args = -o PreferredAuthentications=publickey
      
    • Use pipelining:

      [ssh_connection]
      pipelining = True
      
    • Consider using multiplexing:

      [ssh_connection]
      ssh_args = -o ControlMaster=auto -o ControlPersist=3600s
      
  • Consider installing and using the Mitogen plugin on the controller:

    curl -fsLO 'https://github.com/mitogen-hq/mitogen/releases/download/v0.3.7/mitogen-0.3.7.tar.gz'
    tar -xaf 'mitogen-0.3.7.tar.gz'
    
    [defaults]
    strategy_plugins = mitogen-0.3.7/ansible_mitogen/plugins/strategy
    strategy = mitogen_linear
    

    Be advised that mitogen is not really supported by Ansible and has some issues with privilege escalation (1).

  • Improve the code:

    • Bundle up package installations together.
    • Beware of expensive calls.

Inventories

saturn ansible_python_interpreter=/usr/bin/python3.12 ansible_connection=local
jupiter.lan ansible_python_interpreter=/usr/bin/python3 ansible_port=4444

[accessed_remotely]
saturn
jupiter.lan
uranus.example.com ansible_port=5987

[swap_resistent]
jupiter.lan
saturn

[workstations]
saturn
; mars.lan ansible_port=4444

AWS

Refer Integrate with AWS SSM.

Patterns

Refer Patterns: targeting hosts and groups.

They allow to specify hosts and/or groups from the inventory. Ansible will execute on all hosts included in the pattern.

They can refer to a single host, an IP address, an inventory group, a set of groups, or all hosts.
One can exclude or require subsets of hosts, use wildcards or regular expressions, and more.

Use either a , or a : to separate lists of hosts.
The , is preferred when dealing with ranges and IPv6 addresses.

What Patterns Targets
Everything all, * All hosts
Single host fqdn, 192.168.1.1, localhost The single host directly identified by the pattern
Multiple hosts host1:host2, host1,host2 All hosts directly identified by the pattern
Single group webservers, tag_Application_Gitlab All hosts in the group identified by the pattern
Multiple groups webservers:dbservers All hosts in all groups identified by the pattern
Exclude groups webservers:!atlanta All hosts in the specified groups not identified by the negated pattern
Intersection of groups webservers:&staging All hosts present in all the groups identified by the pattern

One can use wildcard patterns with FQDNs or IP addresses, as long as the hosts are named in your inventory by FQDN or IP address.

Templating

Ansible leverages Jinja2 templating, which can be used directly in tasks or through the template module.

All Jinja2's standard filters and tests can be used, with the addition of:

  • specialized filters for selecting and transforming data
  • tests for evaluating template expressions
  • lookup plugins for retrieving data from external sources for use in templating

All templating happens on the Ansible controller, before the task is sent and executed on the target machine.

Updated examples are available.

# Remove empty or false values from a list piping it to 'select()'.
# Returns ["string"].
- vars:
    list: ["", "string", 0, false]
  ansible.builtin.debug:
    var: list | select

# Remove only empty strings from a list 'reject()'ing them.
# Returns ["string", 0, false].
- vars:
    list: ["", "string", 0, false]
  ansible.builtin.debug:
    var: list | reject('match', '^$')

# Merge two lists.
# Returns ["a", "b", "c", "d"].
- vars:
    list1: ["a", "b"]
    list2: ["c", "d"]
  ansible.builtin.debug:
    var: list1 + list2

# Dedupe elements in a list.
# Returns ["a", "b"].
- vars:
    list: ["a", "b", "b", "a"]
  ansible.builtin.debug:
    var: list | unique

# Sort a list by version number (not lexicographically).
# Returns ['2.7.0', '2.8.0', '2.9.0', '2.10.0' '2.11.0'].
- vars:
    list: ['2.8.0', '2.11.0', '2.7.0', '2.10.0', '2.9.0']
  ansible.builtin.debug:
    var: list | community.general.version_sort

# Generate a random password.
# Returns a random string following the specifications.
- vars:
    password: "{{ lookup('password', '/dev/null length=32 chars=ascii_letters,digits,punctuation') }}"
  ansible.builtin.debug:
    var: password

# Hash a password.
# Returns a hash of the requested type.
- vars:
    password: abcd
    salt: "{{ lookup('community.general.random_string', special=false) }}"
  ansible.builtin.debug:
    var: password | password_hash('sha512', salt)

# Get a variable's type.
- ansible.builtin.debug:
    var: "'string' | type_debug"

Tests

Return a boolean result.

# Compare semver version numbers.
- ansible.builtin.debug:
    var: "'2.0.0-rc.1+build.123' is version('2.1.0-rc.2+build.423', 'ge', version_type='semver')"

# Find specific values in JSON objects.
- ansible.builtin.command: ssm-cli get-diagnostics --output 'json'
  become: true
  register: diagnostics
  failed_when: diagnostics.stdout | to_json | community.general.json_query('DiagnosticsOutput[*].Status=="Failed"')

Loops

# Get the values of some special variables.
# See the 'Further readings' section for the full list.
- ansible.builtin.debug:
    var: "{{ item }}"
  with_items: ["ansible_local", "playbook_dir", "role_path"]

# Fail when any of the given variables is an empty string.
# Returns the ones which are empty.
- when: lookup('vars', item) == ''
  ansible.builtin.fail:
    msg: "The {{ item }} variable is an empty string"
  loop:
    - variable1
    - variableN

# Iterate through nested loops.
- vars:
    middles:
      - 'middle1'
      - 'middle2'
  ansible.builtin.debug:
    msg: "{{ item[0] }}, {{ item[1] }}, {{ item[2] }}"
  with_nested:
    - ['outer1', 'outer2']
    - "{{ middles }}"
    - ['inner1', 'inner2']

Validation

Assertions

- ansible.builtin.assert:
    that:
      - install_method in supported_install_methods
      - external_url is ansible.builtin.url
    fail_msg: What to say if any of the above conditions fail
    success_msg: What to say if all of the above conditions succeed

Asynchronous actions

Refer Asynchronous actions and polling.

Used to avoid connection timeouts and to run tasks concurrently.

Executing tasks in the background will return a Job ID that can be polled for information about that task.
Polling keeps the connection to the remote node open between polls.

Use the async keyword in playbook tasks.
Leaving it off makes tasks run synchronously, which is Ansible's default.

As of Ansible 2.3, async does not support check mode and tasks using it will fail when run in check mode.

Asynchronous tasks will create temporary async job cache file (in ~/.ansible_async/ by default).
When asynchronous tasks complete with polling enabled, the related temporary async job cache file is automatically removed. This does not happen for tasks that do not use polling.

# Execute long running operations asynchronously in the background.
ansible 'all' -B '3600' -P '0' -a '/usr/bin/long_running_operation --do-stuff'   # no polling
ansible 'all' -B '1800' -P '60' -a '/usr/bin/long_running_operation --do-stuff'  # with polling

# Check on background jobs' status.
ansible 'web1.example.com' -m 'async_status' -a 'jid=488359678239.2844'
---
- 
  tasks:
    - name: Simulate long running operation (15 sec), wait for up to 45 sec, poll every 5 sec
      ansible.builtin.command: /bin/sleep 15
      async: 45
      poll: 5

The default poll value is set by the DEFAULT_POLL_INTERVAL setting.
There is no default for async's time limit.

Asynchronous playbook tasks always return changed.

Run tasks in parallel

Use async with poll set to 0.
When poll is 0, Ansible starts the task and immediately moves on to the next one without waiting for a result from the first.
Each asynchronous task runs until it either completes, fails or times out (running longer than the value set for its async). Playbook runs end without checking back on asynchronous tasks.

---
- tasks:
    - name: Simulate long running op (15 sec), allow to run for 45 sec, fire and forget
      ansible.builtin.command: /bin/sleep 15
      async: 45
      poll: 0

Operations requiring exclusive locks, such as YUM transactions, will make successive operations that require those files wait or fail.

Synchronize asynchronous tasks by registering them to obtain their job ID and using it with the async_status module in later tasks:

- tasks:
    - name: Run an async task
      ansible.builtin.yum:
        name: docker-io
        state: present
      async: 1000
      poll: 0
      register: yum_sleeper
    - name: Check on an async task
      async_status:
        jid: "{{ yum_sleeper.ansible_job_id }}"
      register: job_result
      until: job_result.finished
      retries: 100
      delay: 10

Error handling

Using blocks

Refer Blocks.

- name: Error handling in blocks
  block:
    - name: This executes normally
      ansible.builtin.debug:
        msg: I execute normally
    - name: This errors out
      ansible.builtin.command: "/bin/false"
    - name: This never executes
      ansible.builtin.debug:
        msg: I never execute due to the above task failing
  rescue:
    - name: This executes if any errors arose in the block
      ansible.builtin.debug:
        msg: I caught an error and can do stuff here to fix it
  always:
    - name: This always executes
      ansible.builtin.debug:
        msg: I always execute

Output formatting

Introduced in Ansible 2.5

Change Ansible's output setting the stdout callback to json or yaml:

ANSIBLE_STDOUT_CALLBACK='yaml'
# ansible.cfg
[defaults]
stdout_callback = json

yaml will set tasks output only to be in the defined format:

$ ANSIBLE_STDOUT_CALLBACK='yaml' ansible-playbook --inventory='localhost,' 'localhost.configure.yml' -vv --check
PLAY [Configure localhost] *******************************************************************

TASK [Upgrade system packages] ***************************************************************
task path: /home/user/localhost.configure.yml:7
ok: [localhost] => changed=false
  cmd:
  - /usr/bin/zypper
  - --quiet
  - --non-interactive
  …
  update_cache: false

The json output format will be a single, long JSON file:

$ ANSIBLE_STDOUT_CALLBACK='json' ansible-playbook --inventory='localhost,' 'localhost.configure.yml' -vv --check
{
  "custom_stats": {},
  "global_custom_stats": {},
  "plays": [
    {
      "play": {"name": "Configure localhost"
      },
      "tasks": [
        {
          "hosts": {
            "localhost": {
              "action": "community.general.zypper",
              "changed": false,
              …
              "update_cache": false
            }
          }
        }
      ]
    }
  ]
}

Handlers

Blocks and import_tasks tend to make the handlers unreachable.

Instead of using blocks, give the same listen string to all involved handlers:

- - name: Block name
-   block:
-     - name: First task
-       …
-     - name: N-th task
-       …
+ - name: First task
+   listen: Block name
+   …
+ - name: N-th task
+   listen: Block name
+   …

Instead of using ìmport_tasks, use include_tasks:

  - name: First task
-   import_tasks: tasks.yml
+   include_tasks: tasks.yml

Handlers can notify other handlers:

- name: Configure Nginx
  ansible.builtin.copy: 
  notify: Restart Nginx

- name: Restart Nginx
  ansible.builtin.copy: 

Roles

Get roles

Roles can be either created:

ansible-galaxy init 'role-name'

or installed from Galaxy:

---
# requirements.yml
collections:
  - community.docker
ansible-galaxy install 'mcereda.boinc_client'
ansible-galaxy install --roles-path 'path/to/roles' 'namespace.role'
ansible-galaxy install 'namespace.role,v1.0.0'
ansible-galaxy install 'git+https://github.com/namespace/role.git,commit-hash'
ansible-galaxy install -r 'requirements.yml'

Assign roles

In playbooks:

---
- hosts: all
  roles:
    - web_server
    - geerlingguy.postgresql
    - role: /custom/path/to/role
      vars:
        var1: value1
      tags: example
      message: some message

Role assignments cannot be parallelized at the time of writing.

Role dependencies

Set them up in role/meta/main.yml:

---
dependencies:
  - role: common
    vars:
      some_parameter: 3
  - role: postgres
    vars:
      dbname: blarg
      other_parameter: 12

and/or in role/meta/requirements.yml:

---
collections:
  - community.dns

Create custom filter plugins

See Creating your own Ansible filter plugins.

Execution environments

Container images that can be used as Ansible control nodes.

Prefer using ansible-navigator to ansible-runner for local runs as the latter is a pain in the ass to use directly.

Commands example
pip install 'ansible-builder' 'ansible-runner' 'ansible-navigator'
ansible-builder build --container-runtime 'docker' -t 'example-ee:latest' -f 'definition.yml'
ansible-runner -p 'test_play.yml' --process-isolation --container-image 'example-ee:latest'
ansible-navigator run 'test_play.yml' -i 'localhost,' --execution-environment-image 'example-ee:latest' \
  --mode 'stdout' --pull-policy 'missing' --container-options='--user=0'

Build execution environments

Ansible Builder aids in the creation of Ansible Execution Environments.
Refer Introduction to Ansible Builder for how to build one.

Builders' build command defaults to using:

  • execution-environment.yml or execution-environment.yaml as the definition file.
  • $PWD/context as the directory to use for the build context.
execution-environment.yml example

Refer Execution environment definition.

---
version: 3

build_arg_defaults:
  ANSIBLE_GALAXY_CLI_COLLECTION_OPTS: '--pre'

dependencies:
  ansible_core:  # dedicated single-key dictionary
    package_pip: ansible-core==2.14.4
  ansible_runner:  # dedicated single-key dictionary
    package_pip: ansible-runner
  galaxy: requirements.yml
  python:  # pip packages
    - six
    - psutil
  system: bindep.txt
  exclude:
    python:
      - docker
    system:
      - python3-Cython

images:
  base_image:
    name: docker.io/redhat/ubi9:latest
    # Other available base images:
    #   - quay.io/rockylinux/rockylinux:9
    #   - quay.io/centos/centos:stream9
    #   - registry.fedoraproject.org/fedora:38
    #   - registry.redhat.io/ansible-automation-platform-23/ee-minimal-rhel8:latest
    #     (needs an account)

# Custom package manager path for the RHEL based images
# options:
#   package_manager_path: /usr/bin/microdnf

additional_build_files:
  - src: files/ansible.cfg
    dest: configs

additional_build_steps:
  prepend_base:
    - RUN echo This is a prepend base command!
    # Enable Non-default stream before packages provided by it can be installed. (optional)
    # - RUN $PKGMGR module enable postgresql:15 -y
    # - RUN $PKGMGR install -y postgresql
  prepend_galaxy:
    - COPY _build/configs/ansible.cfg /etc/ansible/ansible.cfg

  prepend_final: |
    RUN whoami
    RUN cat /etc/os-release
  append_final:
    - RUN echo This is a post-install command!
    - RUN ls -la /etc
requirements.yml example
---
collections:
  - redhat.openshift

Ansible Navigator

Refer Ansible Navigator documentation.

Settings for Navigator can be provided, in order of priority from highest to lowest:

  1. On the command line.
  2. Via environment variables.
  3. By specifying them in Navigator configuration files.
    Their own priority applies.

Environment variables inside Navigator's shell are set, in order of priority from highest to lowest:

  • From Passed environment variables, if the passed variable is set.
  • From environment variables set from the CLI (with --senv, --set-environment-variable).
  • From environment variables set in the evaluated config file (in ansible-navigator.execution-environment.environment-variables.set).

Volume mount paths must exist.

Navigator configuration files

File name and path can be specified via an environment variable, or it can be placed in one of two default directories.
It can be in the JSON or YAML format. JSON format files must end with the .json extension; YAML format files must end with the .yml or .yaml extension.

Navigator checks the following and uses the first that matches:

  1. The file name specified by the ANSIBLE_NAVIGATOR_CONFIG environment variable, if set.
  2. The ansible-navigator.<ext> file in the current directory.
    This must not be a dotfile.
  3. The .ansible-navigator.<ext> dotfile in the user's home directory.

The current and home directories can have only one settings file each.
Should more than one settings file be found in either directory, the program will error out.

File example
---
# refer <https://ansible.readthedocs.io/projects/navigator/settings/>.
# corresponds to `ansible-navigator --log-file='/dev/null' --container-options='--platform=linux/amd64'
#   --execution-environment-image='012345678901.dkr.ecr.eu-west-1.amazonaws.com/custom-ee' --pull-policy='missing'
#   --execution-environment-volume-mounts "$HOME/.aws:/runner/.aws:ro"
#   --pass-environment-variable 'ANSIBLE_VAULT_PASSWORD' --pass-environment-variable 'ANSIBLE_VAULT_PASSWORD_FILE'
#   --pass-environment-variable 'AWS_PROFILE' --pass-environment-variable 'AWS_REGION'
#   --pass-environment-variable 'AWS_DEFAULT_REGION' --set-environment-variable 'AWS_DEFAULT_REGION=eu-west-1'
#   run --enable-prompts …`
ansible-navigator:
  enable-prompts: true
  execution-environment:
    container-options:
      - --platform=linux/amd64
    image: 012345678901.dkr.ecr.eu-west-1.amazonaws.com/custom-ee
    pull:
      policy: missing
    volume-mounts:  # each must exist
      - src: ${HOME}/.aws
        dest: /runner/.aws
        options: ro
    environment-variables:  # pass from any > set from cli > set from conf
      pass:
        - ANSIBLE_VAULT_PASSWORD
        - ANSIBLE_VAULT_PASSWORD_FILE
        - AWS_DEFAULT_REGION
        - AWS_PROFILE
        - AWS_REGION
      set:
        AWS_DEFAULT_REGION: eu-west-1
  logging:
    file: /dev/null  # avoid leftovers
Commands
# Review the configuration
ansible-navigator settings --effective

# Check the Execution Environment's shell environment
ansible-navigator … exec -- set | sort
ansible-navigator … exec -- printenv | sort

Secrets management

Refer handling secrets in your Ansible playbooks.

Use interactive prompts to ask for values at runtime.

---
- hosts: all
  gather_facts: false
  vars_prompt:
    - name: api_key
      prompt: Enter the API key
  tasks:
    - name: Ensure API key is present in config file
      ansible.builtin.lineinfile:
        path: /etc/app/configuration.ini
        line: "API_KEY={{ api_key }}"

Use Ansible Vault for automated execution when one does not require to use specific secrets or password managers.

Ansible Vault

Refer Protecting sensitive data with Ansible Vault, Ansible Vault tutorial and Ansible Vault with AWX.

Vault encrypts variables and files at rest and allows for their use in playbooks and roles.
It does not prevent tasks to print out data in use. See the no_log attribute for hiding sensible values.

Protected data will require one or more passwords to encrypt and decrypt.
If storing vault passwords in third-party tools, one will need them need to allow for non-interactive access.

Create and view protected data by using the ansible-vault command.

Provide the Vault's password:

  • By using command line options.
    Make ansible ask for it using askvaultpass, or provide a file containing it with --vault-password-file:

    ansible … --ask-vault-pass
    ansible-playbook … --vault-password-file 'password_file.txt'
    
  • By exporting the ANSIBLE_VAULT_PASSWORD or ANSIBLE_VAULT_PASSWORD_FILE environment variables to specify the password itself or the location of the password file, respectively:

    ANSIBLE_VAULT_PASSWORD_FILE='password_file.txt' ansible …
    export ANSIBLE_VAULT_PASSWORD='abracadabra' ; ansible-playbook …
    
  • By using the ansible.cfg config file to either always prompt for the password, or to specify the default location of the password file:

    [defaults]
    vault_password_file = password_file.txt
    ; ask_vault_pass = True
    

    Should the password file be executable, Ansible will execute it and use its output as the password for Vault.
    This works well to integrate with CLI-capable password managers:

    # File 'password_file.sh'
    
    # Gopass
    gopass show -o 'ansible/vault'
    
    # Bitwarden CLI
    # bw login --check >/dev/null && bw get password 'ansible vault'
    

Vault passwords can be any string, and there is currently no special command to create one.
One must provide the/a Vault password every time one encrypts and/or decrypts data with Vault.
If using multiple Vault passwords, one can differentiate between them by means of vault IDs.

By default, Vault IDs only label protected content to remind one which password one used to encrypt it. Ansible will not check that the vault ID in the header of any encrypted content matches the vault ID one provides when using that content, and will try and decrypt the data with the password one provides.
Force this check by setting the DEFAULT_VAULT_ID_MATCH config option.

Vault can only encrypt variables and files.
Encrypted content is marked in playbook and roles with the !vault tag. This tells Ansible and YAML that the content needs to be decrypted. Content created with --vault-id also contains the vault ID's label in the mark.

Encrypted variables allow for mixed plaintext and encrypted content, even inline, in plays or roles.
One cannot rekey encrypted variables.
To encrypt tasks or other content, one must encrypt the entire file.

Input files are encrypted in-place unless one specifies the output files in the command.

Encrypt and use variables
  1. Encrypt the variable's value:

    $ ansible-vault encrypt_string --name 'command_output' 'somethingNobodyShouldKnow'
    New Vault password:
    Confirm New Vault password:
    Encryption successful
    command_output: !vault |
              $ANSIBLE_VAULT;1.1;AES256
              34306534613939316131303430653733633961623931363032633933393039373764356464623461
              3463353332623466623661363831303836396165323238660a353137363562393161396566386565
              35616662336536613365386164353439616232643131306534353264346635373566313630613261
              3531373034333830640a353138306463653533366432623438343266623930396238313763643836
              66646237336338353866306361316233326535333236363136613263346631633836
    
    $ ansible-vault encrypt_string --name 'command_output' 'somethingNobodyShouldKnow' \
        --vault-password-file 'password_file.txt'
    Encryption successful
    command_output: !vault |
              $ANSIBLE_VAULT;1.1;AES256
              31373465393164316666663963643163313032623233356634313038333662653061623936383838
              6166636433313438613338373438343130633766656535390a353338373261393931316533303837
              64363736383163643238336565363936303434393931386131383463336539306466636231633131
              6432396337366333350a356338623630626161333666373831313966633038343133316532383562
              61303538333031333861313733383363656531613333356364363432343361393636
    
  2. Use the output as the value:

    - name: Configure credential 'Gitlab container registry PAT'
      tags:
        - container_registry
        - gitlab
      awx.awx.credential:
        organization: Private
        name: Gitlab container registry PAT
        credential_type: Container Registry
        inputs:
          host: gitlab.example.org:5050
          username: awx  # or anything, really
          password: !vault |
            $ANSIBLE_VAULT;1.1;AES256
            34306534613939316131303430653733633961623931363032633933393039373764356464623461
            3463353332623466623661363831303836396165323238660a353137363562393161396566386565
            35616662336536613365386164353439616232643131306534353264346635373566313630613261
            3531373034333830640a353138306463653533366432623438343266623930396238313763643836
            66646237336338353866306361316233326535333236363136613263346631633836
          verify_ssl: false
        update_secrets: false
    
  3. Require the play execution to ask for the password used during encryption:

    ansible-playbook -i 'localhost,' -c 'local' -Dvvv 'playbook.yml' -t 'container_registry' --ask-vault-pass
    ansible-playbook … --vault-password-file 'password_file.txt'
    
Encrypt and use existing files
  1. Encrypt the file:

    # Input files are encrypted in place unless output files are specified
    $ ansible-vault encrypt 'ssh.key'
    New Vault password:
    Confirm New Vault password:
    Encryption successful
    
    $ ansible-vault encrypt --output 'ssh_key.enc' '.ssh/id_rsa' --vault-password-file 'password_file.txt'
    Encryption successful
    
  2. Use the file normally:

    - name: Test value is read correctly
      tags: debug
      ansible.builtin.debug:
        msg: "{{ lookup('file', 'ssh_key.enc') }}"
    
  3. Require the play execution to ask for the password used during encryption:

    ansible-playbook -i 'localhost,' -c 'local' -Dvvv 'playbook.yml' -t 'container_registry' --ask-vault-pass
    ansible-playbook … --vault-password-file 'password_file.txt'
    

Decrypt files with ansible-vault decrypt 'path/to/file'.
Input files are decrypted in place unless one specifies the output files in the command.

Decrypt files
$ ansible-vault decrypt 'ssh.key'
New Vault password:
Confirm New Vault password:
Decryption successful

$ ansible-vault decrypt --output '.ssh/id_rsa' --vault-password-file 'password_file.txt' 'ssh.key'
Decryption successful

One can quickly view the content of encrypted files with ansible-vault view 'path/to/file':

View encrypted files' content
$ cat 'ssh.key.pub'
$ANSIBLE_VAULT;1.1;AES256
38623265623763366431646435646634363136373831323464356130383432356266616461323730
6436396161613934356339323731336130383064386464610a373664326235376336333736306563
62366635646565633833336638616434353935313632323733326634356366666439316336353030
6635353335653034340a613330323565366365346638343464623036396134626537643064653437
36653734373839306135306165326464633231383236663735646465643332383332626564643038
64363531383430393834373764633564383537326430303038383661656134383631306336633539
33343166386135663537656262343734383339383363343736633965393262666133623932653732
63613034393964333865626532636332393964396463613131356534623433353065313661383461
37646635336433376132393766333761306162366666346634323166353630633036

$ ansible-vault view 'ssh.key.pub'
Vault password:
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFIw4vv6LYg3P7bfgrR5I4k/0123456789abcdefghIL me@example.org

$ ansible-vault view 'ssh.key.pub' --vault-password-file 'password_file.txt'
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFIw4vv6LYg3P7bfgrR5I4k/0123456789abcdefghIL me@example.org

Or even edit their content with ansible-vault edit 'path/to/file'.

Best practices

  • Tag all tasks somehow.

  • Define tasks so that playbook runs will not fail just because one task depends on another.

  • Provide ways to manually feed values to dependent tasks so that runs can start from there or only use tagged tasks, e.g. by using variables that can be overridden in the command line.

  • Consider using blocks to group tasks logically.

  • Keep debugging messages but set them to run only at higher verbosity:

    tasks:
      - debug:
          msg: "I always display!"
      - debug:
          msg: "I only display with ansible-playbook -vvv+"
          verbosity: 3
    
  • When replacing resources, if possible, make sure the replacement is set correctly before deleting the original.

  • If using other systems to maintain a canonical list of systems in one's infrastructure, consider using dynamic inventories.

Troubleshooting

ERROR: Ansible could not initialize the preferred locale: unsupported locale setting

ansible-core requires the locale to have UTF-8 encoding since 2.14.0:

ansible - At startup the filesystem encoding and locale are checked to verify they are UTF-8. If not, the process exits with an error reporting the errant encoding.

LANG='C.UTF-8' ansible …

Print all known variables

Print the special variable vars as a task:

- name: Debug all variables
  ansible.builtin.debug: var=vars

Force notified handlers to run at a specific point

Use the meta plugin with the flush_handlers option:

- name: Force all notified handlers to run at this point, not waiting for normal sync points
  ansible.builtin.meta: flush_handlers

Time tasks execution

Add profile_tasks the list of enable callbacks.

Choose one or more options:

  • Add it to callbacks_enabled in the [defaults] section of Ansible's configuration file:

    [defaults]
    callbacks_enabled = profile_tasks  # or ansible.posix.profile_tasks
    
  • Set the ANSIBLE_CALLBACKS_ENABLED environment variable:

    export ANSIBLE_CALLBACKS_ENABLED='profile_tasks'
    

Run specific tasks even in check mode

Add the check_mode: false pair to the task:

- name: this task will make changes to the system even in check mode
  check_mode: false
  ansible.builtin.command: /something/to/run --even-in-check-mode

Dry-run only specific tasks

Add the check_mode: true pair to the task:

- name: This task will always run under check mode and not change the system
  check_mode: true
  ansible.builtin.lineinfile:
    line: "important file"
    dest: /path/to/file.conf
    state: present

Set up recursive permissions on a directory so that directories are set to 755 and files to 644

Use the special X mode setting in the file plugin:

- name: Fix files and directories' permissions
  ansible.builtin.file:
    dest: /path/to/some/dir
    mode: u=rwX,g=rX,o=rX
    recurse: yes

Only run a task when another has a specific result

When a task executes, it also stores the two special values changed and failed in its results.
One can use those as conditions to execute the next ones:

- name: Trigger task
  ansible.builtin.command: any
  register: trigger_task
  ignore_errors: true

- name: Run only on change
  when: trigger_task.changed
  ansible.builtin.debug: msg="The trigger task changed"

- name: Run only on failure
  when: trigger_task.failed
  ansible.builtin.debug: msg="The trigger task failed"

Alternatively, you can use special checks built for this:

- name: Run only on success
  when: trigger_task is succeeded
  ansible.builtin.debug: msg="The trigger task succeeded"

- name: Run only on change
  when: trigger_task is changed
  ansible.builtin.debug: msg="The trigger task changed"

- name: Run only on failure
  when: trigger_task is failed
  ansible.builtin.debug: msg="The trigger task failed"

- name: Run only on skip
  when: trigger_task is skipped
  ansible.builtin.debug: msg="The trigger task skipped"

Define when a task changed or failed

This lets you avoid using ignore_errors.

Use the changed_when and failed_when attributes to define your own conditions:

- name: Task with custom results
  ansible.builtin.command: any
  register: result
  changed_when:
    - result.rc == 2
    - result.stderr | regex_search('things changed')
  failed_when:
    - result.rc != 0
    - not (result.stderr | regex_search('all good'))

Set environment variables for a play, role or task

Environment variables can be set at a play, block, or task level using the environment keyword:

- name: Use environment variables for a task
  environment:
    HTTP_PROXY: http://example.proxy
  ansible.builtin.command: curl ifconfig.io

The environment keyword does not affect Ansible itself or its configuration settings, the environment for other users, or the execution of other plugins like lookups and filters.
Variables set with environment do not automatically become Ansible facts, even when set at the play level.

Set variables to the value of environment variables

Use the lookup() plugin with the env option:

- name: Use a local environment variable
  ansible.builtin.debug: msg="HOME={{ lookup('env', 'HOME') }}"

Check if a list contains an item and fail otherwise

- name: Check if a list contains an item and fail otherwise
  when: item not in list
  ansible.builtin.fail: msg="item not in list"

Define different values for true/false/null

Create a test and define two values: the first will be returned when the test returns true, the second will be returned when the test returns false (Ansible 1.9+):

{{ (ansible_pkg_mgr == 'zypper') | ternary('gnu_parallel', 'parallel') }}

Since Ansible 2.8 you can define a third value to be returned when the test returns null:

{{ autoscaling_enabled | ternary(true, false, omit) }}

Force a task or play to use a specific Python interpreter

Just set it in the Play's or Task's variables:

vars:
  ansible_python_interpreter: /usr/local/bin/python3.9

Provide a template file content inline

Use the ansible.builtin.copy instead of ansible.builtin.template:

- name: Configure knockd
  ansible.builtin.copy:
    dest: /etc/knockd.conf
    content: |
      [options]
        UseSyslog

Python breaks in OS X

Root Cause:

Mac OS High Sierra and later versions have restricted multithreading for improved security.
Apple has defined some rules on what is allowed and not is not after forking processes, and have also added async-signal-safety to a limited number of APIs.

Solution:

Disable fork initialization safety features as shown in Why Ansible and Python fork break on macOS High Sierra+ and how to solve:

export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES

Load files' content into variables

For local files, use lookups:

user_data: "{{ lookup('file', 'path/to/file') }}"

For remote files, use the slurp module:

- ansible.builtin.slurp:
    src: "{{ user_data_file }}"
  register: slurped_user_data
- ansible.builtin.set_fact:
    user_data: "{{ slurped_user_data.content | ansible.builtin.b64decode }}"

The contents are presented as base64 string. The decode is needed.

Only run a task when explicitly requested

Leverage the never tag to never execute the task unless requested by using the --tags 'never' option:

- tags: never
  ansible.builtin.debug:
    msg: 

Conversely, one can achieve the opposite by using the always tag and the --skip 'always' option:

- tags: always
  ansible.builtin.command: 

Using AWS' SSM with Ansible fails with error Failed to create temporary directory

Message example:

fatal: [i-4ccab452bb7743336]: UNREACHABLE! => {
  "changed": false,
  "msg": "Failed to create temporary directory. In some cases, you may have been able to authenticate and did not have permissions on the target directory. Consider changing the remote tmp path in ansible.cfg to a path rooted in \"/tmp\", for more error information use -vvv. Failed command was: ( umask 77 && mkdir -p \"` echo \u001b]0;@ip-192-168-42-42:/usr/bin\u0007/home/centos/.ansible/tmp `\"&& mkdir \"` echo \u001b]0;@ip-192-168-42-42:/usr/bin\u0007/home/centos/.ansible/tmp/ansible-tmp-1708603630.2433128-49665-225488680421418 `\" && echo ansible-tmp-1708603630.2433128-49665-225488680421418=\"` echo \u001b]0;@ip-192-168-42-42:/usr/bin\u0007/home/centos/.ansible/tmp/ansible-tmp-1708603630.2433128-49665-225488680421418 `\" ), exited with result 1, stdout output: \u001b]0;@ip-192-168-42-42:/usr/bin\u0007bash: @ip-192-168-42-42:/usr/bin/home/centos/.ansible/tmp: No such file or directory\r\r\nmkdir: cannot create directory '0': Permission denied\r\r",
  "unreachable": true
}

Root cause:

By default, SSM starts sessions in the /usr/bin directory.

Solution:

Explicitly set Ansible's temporary directory to a folder the remote user can write to.
See Integrate with AWS SSM.

Future feature annotations is not defined

Refer Newer versions of Ansible don't work with RHEL 8.

Error message example:

SyntaxError: future feature annotations is not defined

Solution: use a version of ansible-core lower than 2.17.

Boolean variables given from the CLI are treated as strings

Refer defining variables at runtime.
Also see How can I pass variable to ansible playbook in the command line?.

Values passed in using the key=value syntax are interpreted as strings.
Use the JSON format if you need to pass non-string values such as Booleans, integers, floats, lists, and so on.

So yeah. Use the JSON format.

ansible … --extra-vars '{ "i_wasted_30_mins_debugging_a_boolean_string": true }'

Another better (?) solution in playbooks/roles would be to sanitize the input as a pre-flight task.

Further readings

Sources