Ansible

Ansible é uno strumento di automazione.

Si utilizza per amministrare server: é stato creato da sviluppatori e amministratori di sistema, per automatizzare procedure in maniera affidabile, semplice ed automatica, compiti originariamente fatti “a mano” dagli amministratori. Questo é anche dovuto dall’esigenza di avere un’architettura altamente scalabile, non solo fisicamente ma anche virtualmente.

Ansible aiuta ad automatizzare la configurazione di server tramite file in formato YAML.

Di default, utilizza OpenSSH.

Compendio di nozioni utili

Qui di seguito si riportano delle nozioni che é bene conoscere.

  • -vvv : ogni qualvolta che si esegue un comando Ansible, é possibile visualizzare il log attraverso il flag -v. Piú v ci sono, maggiore sará la verbositá del log (verbose mode)
  • per fare input utente:
vars_prompt:
- name: username
  prompt: "What is your username?"
  • export ANSIBLE_HOST_KEY_CHECKING=False usi questo nella bash per fare in modo che Ansible non controlli la key ogni volta

Vagrant e Virtualbox

L’utilizzo di Vagrant e Virtualbox fanno sí che si possa andare a creare un’ambiente di test locale in maniera molto agevole.

Vagrant fa da tramite tra la riga di comando e il provisioner (nell’esempio Virtualbox): é un intermediario, ed aiuta a creare ambienti di test in maniera piú rapida.

Tutte le macchine di test (locali) sono descritte dal file di configurazione Vagrantfile.

Per gestire i server virtuali da riga di comando con Vagrant:

  • vagrant up: accende le VM
  • vagrant halt: spegne le VM
  • vagrant destroy: elimina le VM (fisicamente!)
  • vagrant provision: lancia il provisioner
  • vagrant rsync: sincronizza la cartella specificata nel Vagrantfile, comune quindi sia alle VM che al localhost

Esempio di Vagrantfile:

VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
	# General Vagrant VM configuration.
	config.vm.box = "geerlingguy/rockylinux8"
	config.ssh.insert_key = false
	config.vm.synced_folder ".", "/vagrant", disabled: true
	config.vm.provider :virtualbox do |v|
	v.memory = 512
	v.linked_clone = true
end
 
# Application server 1.
config.vm.define "app1" do |app|
	app.vm.hostname = "orc-app1.test"
	app.vm.network :private_network, ip: "192.168.56.4"
end
 
# Application server 2.
config.vm.define "app2" do |app|
	app.vm.hostname = "orc-app2.test"
	app.vm.network :private_network, ip: "192.168.56.5"
end
 

Inventory

Ansible utilizza gli inventory per tenere traccia degli host dell’infrastruttura, attraverso i loro indirizzi ip.

Tendenzialmente sono file .ini, posso anche usare dei file .yaml.

Si possono specificare dei gruppi, denotandoli con []

Ad esempio:

# hosts.ini
[example]        # gruppo
192.168.56.2     # host
192.168.56.3     # un altro host

Per convenzione il nome dei file hosts é hosts.ini.

Negli inventory, per ogni singolo host possono essere definite delle variabili:

  • ansible_ssh_port : per connessioni SSH diverse dalla porta standard di SSH

É buona prassi seguire la regola un inventory per ogni ambiente di produzione (ad esempio):

servercheck/
	inventories/
		inventory-prod.ini
		inventory-cert.ini
		inventory-dev.ini
	playbook.yml

Struttura se ho modificato il file hosts della macchina locale

Struttura (inventory_test) se ho modificato il file hosts:

[stage] # [gruppo] lista di host
stage1.garr.academy      
stage2.garr.academy

Invece nell’hosts:

192.168.56.2        stage1.garr.academy        stage1
192.168.56.3        stage2.garr.academy        stage2

Usare nomi simbolici senza dover modificare il file /etc/hosts

Piu comodo, non bisogna farlo ogni volta che si aggiorna un host… insomma, basta usare questo.

[stage]                  # [gruppo] lista di hosts
stage1.garr.academy      ansible_host=192.168.56.2
stage2.garr.academy      ansible_host=192.168.56.3

Playbooks

I playbook sono un’insieme di azioni (tasks) da eseguire in sequenza.

Sono scritti in YAML: un linguaggio semplice che é sia human che machine-readable, ancora piú del JSON.

Ogni qualvolta che si esegue un playbook, Ansible raccoglie dei facts sugli host, cioé dei dati utili come che OS stanno usando, memoria libera in quel momento ecc. Lo fa sempre all’inizio di tutti i playbook. Per velocizzare l’esecuzione del playbook e disabilitarlo: set gather_facts: no nel playbook principale.

Ad esempio per installare un pacchetto possiamo usare il builtin di Ansible, apt:

