Overwrite de variables hash globales por variables hash de entorno en Ansible

Publicado por Manuel Valle el

AnsibleHash

Uno de los problemas en los que todos los que usamos Ansible tenemos que toparnos es con la preferencia de variables. Una de las ventajas de Ansible es que permite la definición y re-escritura de variables y hashes a todos los niveles ademas de aportar multitud de métodos para incluirlas o definirlas a nivel de host o de grupo. Muchas veces no sabemos o no terminamos de entender donde definir variables para depende qué cosas.

En Ansible 2.0 existe una lista más amplia que en las versiones 1.x y está definida en su documentación

  • role defaults
  • inventory vars
  • inventory group_vars
  • inventory host_vars
  • playbook group_vars
  • playbook host_vars
  • host facts
  • registered vars
  • set_facts
  • play vars
  • play vars_prompt
  • play vars_files
  • role and include vars
  • block vars
  • task vars
  • extra vars

Os imagináis que esta lista (en Ansible 1.x no estaba tan definido) hay que tenerla muy presente a la hora de definir variables en nuestros playbooks sobre todo cuando los niveles de complejidad de nuestro proyecto aumentan.

El caso que quiero contar en este artículo es cuando tenemos una serie de variables "globales" y queremos que sean sobreescritas por unas a nivel de entorno.

Por lo general si queremos definir variables globales para todos los hosts/plays podemos hacerlo en estos métodos:
- inventory groupvars (all) - playbook groupvars (all) - set_facts en todos los hosts - extra vars

Si queremos hacerlo solo a nivel variable tipo 'key:value' es sencillo, elegimos 2 de la lista y definimos las de entorno mas prioritarias que las "globales". El problema viene cuando tenemos un hash y queremos que se mezclen los diccionarios y no se sobrescriba completamente. En este caso Ansible nos da la posibilidad de utilizar la variable de configuración hash_behaviour. Esta variable por defecto tiene valor 'replace' cuando hay sobreescritura de hashes. Lo que hace es una sobrescritura completa del hash. Pero si escribimos en nuestro ansible.cfg 'hash_behaviour=merge' entonces hará un merge de los hashes. Ejemplo:

vars-globales.yml:

---
- numeros
    primero: 1
    segundo: 2
    tercero: 10

vars-entorno.yml:

---
- numeros
    primero: 1
    segundo: 3

playbook.yml:

- name: Set Global Variables
  hosts: all
  tasks:
    - debug: var=numeros

ansible.cfg:

[defaults]
hash_behaviour=merge  

Si ejecutamos el playbook como extra vars la prioridad seria el orden y nos devolverá:

$> ansible-playbook playbook.yml  -e "@vars-globales.yml" -e "@vars-entorno.yml"

PLAY [Set Global Variables] ****************************************************

TASK [setup] *******************************************************************  
ok: [localhost]


TASK [debug] *******************************************************************  
ok: [localhost] => {  
    "numeros": {
        "primero": "1",
        "segundo": "3",
        "tercero": "10"
    }
....

Si ejecutamos el playbook con la variable hash_behaviour definida por defecto ("replace"):

$> ansible-playbook playbook.yml  -e "@vars-globales.yml" -e "@vars-entorno.yml"

PLAY [Set Global Variables] ****************************************************

TASK [setup] *******************************************************************  
ok: [localhost]


TASK [debug] *******************************************************************  
ok: [localhost] => {  
    "numeros": {
        "primero": "1",
        "segundo": "3"
    }
....

En este caso se remplaza completamente el hash "numeros" y perdemos el valor de la variable "tercero".

Como vemos en la primera ejecución el merge se realiza perfectamente. Pero este comportamiento de la variable "hash_behaviour" se arrastra al resto de playbook y puede que un por ejemplo un role de terceros este desarrollado para contemplar un uso de esta variables con su valor por defecto de replace. Es por esto que no se recomienda el uso de esta configuración.

Para solventar este problema, por ejemplo, en el caso que exponíamos mas arriba, en el que queremos tener variables de entrada a nivel global que puedan ser o no sobreescritas por unas de entorno, utilizamos el filtro jinja combine. Para ello definimos la siguiente estructura de directorios:

├── inputs
│   ├── dev
│   │   └── entorno.yml
│   └── pro
│   |   └── entorno.yml
│   └── globales.yml
└── playbook.yml

Hemos definido un yaml con la variables globales "globales.yml" y una estructura de directorios "inputs" donde definimos cada entorno, en este caso "dev" y "pro" con otro fichero de variables que sobreescriben las primeras.

Lo primero que haremos será definir nuestro fichero de variables globales

---
configuracion:  
  mysql:
    usuario: "usuario-app"
  tomcat:
    logdir: "/var/log/tomcat/"
  rotacion_logs:
    tomcat: 30
    apache: 30

Ahora definimos por ejemplo el fichero de entorno para dev:

---
configuracion:  
  tomcat:
    logdir: "/var/log/app/"
  rotacion_logs:
    apache: 2
    tomcat: 2

y nuestro playbook

- name: todos
  hosts: all
  tasks:
    - include_vars: inputs/globales.yml
      register: configuracion_global
    - include_vars: inputs/{{ entorno }}/entorno.yml
      register: configuracion_entorno
    - set_fact: configuracion_merged="{{ configuracion_global.ansible_facts.configuracion |combine(configuracion_entorno.ansible_facts.configuracion) }}"
    - debug: var=configuracion_merged
    - debug: msg="apache hace rotado de {{configuracion_merged.rotacion_logs.apache}} dias"

Lo que hacemos es registrar variables con los inputs y luego definir con set_facts la variable mergeada con el filtro jinja combine. Para ejecutar este playbook simplemente pasamos el entorno como variable extra:

$> ansible-playbook playbook.yml  -e 'entorno=dev'

PLAY [todos] *******************************************************************

TASK [setup] *******************************************************************  
ok: [localhost]

TASK [include_vars] ************************************************************  
ok: [localhost]

TASK [include_vars] ************************************************************  
ok: [localhost]

TASK [set_fact] ****************************************************************  
ok: [localhost]

TASK [debug] *******************************************************************  
ok: [localhost] => {  
    "configuracion_merged": {
        "mysql": {
            "usuario": "usuario-app"
        },
        "rotacion_logs": {
            "apache": 2,
            "tomcat": 2
        },
        "tomcat": {
            "logdir": "/var/log/app/"
        }
    }
}

TASK [debug] *******************************************************************  
ok: [localhost] => {  
    "msg": "apache hace rotado de 2 días"
}

PLAY RECAP *********************************************************************  
localhost                  : ok=6    changed=0    unreachable=0    failed=0   

Espero que os sirva de ayuda, y no olvidéis tener siempre presenta la lista ordenada por preferencia a la hora de definir y hacer mas versátil vuestros playbooks.

Autor

Manuel Valle

Cloud Architect en Red Hat.
Implantador y "contagiador" de filosofía DevOps y metodologías Ágiles. Especialista en IaaS y PaaS.
Twitter: @manuvaldi