Ansible Documentation | Getting Started

Agentless Push Automation : ansible-topology.png

ansible-playbook \
    -i inventory.file \
    -u ssh_user \
    playbook.yml

Summary

Ansible is the property of RedHat, Inc.

Ansible is a highly versatile tool for remotely provisioning and confuguring declared sets of target machines. It is designed for targets well beyond Linux; network appliances of many vendors. That is its strong point, and explains its lack of sensible defaults for Linux.

Referred to as "agentless" because the app itself has no process running on targets, unlike other provisioning tools. Ansible connects to targets via their SSH server (sshd), though its default behavior is to ignore the user's SSH configurations (on the control node).

Ansible is written in Python, with an ecosystem of modules for a vast range of tasks and target types. Python must also be installed on all targets for all but the most trivial use cases; a version compatible with that of Ansible (installed only on the control node).

Each target environment and task has its own set of Python modules and its own set of configuration (YAML or INI) requirements.

Ansible makes no attempt to abide any existing GNU/POSIX/Bash conventions or configurations. So, manhours consumed in declaring IaC of any real-world environment is significant. If all targets are Linux, however, that cost may be significantly lowered by using Ansible only as a bash-script runner; avoiding its labyrinth of per-module syntax and configurations.

Running ansible in verbose mode (-vvv) prints its (attempted) SSH connection statement:

ssh -C -o ControlMaster=auto -o ControlPersist=60s -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o 'User="u1"' -o ConnectTimeout=10 -o 'ControlPath="/c/HOME/.ansible/cp/975de6127e"' $ip '/bin/sh -c '"'"'hostname'"'"''

Install

# Install Python 3
sudo dnf -y install python3
# Set Python 3 as default
alternatives --set python /usr/bin/python3
# Install Python pip
sudo dnf -y install python3-pip
# Install ansible for current user (@ ~/.local or %APPDATA%Python)
python3 -m pip install --user ansible 
# Or 
python3 -m pip install --include-deps --user ansible
# Or just the core
python3 -m pip install ansible-core

# Upgrade
python3 -m pip install --upgrade --user ansible

# Add Autocomplete
python -m pip install --user argcomplete 

# Verify
ansible --version

Configuration : ansible.cfg : Configuration Settings

Format is a particular INI variant:

# Comment
; Comment
foo = bar ; Comment inline

Search order:

(None are created upon installation.)

Create

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

# Defaults
ansible-config init > ansible.cfg
# Zero config : everything commented out
ansible-config init --disabled > ansible.cfg
# Include those of all "existing" plugins
ansible-config init -t all > ansible.cfg

Project Structure

mkdir -p {inventory/{dev/{group_vars,host_vars},pro/{group_vars,host_vars}},playbooks/roles/{common/{tasks,handlers,templates,files,vars,defaults,meta,library},role-a/{tasks,files},role-b/{tasks,files,templates,vars}}}
touch ansible.cfg inventory/dev/hosts inventory/pro/hosts
☩ tree
.
├── inventory
│   ├── dev
│   │   ├── group_vars
│   │   ├── host_vars
│   │   └── hosts
│   └── pro
│       ├── group_vars
│       ├── host_vars
│       └── hosts
├── playbooks
│   └── roles
│       ├── common
│       │   ├── defaults
│       │   ├── files
│       │   ├── handlers
│       │   ├── library
│       │   ├── meta
│       │   ├── tasks
│       │   ├── templates
│       │   └── vars
│       ├── role-a
│       │   ├── files
│       │   └── tasks
│       └── role-b
│           ├── files
│           ├── tasks
│           ├── templates
│           └── vars
└── ansible.cfg

@ ansible.cfg | Configuration Settings

[defaults]
inventory = ./inventory/hosts

@ inventory/hosts | Inventory Guide

Note it's typically named hosts, not hosts.yml.

all:
  children:
    cluster:
      vars:
        cluster_scope: All hosts of all groups under cluster group
      children:
        master:
          hosts:
            a: a.lan
            b: b.lan
          vars:
            master_scope: All hosts of master group 
        worker:
          hosts:
            c: 192.168.1.10
            d: 
             hostname: d.lan
             port: 5555
    local:
      hosts:
        localhost:
      vars:
        ansible_connection: local
  vars:
    all_scope: All hosts of all groups, local and remote
    ansible_python_interpreter: /usr/bin/env python

Target multiple groups and/or hosts with an ad-hoc command:

ansible master,c -a 'ip -4 addr'

@ playbooks/site.yml (Playbook AKA Playbook file)

- hosts: all
  roles:
    - role: myrole  # This will run `roles/myrole/tasks/main.yml`

@ roles/ (Roles folder)

roles/
├── myrole/
│   ├── tasks/
│   │   └── main.yml
│   ├── files/
│   │   └── example_file.txt
│   ├── templates/
│   │   └── example_template.j2
│   └── vars/
│       └── main.yml
playbooks/
└── site.yml

@ myrole/tasks/main.yml (Task file)

- name: Copy file from role's files directory
  copy:
    src: example_file.txt   # This references roles/myrole/files/example_file.txt
    dest: /destination/path/on/remote

- name: Deploy configuration file from template
  template:
    src: example_template.j2   # This references roles/myrole/templates/example_template.j2
    dest: /etc/config.conf

Common Connection variables

Connection variables are normally used to set the specifics on how to execute actions on a target. Some are (required) per module. Here are common ones:

Magic variables

Ansible-controlled; overrides any set by user.

inventory.yml | Ansible Inventories