---     # Iniziare sempre con ---
- name: Awesome Playbook   # un nome, facoltativo
- hosts: all    # specifica quale gruppo di host  
  become: yes # superadmin
  tasks:
	 - name: Ensure chrony (for time synchronization) is installed.
	 apt:
		 name: chrony
		 state: present
	
	 - name: Ensure chrony is running.
	 service:
		 name: chronyd
		 state: started
		 enabled: yes

Nomi e commenti sono facoltativi, ma é buona prassi metterli in modo tale da rendersi conto cosa succede in fase di debug del playbook.

Puoi importare task da altri file usando import_tasks: nomefile.yml

Puoi importare altri playbook da altri file usando import_playbooks: nomefile.yml.

Puoi importare tutto quello che vuoi.

Esistono dei task molto basilari chiamati builtin. Tutti i builtin dalla docs di Ansible. Includono operazioni come copia, spostamento file, rinomina, crea nuovo utente o gruppo…

Esecuzione

  • in generale:

    ansible-playbook playbook.yml -i hosts.ini
    
  • limitare l’esecuzione di un playbook su un host specifico:

ansible-playbook playbook.yml --limit xyz.example.com
  • vedere la lista degli host sul quale andresti a runnare il playbook:
ansible-playbook playbook.yml --list-hosts
  • eseguire un playbook con chiave:
ansible-playbook my-academy-playbook.yaml -i my-academy-inv.ini --private-key=$HOME/vagrant4academy/.vagrant/machines/stage1/virtualbox/private_key
  • lanciare un playbook sulla macchina locale
# dentro hosts.ini
localhost       ansible_connection=local

# da riga di comando
ansible-playbook playbook.yml -i hosts.ini

oppure

ansible-playbook playbook.yml --connection=local
  • lanciare un playbook come root
ansible-playbook playbook.yml -b -i hosts.ini

oppure

# inserire password manualmente / richiedere password manualmente
ansible-playbook playbook.yml --connection=local -K

oppure specificare nel playbook become: yes.

É buona prassi andare a fare un --list-hosts prima di eseguire un playbook.

SSH

Per usare un playbook devo essere autenticato con SSH, tramite private key.

Per farlo, la private key viene data come parametro a ansible-playbook oppure nell’inventory viene messo il percorso della chiave.

In quest’ultimo caso, l’Inventory risulterá in questo modo:

[stage]
stage1.garr.academy     ansible_host=192.168.56.2       ansible_ssh_private_key_file=/home/academy/vagrant4academy/.vagrant/machines/stage1/virtualbox/private_key
 
stage2.garr.academy     ansible_host=192.168.56.3       ansible_ssh_private_key_file=/home/academy/vagrant4academy/.vagrant/machines/stage2/virtualbox/private_key

Facts

Informazioni reperite dai server host remoti e utilizzati come variabili negli ansible playbook.

Tramite il comando:

ansible stage1.garr.academy -m ansible.builtin.setup -i my-academy-project/my-academy-inv.ini --private-key .vagrant/machines/stage1/virtualbox/private_key -u vagrant

ci connettiamo all’host e otteniamo una serie di facts in json, di informazioni utili!

Per disabilitare la raccolta di facts: all’inizio del playbook: settare gather_facts: no all’inizio.

Facts piu utilizzati:

  • "ansible_os_family":"Debian"
  • per usarlo in un playbook o ovunque dovro usare {{ ansible_os_family }}: proprio come fosse una variabile. Infatti troveremo ‘Debian’.
  • ansible_default_ipv4['address']

Pre e post tasks

  • pre_tasks: operazioni che devono essere eseguite PRIMA ANCORA di tutto il playbook, a prescindere (come un update della cache dei pacchetti o il fetch di chiavi gpg)

Handlers

Gli handlers sono delle operazioni che vengono eseguite a seguito di alcuni eventi.

Funzionano usando il paradigma “notify”: al termine di un’operazione si “risveglia” e si chiama un evento.

Hanno queste caratteristiche:

  • sono atomici: vengono eseguiti una e una sola volta alla fine del playbook
  • sono condizionali: vengono eseguiti solamente se qualcuno li richiama
  • sono diversamente ineluttabili: se il playbook fallisce per una qualsiasi ragione, non vengono eseguiti
    • per ovviare a questo problema, si puó usare --force-handlers a patto che gli host non vadano offline

Variabili

Come qualsiasi linguaggio, Ansible offre la possibilitá di definire delle variabili.

Essendo i file scritti in yml:

---
variabile: "VALORE"

Niente punti, trattini alti e caratteri speciali, o numeri all’inizio delle variabili e per convenzione si scrivono tutte in minuscolo, niente camelcase.

Tipicamente si trovano nel file vars.yml, ma possono essere anche definite nei seguenti modi:

  • attraverso il .bash_profile aggiungendo le linee che servono. Solo shell peró sará in grado di leggerle, quindi non é una buona prassi:
- name: Add an environment variable to the remote user's shell.
  lineinfile:
	 dest: ~/.bash_profile
	 regexp: '^ENV_VAR='
	 line: "ENV_VAR=value"

- name: Get the value of the environment variable we just added.
  shell: 'source ~/.bash_profile && echo $ENV_VAR'
  register: foo

- name: Print the value of the environment variable.
  debug:
     msg: "The variable is {{ foo.stdout }}"
  • usando la riga di comando usando il parametro --extra-vars:
ansible-playbook example.yml --extra-vars "foo=bar"
  • usando la sezione vars nei playbook, sia nel caso di variabili singole che di file:
vars:
  foo: bar

o

vars_files:
    - vars.yml
  • tramite inventory
# Host-specific variables (defined inline).
[washington]
app1.example.com proxy_state=present
app2.example.com proxy_state=absent

# Variables defined for the entire group.
[washington:vars]
cdn_host=washington.static.example.com
api_version=3.0.1

Per registrare l’output delle variabili si puó usare la direttiva register: l’output di un comando andrá a finire nella variabile specificata. Es.

- name: Run a shell command and register its output as a variable
  ansible.builtin.shell: /usr/bin/foo
  register: foo_result
  ignore_errors: true

Per usare una variabile "{{ }}" (le virgolette sono importanti)

Condizionali: when, register, tags

