Ansible

Siehe auch

Ansible (2012 entstanden, 2016 von Red Hat gekauft) ist ein SSH-basiertes Verwaltungstool zur Orchestrierung und Konfiguration. Ansible kommt ohne Agenten aus und erledigt durchzuführende Aktionen per SSH und Python >= 2.6 direkt auf den Zielsystemen.

Begriffe

  • Control Node: der Host, der Ansible ausführt

  • Managed Node: der vom Control Node zu konfigurierende Client (bei Salt heisst dieser beispielsweise Minion)

Links

Ansible Cheat Sheet

Control Node - Installation per pip:

# using pip, for the current user: pip install –user ansible ansible-core ansible-base

Control Node - Installation per Subscription Manager:

# with full support
subscription-manager register
subscription-manager role --set="Red Hat Enterprise Linux Server"
subscription-manager list --available
subscription-manager attach --pool=<engine-subscription-pool>

# with limited support
subscription-manager refresh

# after one of both:
subscription-manager repos --enable ansible-2-for-rhel-8-x86_64-rpms

# then:
yum install ansible

Ansible System Roles von Red Hat installieren:

yum -y install rhel-system-roles
# /usr/share/ansible/roles/rhel-system-roles.\*
# /usr/share/doc/rhel-system-roles/

Voraussetzungen auf den Managed Hosts:

# RHEL 8
dnf module install python36
dnf install python3-libselinux

# RHEL 7
yum install python
yum install libselinux-python

Doc:

# list all or specific type of modules
ansible-doc --type become|module|... --list

ansible-doc ping
ansible-doc --snippet ping

Konfiguration - ./ansible.cfg überschreibt ~/.ansible.cfg überschreibt /etc/ansible/ansible.cfg. Beispiel:

ansible.cfg
[defaults]
ask_pass = false
inventory = /path/to/inventory_dir
log_path = /path/to/ansible.log
remote_user = linuxfabrik

[privilege_escalation]
# Disable privilege escalation by default.
become = False
# If privilege escalation is enabled (from the command line), configure default settings
# to have Ansible use the sudo method to switch to the root user account
become_ask_pass = False
become_method = sudo
become_user = root

Mögliche Verzeichnisstruktur:

project
+-- ansible.cfg
+-- group_vars
    +-- group_name (file or dir)
+-- host_vars
    +-- hostname
        +-- vault
+-- inventory
    +-- hosts
+-- roles
+-- playbooks

Inventory als ini-File (hosts.ini):

[ch]
zurich[1:2].linuxfabrik.ch

[de]
user=joe
duesseldorf.linuxfabrik.de ansible_connection=ssh ansible_user=linus ansible_port=6666 ansible_host=192.0.2.50
stuttgart.linuxfabrik.de

[europe:children]
ch
de

Inventory als YAML-File (hosts.yml):

test:
  hosts:
    192.168.122.34:
    192.168.122.199:

# a comment
rocky8:
  hosts:
    192.168.122.249:
  children:
    cis_rocky8:
      vars:
        ansible_become: true
      hosts:
        192.168.122.249:

Interessante group_vars/host_vars:

ansible_connection: winrm
ansible_port: 5986

facts_file (Variablen für Playbooks):

[general]
key = value

Variablen - Reihenfolge des Überschreibens (nachfolgende überschreibt vorhergehende):

  • role/defaults

  • inventory

  • group_vars

  • host_vars

  • Command-Line

Variablen:

group_vars/myvars.yml
# Vars (key: value)
var1: value1
var2: value2

# Dictionary. Usage: `{{ users['linux']['home_dir'] }}`
users:
  linus:
    first_name: Linus
    home_dir: /users/linus

# List
wishlist:
  - item1
  - item2

Die nützlichsten Ansible-internen „Magic“-Variablen:

  • group_names (when: "dev" in group_names)

  • groups

  • hostvars

  • inventory_hostname

Häufiger verwendete Ansible Facts:

ansible --module-name setup localhost
ansible_facts['date_time']
ansible_facts['default_ipv4']
ansible_facts['devices']
ansible_facts['distribution']
ansible_facts['distribution_version']
ansible_facts['dns']
ansible_facts['fqdn']
ansible_facts['hostname']
ansible_facts['interfaces']
ansible_facts['kernel']
ansible_facts['memfree_mb']
ansible_facts['memtotal_mb']
ansible_facts['mounts']
ansible_facts['os_family']
ansible_facts['processor_count']