Ansible inventories are set(s) of target hosts upon which ansible operates. These are declared. Default is inventory.cfg file.

Format is YAML or proprietary "Ansible Inventory Format". Regardless, all must abide Ansible-specified (sub)keys.

Location of this file is declared in Ansible configuration (e.g., ansible.cfg) at "inventory" key.

[defaults]
inventory=inventory.cfg

@ inventory.yml

Default inventory file: /etc/ansible/hosts

Validate the inventory file


ansible-inventory -i inventory/hosts.yml --list --yaml
ansible-inventory -i inventory/hosts.yml --graph

# YAML linter
yamllint inventory/hosts.yml # dnf install yamllint

Prep target(s)

There are several conventions for configuring target machines. A simple, secure method is to configure the script user (gitops) on the target(s) such that their password login is entirely disabled, making remote, key-based ssh login the only method of access, and then creating a /etc/sudoers.d/gitops file that enables elevated privileges sans password entry.

@ ~/.ansible.cfg | ansible.cfg

[defaults]
action_warnings=False
inventory=inventory.cfg
deprecation_warnings=False
remote_user  = u2
;; become : This setting (True) works only for playbooks. 
;; Whereas ad-hoc commands (-a COMMAND) REQUIRE flag --become regardless.
become = True
become_user = root
[privilege_escalation]
become_method = sudo
become_ask_pass = True
[ssh_connection]
ssh_config = ${HOME}/.ssh/config
;; TTY allocation may cause failure by infinite silent hang 
;; depending on sudoers files configuration. 
;; Sudoers config may require : "Defaults !requiretty"
;ssh_args = -tt
usetty = True
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
pipelining = True
scp_if_ssh = smart
;; Force scp
;ssh_transfer_method = scp
timeout = 10

Use

# Ad-hoc command at one machine of default hosts file @ /etc/ansible/hosts
ansible -i hosts 192.168.1.109 -m ping
# Ad-hoc command (model: ping) at all target machines of hosts file @ /etc/ansible/hosts
ansible -i hosts app -m ping
# Same as above, but target is self
ansible -m ping localhost 
# Lists all facts of target (using model: setup)
ansible -m setup localhost

# Create ./ansible.cfg that includes declared inventory file.
ansible-config init --disabled |tee ansible.cfg.disabled
vim ansible.cfg.disabled # Edit; declare "inventory=inventory.cfg", and save as ansible.cfg
vim inventory.cfg # add [target] list of hosts
target='target'

# Ad-hoc command ping at $target machines declared in inventory file declared in ./ansible.cfg
ansible $target -m ping
# Ad-hoc : two commands
ansible $target -a hostname -a id
# Ad-hoc : Test is Ansible's ssh user (defaults to current user) has sudo sans password
ansible $target -a 'ls -hl /etc/sudoers.d/' --become
# shell module
ansible $target -m ansible.builtin.shell -a hostname
# script module
ansible $target -m ansible.builtin.script -a foo.sh 

# playbook : script w/ args injected
ansible-playbook foo.yml -e a=foo -e b=bar 

Privilege Escalation : sudo

# Ad-hoc sudo commands REQUIRE flag "--become" REGARDLESS of its mirror setting at .cfg 
ansible target -a 'cat /etc/sudoers.d/gitops' --become #=> "BECOME password: "

ansible-vault

# Create vault and add become_password
vault=become_pass.yml
ansible-vault create $vault
    # Prompts for vault password,
    # then opens in editor (vi). Add:
    # ---
    # ansible_become_password:·"PASSWORD"

# View content
ansible-vault view $vault
# Edit content
ansible-vault edit $vault --ask-vault-pass

# Use : 
# 1. Mod ansible.cfg
# - vault_password_file = become_pass.yml
# - become_ask_pass = False
# 2. Playbook
ansible-playbook playbook.yml --extra-vars "@$vault" --ask-vault-pass
# Or Ad-hoc
ansible target -a 'ls -hl /etc/sudoers.d/' --become --extra-vars "@$vault" --ask-vault-pass

Playbook (YAML)

@ example.yml

---
- name: example playbook
  hosts: local
  vars:
    foo: "bar"
    fbool: false
    cities:
    - Maryland
    - Virginia
  tasks:
  - name: print foo
    ## model: debug
    ansible.builtin.debug:
      msg: "value of foo is: {{ foo }}"
    ## Run task only on fbool: true
    when: fbool
  - name: print cities
    ## model: debug
    ansible.builtin.debug:
      #var: item 
    loop: "{{ cities }}"

Because ansible.cfg set the inventory-file path inventory.cfg (@ PWD), and playbook (example.yml) set target (group) name (hosts: local), the command to run the playbook is simply:

☩ ansible-playbook example.yml

PLAY [example playbook] ***...


TASK [Gathering Facts] ***...
ok: [localhost]

TASK [print foo] ***...
skipping: [localhost]

TASK [print cities] ***...
ok: [localhost] => (item=Maryland) => {
    "msg": "Hello world!"
}
ok: [localhost] => (item=Virginia) => {
    "msg": "Hello world!"
}

PLAY RECAP ***...
localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0

@ foo.yml

---
- name: Testing
  hosts: target
  vars:
    config_file_path: ~/.ansible/ansible.cfg
  become: true
  #become_flags: "-H -S -n"
  gather_facts: false
  tasks:
  - name: Task 1
    #command: "sh $HOME/devops/ansible/foo.sh {{a}} {{b}}"
    command: "sh $HOME/devops/ansible/foo.sh"