I task possono essere eseguiti a seguito di particolari condizioni:

  • condizionali classici: `if/then/when

    • Es. when: ansible_facts['os_family'] == "Debian"
  • changed_when and failed_when: di solito si usano con gli output (es. changed_when: 'Not found' in out‘)

  • tags: delle stringhe che etichettano un playbook, un role o qualsiasi altra cosa

Precedenza delle variabili

  1. --extra-vars passed in via the command line (these always win, no matter what).
  2. Task-level vars (in a task block).
  3. Block-level vars (for all tasks in a block).
  4. Role vars (e.g. [role]/vars/main.yml) and vars from include_vars module.
  5. Vars set via set_facts modules.
  6. Vars set via register in a task.
  7. Individual play-level vars: 1. vars_files 2. vars_prompt 3. vars
  8. Host facts.
  9. Playbook host_vars.
  10. Playbook group_vars.
  11. Inventory: 1. host_vars 2. group_vars 3. vars
  12. Role default vars (e.g. [role]/defaults/main.yml).

Link alla docs

Variabili magiche

Variabili speciali, che richiamandole danno accesso a delle informazioni.

Sono comode.

Per approfondire: https://docs.ansible.com/ansible/latest/reference_appendices/special_variables.html

Le piu importanti:

  • {{ hostvars }}: tutti i server/host presenti e le variabili assegnate a loro
  • {{ inventory_name }}: su chi sto eseguendo la ricetta? host
  • {{ groups }}: tutti i gruppi presenti nell’inventory e server/host sotto di loro
  • {{ group_names }}: tutti i gruppi a cui appartiene host per cui si sta eseguendo il playbook

Cicli

Servono per eseguire task piu volte.

Deve essere usato item, se no, non funziona nulla e il corpo del loop non viene eseguito.

Concetto di foreach (array semplice):

- name: Create users
    ansible.builtin.user:
        name: \`{{ item }}\`
        state: present
    loop:
        - vittoria
        - andrea
        - stefano
        - ...

Concetto di foreach (array):

- name: Create users
    ansible.builtin.user:
        name: \`{{ item['name'] }}\`
        uid: \`{{ item['uid'] }}\`
        groups: \`{{ item['groups'] }}\`
        state: present
    loop:
        - { name: 'vittoria', uid: 1001, groups:'devOps'}
        - ...

Group vars e host vars

Di default Ansible cerca due cartelle nominate group_vars e host_vars:

  1. nella stessa cartella dell’inventory
  2. /etc/ansible se usi l’inventory predefinito

group_vars: è utilizzata per definire le variabili che si applicano a un gruppo specifico di host.

host_vars: è utilizzata per definire le variabili che si applicano a host specifici. Ogni host ha il suo file di variabili all’interno della directory host_vars, dove è possibile definire variabili personalizzate.

Es.

# File: group_vars/web_servers.yml
apache_port: 80
document_root: /var/www/html
server_type: web
# File: host_vars/web_server1.yml
hostname: webserver1.example.com
ip_address: 192.168.1.101
os_type: ubuntu

Dati sensibili

Password e altri dati sensibili devono essere conservati in maniera adeguata. Per questo si utilizza Ansible Vault, un tool che cripta i file usando la cifratura AES 256.

Funziona in questo modo:

  1. In un file YAML si definiscono i dati sensibili
  2. Si criptano i dati con Ansible Vault utilizzando una chiave (password o file)
  3. Si conserva la chiave in un luogo sicuro e al di fuori della repo
  4. Ogni qualvolta che bisogna eseguire un playbook, si usa la chiave e si decripta il vault

Criptare un file (ansible-vault chiederá una password):

ansible-vault encrypt vars/api_key.yml

Criptare un file utilizzando un file:

ansible-playbook encrypt main.yml --vault-password-file .vault.pwd

Ruoli

Pacchetti di configurazioni ad-hoc per qualcosa.

Solitamente é consigliabile farli altamente customizzabili per evitare di rimetterci le mani ogni volta.

Includono playbook perlopiú.

Solitamente hanno:

  • tasks/main.yml: il playbook principale
  • defaults/main.yml: valori di default
  • Handlers, Files and Templates per quel ruolo

Per dirla con una metafora: é una specie di grande script (main.yml) che fa qualcosa (es. settare web server)

Prima di riscrivere un role, sempre controllare che gia’ ne esiste uno.

Ruoli cross platform

  • se servono uno o due sistemi operativi: when: ansible_os_family == 'Debian' ...
  • in generale vale la regola un playbook per ogni piattaforma
---
- name: Include OS-specific variables.
include_vars: "{{ ansible_os_family }}.yml"

- name: Include OS-specific setup tasks.
include_tasks: setup-{{ ansible_os_family }}.yml

Usare un ruolo

- name: Creazione di file sulle VM
hosts: stage
become: yes
remote_user: vagrant
 
roles:
- nginxinc.nginx

Collection

Una collection é un insieme di ruoli, moduli, plugin e playbook.

Vengono installate per default:

  • ∼/.ansible/collections
  • /usr/share/ansible/collections

Usare una collection

- name: Test pb
hosts: stage
become: yes
remote_user: vagrant
 
collections:
- general.community

Moduli

I moduli sono librerie Python che fanno in modo di eseguire determinate operazioni su un qualche nodo.

Quelli di ansible.builtin fanno parte della collezione predefinita.

Builtin importanti

I piu importanti:

  • systemd
  • copy
  • pip
  • command
  • import_task: importa i task da un altro file

Ansible Galaxy

Un servizio dove le persone condividono ruoli e collection.

Per installare qualcosa:

ansible-galaxy role install geerlingguy.apache

Con il classico requirements.yml:

ansible-galaxy
install -r requirements.yml

Sicurezza

  1. Utilizzare comunicazioni sicure e criptate.
  2. Disattivare l’utente root e usare sudo.
  3. Rimuovere il software non utilizzato e aprire solo le porte necessarie.
  4. Utilizzare il principio del minimo privilegio: permessi corretti su file e cartelle.
  5. Aggiornare il sistema operativo e il software installato.
  6. Utilizzare un firewall correttamente configurato.
  7. Assicurarsi che i file di log siano popolati e ruotati.
  8. Monitorare i login e bloccare gli indirizzi IP sospetti: fail2ban.
  9. SeLinux

Consigli e best practises

  • usare commenti e nomi
  • un playbook non dovrebbe MAI superare le 100 righe
  • la modularitá é importante
    • prediligere approcci con import_tasks o include_tasks o include_playbooks importare da altri file
    • prediligere l’utilizzo di ruoli
  • essere YAML-compliant quando si scrivono file yaml
  • ci sono due modi per scrivere un task in Ansible:
    • one-line: da preferire in caso di cose rapide che non richiedono una particolare configurazione
    - name: Install Nginx
      dnf: name=nginx state=present
    
    • strutturata: da preferire in generale
    -  name: Copy ssh.
    template:
        src: "files/ssh/authorized_keys"
    	dest: "/home/{{ host_user }}/.ssh"
    	owner: "{{ host_user }}"
    	group: "{{ host_user }}"
    	mode: 0755
    
  • preferire un ambiente di test, anziché la propria macchina
  • configurare l’ansible cfg in questo modo:
    # ansible.cfg
    [defaults]
    nocows = True
    collections_paths = ./collections
    roles_path = ./roles
    
  • usare il file requirements.ymlper definire i requisiti
  • valutare bene quando si usa un ruolo della community
    • ultimo aggiornamento
    • quanto viene utilizzato
    • semplice da sistemare

Gestione degli errori

Chiavi speciali che modificano l’esecuzione del playbook.

  • default: se errore, blocca la macchina che da errore, il resto vai

  • any_errors_fatal: true: se si verifica un errore, ferma tutto il parco macchine. Non si mette all’interno dei task.

  • ignore_errors: true se il playbook ha un errorcode diverso da 0, anche se c’e’ un errore lo ignora. Si mette all’interno dei task.

  • failed_when: a una specifica condizione il playbook si ferma