Playbook-Elemente:

- name: My first play.
  hosts:
    - localhost
    - srv.linuxfabrik.ch
    - 192.168.109.*

  remote_user: ansible
  become: true

  vars:
    user: linus
    home: /home/linus
    facts_file: custom.fact
  vars_files:
    - vault/secret.yml

  # for example if VM currently does not exist:
  gather_facts: true

  # static, preprocessed
  import_playbook: db.yml
  import_tasks: install.yml

  # dynamic, during the run
  include_role: ...
  include_tasks: tasks/environment.yml

  pre_tasks:
    - name: ...

  serial: 2
  tasks:
    - name: ...
      task_name:
        ...
      delegate_to: localhost
  roles:
    - role: sshd
      key1: value1

  post_tasks:
    - name: ...

Mögliche Elemente eines Tasks:

- name: "Describe what we do here with {{ variables }}"
  hosts: localhost
  become: false

  <modulename>:
    <attr1>: "{{ the_devil }}"
    <attr2>: "{{ lookup('file', 'files/' + item.uid) }}"
    <attr3>: present

  loop: "{{ var }}"  # use "{{ item }}"
  loop_control:
    loop_var: i      # use "{{ i }}"

  register: result

  when: >
    ( a == "RedHat" and a == "7" )
    or
    ( a == "Fedora" and a == "28" )
  ignore_errors: true
  failed_when:
  changed_when:
    - false
    - not a
    - a is not defined
    - a is failed
    - "Success" not in a.stdout
    - "dev" in a
    - a == "RedHat" or a == "Fedora"
    - a < 256

  notify:
    - systemctl restart httpd
  force_handlers: true

- name: Print all facts
  debug:
    var: ansible_facts
    verbosity: 2

- name: ...
  block:
    ...
  rescue:
    # tasks to run if the tasks defined in the block clause fail
  always:
    # tasks that will always run independently of the success or failure of tasks
    # defined in the block and rescue clauses
  when:
    # also applies to rescue and always clauses if present

Role erstellen:

ansible-galaxy init myrole

Handler sind nichts anderes als Tasks:

- name: systemctl restart httpd
  systemd:
    name: httpd
    state: restarted
  when: a == false and b is not defined

Dependencies (meta/main.yml):

dependencies:
  - role: apache
    port: 8080
  - name: my-role
    src: https://github.com/Linuxfabrik/lfops
    scm: git
    version: main

Häufig verwendete Core-Module:

setup:

blockinfile:
lineinfile:

file: (= mkdir, chmod etc.)
stat:
sefcontext:

copy:
fetch:
synchronize: (= rsync)

assert:
that:

fail:

authorized_key:
known_hosts:

command:
shell:

at:
cron:

debug:

apt:
dnf:
gem:
package:
package_facts:
pip:
rpm_key:
yum:
yum_repository:

service:
systemd:

filesystem: (= Filesystem erzeugen)
lvg:
lvol:
mount:
parted:

firewalld:
nmcli:

get_url:
uri:

mysql_user:

group:
user: (inkl. SSH-Keys)

redhat_subscription:
rhsm_repository:

template: (Jinja2)

timezone:

reboot:

Newlines behalten:

include_newlines: |
    Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
    tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
    quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
    consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
    cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
    proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Newlines in Spaces umwandeln:

fold_newlines: >
    Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
    tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
    quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
    consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
    cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
    proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Jinja-Templating:

# {{ ansible_managed }}
{# Jinja comment #}
{{ ansible_facts['default_ipv4']['address'] }}

{% if finished %}
    {{ result }}
{% endif %}

{% for user in users if not user == "root" %}
    {{ loop.index }}: {{ user }}
{% endfor %}
- name: template render
  template:
    src: path/to/app.conf.j2
    dest: /path/to/app.conf

Jinja-Filter:

{{ output | from_json }}
{{ output | to_json }}
{{ output | to_nice_json }}
{{ output | from_yaml }}
{{ output | to_yaml }}
{{ output | to_nice_yaml }}

Ad-hoc:

ansible all|localhost|ungrouped|mygroup|mysrv --inventory inventory --user user --one-line ...

ansible ... --list-hosts
ansible ... --module-name setup --args 'filter=ansible_devices'
ansible ... --module-name ping
ansible ... --module-name command|shell|raw --args 'uptime'
ansible ... --module-name user --args "name=the_devil uid=666 state=present"

# request privilege escalation from the command line
ansible ... --module-name copy --args 'content="Managed by Ansible" dest=/etc/motd' --become

Ansible-Config (dazu ins Ansible-Verzeichnis wechseln):

# list all options:
ansible-config list

# cd to your working directory
ansible-config dump --verbose --only-changed

Ansible-Inventory veranschaulichen:

ansible-inventory --inventory path/to/inventory --graph

Run der Ansible Playbooks:

ansible-playbook --syntax-check path/to/playbook.yml

ansible-playbook -vvvv path/to/playbook.yml
ansible-playbook --inventory path/to/hosts.yml path/to/playbook.yml

ansible-playbook --step path/to/playbook.yml
ansible-playbook --start-at-task "my task" path/to/playbook.yml

# dry-run / smoke tests without changing anything:
ansible-playbook --check path/to/playbook.yml

# report the changes made to (small) files
ansible-playbook --diff path/to/playbook.yml

ansible-playbook --extra-vars="key=value" path/to/playbook.yml

Tipp

Vor einem Produktiv-Run empfiehlt sich ein ansible-playbook --check --diff playbook.yml, um die kommenden Änderungen zu prüfen.

Aber beachten:

  • Wenn Tasks über Conditionals gesteuert werden, funktioniert --check möglicherweise nicht wie erwartet.

  • Tasks mit check_mode: false werden immer, Tasks mit check_mode: true nur im Check-Mode ausgeführt.

Tipp

ansible-config, ansible-doc, ansible-inventory und ansible-playbook können durch ansible-navigator ersetzt werden, welches zudem mit einem TUI aufwarten kann.

Ansible-Vault (Verwendung per vars_files:):

ansible-vault create --vault-password-file=vault-pass secret.yml
ansible-vault view secret.yml
ansible-vault edit secret.yml
ansible-vault encrypt plain.yml --output=secret.yml
ansible-vault decrypt secret.yml --output=plain.yml
ansible-vault rekey secret.yml
ansible-vault rekey --new-vault-password-file=NEW_VAULT_PASSWORD_FILE secret.yml

ansible-playbook --ask-vault-pass|--vault-password-file=vault-pass
ansible-playbook --vault-id @prompt --vault-password-file=vault-pw-file playbook.yml

Ansible Galaxy:

ansible-galaxy list
ansible-galaxy search 'redis' --platforms EL
ansible-galaxy info geerlingguy.redis
ansible-galaxy install --role-file roles/requirements.yml --roles-path roles
ansible-galaxy collection install git@github.com:Linuxfabrik/lfops.git
ansible-galaxy remove nginx-acme-ssh

Eine Ansible Galaxy requirements.yml-Datei:

- name: my-role
  src: https://github.com/Linuxfabrik/lfops
  scm: git
  version: main

Ansible-Lint

Hält man die Best Practices ein?

dnf -y install ansible-lint

# list alle rules
ansible-lint -L

# check my playbook
ansible-lint playbooks/playbook.yml

Ansible-Review

dnf -y install ansible-review

Mitogen - Ansible Beine machen

Vielversprechend. Siehe https://mitogen.networkgenomics.com

  • mitogen v0.2.9 bietet keinen Support für Python-Interpreter Discovery, und funktioniert deshalb nicht unter RHEL 8 (da es kein /usr/bin/python gibt).

  • mitogen v0.2.10 (pre-release) hat noch Probleme: AttributeError: module 'ansible_collections.ansible.builtin.plugins.action' has no attribute 'ActionBase'.

Zeiten (ansible-playbook --inventory inv playbook.yml --tags mytag):

  • default: 0:07:15.744 (h:mm:ss)

  • ssh-pipelining: 0:03:59.723

  • mitogen gegen CentOS 7: 0:00:11.122

  • mitogen gegen CentOS 7 & ssh-pipelining: 0:00:11.256

  • mitogen gegen CentOS 8: 0:00:17.376

Built on 2022-06-03