Capítulo 4. Inventario: Describiendo tus servidores

Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com

Hasta ahora, hemos estado trabajando con un solo servidor (o host, como lo llama Ansible). El inventario más sencillo es una lista de nombres de host separados por comas, que puedes hacer incluso sin servidor:

$ ansible all -i 'localhost,' -a date

En realidad, vas a gestionar varios hosts. El conjunto de hosts que Ansible conoce se denomina inventario. En este capítulo aprenderás a describir un conjunto de hosts como inventario de Ansible, creando un inventario que contenga varias máquinas.

Tu archivo ansible.cfg debe parecerse al Ejemplo 4-1, que habilita explícitamente todos los complementos del inventario.

Ejemplo 4-1. ansible.cfg
[defaults]
inventory = inventory

[inventory]
enable_plugins = host_list, script, auto, yaml, ini, toml

En este capítulo, utilizaremos un directorio llamado inventario para los ejemplos de inventario. El inventario de Ansible es un objeto muy flexible: puede ser un archivo (en varios formatos), un directorio o un ejecutable, y algunos ejecutables se incluyen como plug-ins. Los plug-ins de inventario nos permiten apuntar a fuentes de datos, como tu proveedor de la nube, para compilar el inventario. Un inventario puede almacenarse por separado de tus libros de jugadas. Esto significa que puedes crear un directorio de inventario para utilizarlo con Ansible en la línea de comandos, con hosts que se ejecuten en Vagrant, Amazon EC2, Google Cloud Platform o Microsoft Azure, ¡o donde quieras!

Nota

Serge van Ginderachter es la persona que más sabe de inventario Ansible. Consulta su blog para obtener información detallada.

Inventario/Archivos

La forma predeterminada de describir tus hosts en Ansible es listarlos en archivos de texto, llamados archivos hosts de inventario. La forma más sencilla es simplemente una lista de nombres de host en un archivo llamado hosts, como se muestra en el Ejemplo 4-2.

Ejemplo 4-2. Un fichero de inventario muy sencillo
frankfurt.example.com
helsinki.example.com
hongkong.example.com
johannesburg.example.com
london.example.com
newyork.example.com
seoul.example.com
sydney.example.com

Ansible añade automáticamente un host al inventario por defecto: localhost. Entiende que localhost se refiere a tu máquina local, con la que interactuará directamente en lugar de conectarse por SSH.

Preliminares: Varias máquinas Vagrant

Para hablar de inventario, necesitarás interactuar con varios hosts. Configuremos Vagrant para que aparezcan tres hosts. Los llamaremos, de forma poco imaginativa, vagrant1, vagrant2, y vagrant3.

Antes de crear un nuevo archivo Vagrant para este capítulo, asegúrate en de que destruyes tu(s) máquina(s) virtual(es) existente(s) ejecutando lo siguiente:

$ vagrant destroy --force

Si no incluyes la opción --force, Vagrant te pedirá que confirmes que quieres destruir cada máquina virtual incluida en el archivo Vagrantfile.

A continuación, crea un nuevo archivo Vagrantfile parecido al del Ejemplo 4-3.

Ejemplo 4-3. Vagrantfile con tres servidores
VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  # Use the same key for each machine
  config.ssh.insert_key = false

  config.vm.define "vagrant1" do |vagrant1|
    vagrant1.vm.box = "ubuntu/focal64"
    vagrant1.vm.network "forwarded_port", guest: 80, host: 8080
    vagrant1.vm.network "forwarded_port", guest: 443, host: 8443
  end
  config.vm.define "vagrant2" do |vagrant2|
    vagrant2.vm.box = "ubuntu/focal64"
    vagrant2.vm.network "forwarded_port", guest: 80, host: 8081
    vagrant2.vm.network "forwarded_port", guest: 443, host: 8444
  end
  config.vm.define "vagrant3" do |vagrant3|
    vagrant3.vm.box = "centos/stream8"
    vagrant3.vm.network "forwarded_port", guest: 80, host: 8082
    vagrant3.vm.network "forwarded_port", guest: 443, host: 8445
  end
end

Vagrant, a partir de la versión 1.7, utiliza por defecto en una clave SSH diferente para cada host. El Ejemplo 4-3 contiene la línea para volver al comportamiento anterior de utilizar la misma clave SSH para cada host:

config.ssh.insert_key = false

Utilizar la misma clave en cada host simplifica nuestra configuración de Ansible porque podemos especificar una única clave SSH en la configuración.

Por ahora, supongamos que cada uno de estos servidores puede ser potencialmente un servidor web, así que el Ejemplo 4-3 asigna los puertos 80 y 443 dentro de cada máquina Vagrant a un puerto de la máquina local.

Deberíamos poder hacer aparecer las máquinas virtuales ejecutando lo siguiente:

$ vagrant up

Si todo va bien, el resultado debería ser algo parecido a esto:

Bringing machine 'vagrant1' up with 'virtualbox' provider...
Bringing machine 'vagrant2' up with 'virtualbox' provider...
Bringing machine 'vagrant3' up with 'virtualbox' provider...
...
    vagrant1: 80 (guest) => 8080 (host) (adapter 1)
    vagrant1: 443 (guest) => 8443 (host) (adapter 1)
    vagrant1: 22 (guest) => 2222 (host) (adapter 1)
==> vagrant1: Running 'pre-boot' VM customizations...
==> vagrant1: Booting VM...
==> vagrant1: Waiting for machine to boot. This may take a few minutes...
    vagrant1: SSH address: 127.0.0.1:2222
    vagrant1: SSH username: vagrant
    vagrant1: SSH auth method: private key
==> vagrant1: Machine booted and ready!
==> vagrant1: Checking for guest additions in VM...
==> vagrant1: Mounting shared folders...
    vagrant1: /vagrant => /Users/bas/code/ansible/ansiblebook/ansiblebook/ch03

A continuación, necesitamos saber qué puertos de la máquina local se asignan al puerto SSH (22) dentro de cada máquina virtual. Recuerda que podemos obtener esa información ejecutando lo siguiente:

$ vagrant ssh-config

El resultado debería ser algo parecido a esto:

Host vagrant1
  HostName 127.0.0.1
  User vagrant
  Port 2222
  UserKnownHostsFile /dev/null
  StrictHostKeyChecking no
  PasswordAuthentication no
  IdentityFile /Users/lorin/.vagrant.d/insecure_private_key
  IdentitiesOnly yes
  LogLevel FATAL
Host vagrant2
  HostName 127.0.0.1
  User vagrant
  Port 2200
  UserKnownHostsFile /dev/null
  StrictHostKeyChecking no
  PasswordAuthentication no
  IdentityFile /Users/lorin/.vagrant.d/insecure_private_key
  IdentitiesOnly yes
  LogLevel FATAL
Host vagrant3
  HostName 127.0.0.1
  User vagrant
  Port 2201
  UserKnownHostsFile /dev/null
  StrictHostKeyChecking no
  PasswordAuthentication no
  IdentityFile /Users/lorin/.vagrant.d/insecure_private_key
  IdentitiesOnly yes
  LogLevel FATAL

Gran parte de la información de ssh-config es repetitiva y puede reducirse. La información que difiere por host es que vagrant1 utiliza el puerto 2222, vagrant2 utiliza el puerto 2200 y vagrant3 utiliza el puerto 2201.

Ansible utiliza tu cliente SSH local por defecto, lo que significa que entenderá cualquier alias que configures en tu archivo de configuración SSH. Por lo tanto, utilizamos un alias comodín en el archivo ~/.ssh/config:

Host vagrant*
  Hostname 127.0.0.1
  User vagrant
  UserKnownHostsFile /dev/null
  StrictHostKeyChecking no
  PasswordAuthentication no
  IdentityFile ~/.vagrant.d/insecure_private_key
  IdentitiesOnly yes
  LogLevel FATAL

Modifica tu archivo inventario/hosts para que tenga este aspecto:

vagrant1 ansible_port=2222
vagrant2 ansible_port=2200
vagrant3 ansible_port=2201

Ahora, asegúrate de que puedes acceder a estas máquinas. Por ejemplo, para obtener información sobre la interfaz de red de vagrant2, ejecuta lo siguiente:

$ ansible vagrant2 -a "ip addr show dev enp0s3"

El resultado debería ser algo parecido a esto:

vagrant2 | CHANGED | rc=0 >>
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP 
group default qlen 1000
    link/ether 02:1e:de:45:2c:c8 brd ff:ff:ff:ff:ff:ff
    inet 10.0.2.15/24 brd 10.0.2.255 scope global dynamic enp0s3
       valid_lft 86178sec preferred_lft 86178sec
    inet6 fe80::1e:deff:fe45:2cc8/64 scope link
       valid_lft forever preferred_lft forever

Parámetros del Inventario de Comportamiento

Para describir nuestras máquinas Vagrant en el archivo de inventario de Ansible, tuvimos que especificar explícitamente el puerto (2222, 2200 o 2201) al que debía conectarse el cliente SSH de Ansible. Ansible denomina a este tipo de variables parámetros de inventario de comportamiento, y hay varios de ellos que puedes utilizar cuando necesites anular los valores predeterminados de Ansible para un host (consulta la Tabla 4-1).

Tabla 4-1. Parámetros del inventario de comportamiento
Nombre Por defecto Descripción
ansible_host Nombre del anfitrión Nombre de host o dirección IP para SSH
ansible_port 22 Puerto a SSH
ansible_user $USUARIO Usuario para SSH como
ansible_password (Ninguno) Contraseña a utilizar para la autenticación SSH
ansible_connection inteligente Cómo se conectará Ansible al host (consulta la sección siguiente)
ansible_ssh_private_key_file (Ninguno) Clave privada SSH a utilizar para la autenticación SSH
ansible_shell_type sh Shell a utilizar para los comandos (consulta la sección siguiente)
ansible_python_interpreter /usr/bin/python Intérprete de Python en el host (consulta la sección siguiente)
ansible_*_interpreter (Ninguno) Como ansible_python_interpreter para otras lenguas (consulta la sección siguiente)

Para algunas de estas opciones, el significado es obvio por el nombre, pero otras requieren más explicaciones:

ansible_connection

Ansible admite varios transportes, que son mecanismos que Ansible utiliza para conectarse al host. El transporte por defecto, smart, comprobará si el cliente SSH instalado localmente admite una función llamada ControlPersist. Si el cliente SSH es compatible con ControlPersist, Ansible utilizará el cliente SSH local. Si no, el transporte inteligente volverá a utilizar una biblioteca de cliente SSH basada en Python llamada Paramiko.

ansible_shell_type

Ansible funciona estableciendo conexiones SSH con máquinas remotas y luego invocando guiones. Por defecto, Ansible asume que el shell remoto es el shell Bourne ubicado en /bin/sh, y generará los parámetros de línea de comandos adecuados que funcionen con él. Crea directorios temporales para almacenar estos scripts.

Ansible también acepta csh, fish, y (en Windows) powershell como valores válidos para este parámetro. Ansible no funciona con shells restringidos.

ansible_python_interpreter

Ansible necesita conocer la ubicación del intérprete de Python en la máquina remota. Puede que quieras cambiar esto para elegir una versión que funcione para ti. La forma más sencilla de ejecutar Ansible con Python 3 es instalarlo con pip3 y establecer esto:

ansible_python_interpreter="/usr/bin/env python3"
ansible_*_interpreter

Si utilizas un módulo personalizado que no está escrito en Python, puedes utilizar este parámetro para especificar la ubicación del intérprete (como /usr/bin/ruby). Trataremos este tema en el Capítulo 12.

Cambiar los valores por defecto de los parámetros de comportamiento

Puedes anular algunos de los valores por defecto de los parámetros de comportamiento en el archivo de inventario, o puedes anularlos en la sección defaults del archivo ansible.cfg(Tabla 4-2). Considera dónde cambias estos parámetros. ¿Son los cambios una elección personal, o se aplican a todo tu equipo? ¿Una parte de tu inventario necesita una configuración diferente? Recuerda que puedes configurar las preferencias SSH en el archivo ~/.ssh/config.

Tabla 4-2. Valores por defecto que se pueden anular en ansible.cfg
Parámetro del inventario de comportamiento opción ansible.cfg
ansible_port remote_port
ansible_user remote_user
ansible_ssh_private_key_file ssh_private_key_file
ansible_shell_type executable (véase el párrafo siguiente)

La opción de configuración de ansible.cfg executable no es exactamente igual que el parámetro de inventario de comportamiento ansible_shell_type. El ejecutable especifica la ruta completa del intérprete de comandos a utilizar en la máquina remota (por ejemplo, /usr/local/bin/fish). Ansible buscará el nombre base de esta ruta (en este caso fish) y lo utilizará como valor por defecto para ansible_shell_type.

Grupos y Grupos y Grupos

Normalmente queremos realizar acciones de configuración en grupos de hosts, en lugar de en un host individual. Ansible define automáticamente un grupo llamado all (o *), que incluye todos los hosts del inventario. Por ejemplo, podemos comprobar si los relojes de las máquinas están aproximadamente sincronizados ejecutando lo siguiente:

$ ansible all -a "date"

o

$ ansible '*' -a "date"

La salida en el sistema de Bas tiene este aspecto:

vagrant2 | CHANGED | rc=0 >>
Wed 12 May 2021 01:37:47 PM UTC
vagrant1 | CHANGED | rc=0 >>
Wed 12 May 2021 01:37:47 PM UTC
vagrant3 | CHANGED | rc=0 >>
Wed 12 May 2021 01:37:47 PM UTC

Podemos definir nuestros propios grupos en el archivo hosts de inventario. Ansible utiliza el formato de archivo .ini para los archivos hosts de inventario; agrupa los valores de configuración en secciones.

He aquí cómo especificar que nuestros hosts vagrant estén en un grupo llamado vagrant, junto con los otros hosts de ejemplo mencionados al principio del capítulo:

frankfurt.example.com
helsinki.example.com
hongkong.example.com
johannesburg.example.com
london.example.com
newyork.example.com
seoul.example.com
sydney.example.com

[vagrant]
vagrant1 ansible_port=2222
vagrant2 ansible_port=2200
vagrant3 ansible_port=2201

Podríamos listar alternativamente los hosts Vagrant al principio y luego también en grupo, así:

frankfurt.example.com
helsinki.example.com
hongkong.example.com
johannesburg.example.com
london.example.com
newyork.example.com
seoul.example.com
sydney.example.com
vagrant1 ansible_port=2222
vagrant2 ansible_port=2200
vagrant3 ansible_port=2201

[vagrant]
vagrant1
vagrant2
vagrant3

Puedes utilizar los grupos de la forma que te convenga: pueden solaparse o estar anidados, como quieras. El orden no importa, salvo por la legibilidad humana.

Ejemplo: Implementación de una aplicación Django

Imagina que eres responsable de la implementación de una aplicación web basada en Django que procesa trabajos de larga duración. La aplicación necesita soportar los siguientes servicios:

  • La propia aplicación web Django, ejecutada por un servidor HTTP Gunicorn

  • Un servidor web NGINX, que se situará delante de Gunicorn y servirá activos estáticos

  • Una cola de tareas Celery que ejecutará trabajos de larga duración en nombre de la aplicación web

  • Una cola de mensajes RabbitMQ que sirve de backend para Celery

  • Una base de datos Postgres que sirve como almacén persistente

En capítulos posteriores, trabajaremos con un ejemplo detallado de implementación de este tipo de aplicación basada en Django, aunque nuestro ejemplo no utilizará Celery ni RabbitMQ. Por ahora, necesitamos desplegar esta aplicación en tres entornos diferentes: producción (el real), staging (para pruebas en hosts a los que nuestro equipo tiene acceso compartido) y Vagrant (para pruebas locales).

Cuando realizamos la implementación en producción, queremos que todo el sistema responda con rapidez y fiabilidad, por lo que hacemos lo siguiente:

  • Ejecuta la aplicación web en varios hosts para mejorar el rendimiento y coloca un equilibrador de carga delante de ellos

  • Ejecuta servidores de cola de tareas en varios hosts para mejorar el rendimiento

  • Pon Gunicorn, Celery, RabbitMQ y Postgres en servidores separados

  • Utiliza dos hosts Postgres: uno primario y otro réplica

Suponiendo que tengamos un equilibrador de carga, tres servidores web, tres colas de tareas, un servidor RabbitMQ y dos servidores de bases de datos, son 10 hosts con los que tenemos que lidiar(Figura 4-1).

Django application deployment
Figura 4-1. Diez hosts para la implementación de una aplicación Django

Para nuestro entorno de ensayo, queremos utilizar menos hosts que en producción para ahorrar costes, ya que va a ver mucha menos actividad que en producción. Digamos que decidimos utilizar sólo dos hosts para la puesta en escena; pondremos el servidor web y la cola de tareas en un host de puesta en escena, y RabbitMQ y Postgres en el otro.

Para nuestro entorno local Vagrant, decidimos utilizar tres servidores: uno para la aplicación web, otro para una cola de tareas y otro que contendrá RabbitMQ y Postgres.

El Ejemplo 4-4 muestra un archivo de inventario de muestra que agrupa los servidores por entorno (producción, staging, Vagrant) y por función (servidor web, cola de tareas, etc.).

Ejemplo 4-4. Archivo de inventario para la implementación de una aplicación Django
[production]
frankfurt.example.com
helsinki.example.com
hongkong.example.com
johannesburg.example.com
london.example.com
newyork.example.com
seoul.example.com
sydney.example.com
tokyo.example.com
toronto.example.com

[staging]
amsterdam.example.com
chicago.example.com

[lb]
helsinki.example.com

[web]
amsterdam.example.com
seoul.example.com
sydney.example.com
toronto.example.com
vagrant1

[task]
amsterdam.example.com
hongkong.example.com
johannesburg.example.com
newyork.example.com
vagrant2

[rabbitmq]
chicago.example.com
tokyo.example.com
vagrant3

[db]
chicago.example.com
frankfurt.example.com
london.example.com
vagrant3

Podríamos haber enumerado primero todos los servidores en la parte superior del archivo de inventario, sin especificar un grupo, pero no es necesario, y eso habría alargado aún más este archivo.

Ten en cuenta que sólo tenemos que especificar una vez los parámetros del inventario de comportamiento para las instancias Vagrant.

Alias y puertos

Hemos descrito así nuestros hosts Vagrant:

[vagrant]
vagrant1 ansible_port=2222
vagrant2 ansible_port=2200
vagrant3 ansible_port=2201

Los nombres vagrant1, vagrant2 y vagrant3 son alias. No son los nombres de host reales, sólo nombres útiles para referirse a estos hosts. Ansible resuelve los nombres de host utilizando el inventario, tu archivo de configuración SSH, /etc/hosts y DNS. Esta flexibilidad es útil en el desarrollo, pero puede ser causa de confusión.

Ansible también admite el uso de la sintaxis <hostname>:<port> al especificar hosts , por lo que podríamos sustituir la línea que contiene vagrant1 por 127.0.0.1:2222 (Ejemplo 4-5).

Ejemplo 4-5. Esto no funciona
[vagrant]
127.0.0.1:2222
127.0.0.1:2200
127.0.0.1:2201

Sin embargo, en realidad no podemos ejecutar lo que ves en el Ejemplo 4-5. La razón es que el inventario de Ansible sólo puede asociar un único host a 127.0.0.1, por lo que el grupo Vagrant sólo contendría un host en lugar de tres.

Grupos de Grupos

Ansible también te permite definir grupos que estén formados por otros grupos. Por ejemplo, como tanto los servidores web como los servidores de cola de tareas necesitarán Django y sus dependencias, podría ser útil definir un grupo django que contenga ambos. Añadirías esto al archivo de inventario:

[django:children]
web
task

Ten en cuenta que la sintaxis cambia cuando especificas un grupo de grupos, en lugar de un grupo de hosts. Esto es para que Ansible sepa que debe interpretar web y task como grupos y no como hosts.

Anfitriones numerados (mascotas frente a ganado)

El archivo de inventario que viste en el Ejemplo 4-4 parece complejo. Describe 15 hosts, lo que no parece un gran número en este mundo nublado y escalable. Sin embargo, tratar con 15 hosts en el archivo de inventario puede ser engorroso, porque cada host tiene un nombre de host completamente distinto.

A Bill Baker, de Microsoft, se le ocurrió distinguir entre tratar a los servidores como mascotas y tratarlos como ganado.1 A las mascotas les damos nombres distintivos y las tratamos y cuidamos como individuos; al ganado, sin embargo, nos referimos a él por un número de identificación y lo tratamos como ganado.

El enfoque "ganado" de los servidores es mucho más escalable, y Ansible lo soporta bien al admitir patrones numéricos. Por ejemplo, si tus 20 servidores se llaman web1.ejemplo.com, web2.ejemplo.com, etc., puedes especificarlos así en el archivo de inventario:

[web]
web[1:20].example.com

Si prefieres que tenga un cero a la izquierda (como web01.ejemplo.com), especifícalo en el rango, así:

[web]
web[01:20].example.com

Ansible también admite el uso de caracteres alfabéticos para especificar rangos. Si quieres utilizar la convención web-a.ejemplo.com, web-b.ejemplo.com, etc., para tus 20 servidores, puedes hacerlo en:

[web]
web-[a:t].example.com

Hosts y Variables de Grupo: Dentro del Inventario

Recuerda cómo podemos especificar parámetros de comportamiento 1 inventario para hosts Vagrant:

vagrant1 ansible_host=127.0.0.1 ansible_port=2222
vagrant2 ansible_host=127.0.0.1 ansible_port=2200
vagrant3 ansible_host=127.0.0.1 ansible_port=2201

Esos parámetros son variables que tienen un significado especial para Ansible. También podemos definir nombres de variables arbitrarios y valores asociados en los hosts. Por ejemplo, podríamos definir una variable llamada color y establecerle un valor para cada servidor:

amsterdam.example.com color=red
seoul.example.com color=green
sydney.example.com color=blue
toronto.example.com color=purple

Entonces podríamos utilizar esta variable en un libro de jugadas, como cualquier otra variable. Personalmente, sus autores no suelen asociar variables a hosts concretos. En cambio, solemos asociar variables a grupos.

Volviendo a nuestro ejemplo de Django, la aplicación web y el servicio de cola de tareas necesitan comunicarse con RabbitMQ y Postgres. Supondremos que el acceso a la base de datos Postgres está asegurado tanto en la capa de red (de modo que sólo la aplicación web y la cola de tareas puedan acceder a la base de datos) como mediante nombre de usuario y contraseña. RabbitMQ sólo está asegurado por la capa de red.

Para configurarlo todo, puedes

  • Configura los servidores web con el nombre de host, el puerto, el nombre de usuario y la contraseña del servidor Postgres principal, y el nombre de la base de datos.

  • Configura las colas de tareas con el nombre de host, el puerto, el nombre de usuario y la contraseña del servidor Postgres primario, y el nombre de la base de datos.

  • Configura los servidores web con el nombre de host y el puerto del servidor RabbitMQ.

  • Configura las colas de tareas con el nombre de host y el puerto del servidor RabbitMQ.

  • Configura el servidor Postgres primario con el nombre de host, el puerto y el nombre de usuario y contraseña del servidor Postgres réplica (sólo producción).

Esta información de configuración varía según el entorno, por lo que tiene sentido definirlas como variables de grupo en los grupos de producción, staging y Vagrant. El Ejemplo 4-6 muestra una forma de hacerlo en el archivo de inventario. (En el Capítulo 8 se analiza una forma mejor de almacenar las contraseñas).

Ejemplo 4-6. Especificar variables de grupo en el inventario
[all:vars]
ntp_server=ntp.ubuntu.com
[production:vars]
db_primary_host=frankfurt.example.com
db_primary_port=5432
db_replica_host=london.example.com
db_name=widget_production
db_user=widgetuser
db_password=pFmMxcyD;Fc6)6
rabbitmq_host=johannesburg.example.com
rabbitmq_port=5672
[staging:vars]
db_primary_host=chicago.example.com
db_primary_port=5432
db_name=widget_staging
db_user=widgetuser
db_password=L@4Ryz8cRUXedj
rabbitmq_host=chicago.example.com
rabbitmq_port=5672
[vagrant:vars]
db_primary_host=vagrant3
db_primary_port=5432
db_name=widget_vagrant
db_user=widgetuser
db_password=password
rabbitmq_host=vagrant3
rabbitmq_port=5672

Observa cómo las variables de grupo están organizadas en secciones denominadas [<group name>:vars]. Además, hemos aprovechado el grupo all (que, como recordarás, Ansible crea automáticamente) para especificar variables queno cambian entre hosts.

Variables de host y de grupo: En sus propios archivos

El archivo de inventario es un lugar razonable para poner variables de host y grupo si no tienes demasiados hosts. Pero a medida que tu inventario aumenta, se hace más difícil gestionar las variables de esta forma. Además, aunque las variables de Ansible pueden contener booleanos, cadenas, listas y diccionarios, en un archivo de inventario sólo puedes especificar booleanos y cadenas.

Ansible ofrece un enfoque más escalable para hacer un seguimiento de las variables de host y grupo: puedes crear un archivo de variables distinto para cada host y cada grupo. Ansible espera que estos archivos de variables estén en formato YAML.

Busca archivos de variables de host en un directorio llamado host_vars y archivos de variables de grupo en un directorio llamado group_vars. Ansible espera que estos directorios estén en el directorio que contiene tus libros de jugadas o en el directorio adyacente a tu archivo de inventario. Si tienes ambos directorios, el primero (el directorio de los libros de jugadas) tiene prioridad.

Por ejemplo, si Lorin tiene un directorio que contiene sus playbooks en /home/lorin/playbooks/ con un directorio de inventario y un archivo hosts en /home/lorin/inventory/hosts, debe poner variables para el host amsterdam . example .com en el archivo /home/lorin/inventory/host_vars/amsterdam.example.com y variables para el grupo de producción en el archivo /home/lorin/inventory/group_vars/production (como se muestra en el Ejemplo 4-7).

Ejemplo 4-7. group_vars/production
---
db_primary_host: frankfurt.example.com
db_primary_port: 5432
db_replica_host: london.example.com
db_name: widget_production
db_user: widgetuser
db_password: 'pFmMxcyD;Fc6)6'
rabbitmq_host: johannesburg.example.com
rabbitmq_port: 5672
...

También podemos utilizar diccionarios YAML para representar estos valores, como se muestra en el Ejemplo 4-8.

Ejemplo 4-8. group_vars/production, con diccionarios
---
db:
  user: widgetuser
  password: 'pFmMxcyD;Fc6)6'
  name: widget_production
  primary:
    host: frankfurt.example.com
    port: 5432
  replica:
    host: london.example.com
    port: 5432
rabbitmq:
  host: johannesburg.example.com
  port: 5672
...

Si elegimos los diccionarios YAML, accedemos a las variables con notación de puntos, así:

"{{ db.primary.host }}"

También podemos acceder a las variables del diccionario así:

"{{ db['primary']['host'] }}"

Compáralo con la forma en que accederíamos a ellos de otro modo:

"{{ db_primary_host }}"

Si queremos separar aún más las cosas, Ansible nos permite definir group_vars/production como un directorio en lugar de como un archivo. Podemos colocar en él varios archivos YAML que contengan definiciones de variables. Por ejemplo, podríamos poner las variables relacionadas con la base de datos en un archivo y las variables relacionadas con RabbitMQ en otro archivo, como se muestra en los Ejemplos 4-9 y 4-10.

Ejemplo 4-9. group_vars/production/db
---
db:
  user: widgetuser
  password: 'pFmMxcyD;Fc6)6'
  name: widget_production
  primary:
    host: frankfurt.example.com
    port: 5432
  replica:
    host: london.example.com
    port: 5432
...
Ejemplo 4-10. group_vars/production/rabbitmq
---
rabbitmq:
  host: johannesburg.example.com
  port: 6379
...

A menudo es mejor empezar de forma sencilla, en lugar de dividir las variables en demasiados archivos. En equipos y proyectos más grandes, el valor de los archivos separados aumenta, ya que muchas personas pueden necesitar tirar de los archivos y trabajar en ellos al mismo tiempo.

Inventario dinámico

Hasta este punto, hemos estado especificando explícitamente todos nuestros hosts en nuestro archivo de inventario de hosts. Sin embargo, es posible que tengas un sistema externo a Ansible que realice un seguimiento de tus hosts. Por ejemplo, si tus hosts se ejecutan en Amazon EC2, entonces EC2 rastrea la información sobre tus hosts por ti. Puedes recuperar esta información a través de la interfaz web de EC2, su API de consulta o herramientas de línea de comandos como awscli. Otros proveedores de nubes tienen interfaces similares.

Si gestionas tus propios servidores utilizando un sistema de aprovisionamiento automatizado como Cobbler o Ubuntu Metal as a Service (MAAS), entonces tu sistema ya lleva un registro de tus servidores. O tal vez tengas una de esas elegantes bases de datos de gestión de la configuración (CMDB) donde reside toda esta información.

No querrás duplicar manualmente esta información en tu archivo hosts, porque a la larga ese archivo no coincidirá con tu sistema externo, que es la verdadera fuente de información sobre tus hosts. Ansible admite una función llamada inventario dinámico que te permite evitar esta duplicación.

Si el archivo de inventario está marcado como ejecutable, Ansible asumirá que se trata de un script de inventario dinámico y ejecutará el archivo en lugar de leerlo.

Nota

Para marcar un archivo como ejecutable , utiliza el comando chmod +x. Por ejemplo

$ chmod +x vagrant.py

Plug-ins de inventario

Ansible viene con varios ejecutables que pueden conectarse a varios sistemas en la nube, siempre que instales los requisitos y configures la autenticación. Estos complementos suelen necesitar un archivo de configuración YAML en el directorio de inventario, así como algunas variables de entorno o archivos de autenticación.

Para ver la lista de plug-ins disponibles:

$ ansible-doc -t inventory -l

Para ver documentación y ejemplos específicos de los plug-ins:

$ ansible-doc -t inventory <plugin name>

Amazon EC2

Si utilizas Amazon EC2, instala los requisitos de:

$ pip3 install boto3 botocore

Crea un archivo inventario/aws_ec2.yml con, al menos:

plugin: aws_ec2

Gestor de recursos Azure

Instala estos requisitos de en un virtualenv de Python con Ansible 2.9.xx:

$ pip3 install msrest msrestazure

Crea un archivo inventory/azure_rm.yml con, al menos:

plugin: azure_rm
platform: azure_rm
auth_source: auto
plain_host_names: true

La Interfaz para un Script de Inventario Dinámico

Un script de inventario dinámico de Ansible debe admitir dos banderas de línea de comandos:

  • --host=<hostname> para mostrar los detalles del anfitrión

  • --list para listar grupos

También debe devolver la salida en formato JSON con una estructura específica que Ansible pueda interpretar.

Mostrar detalles del anfitrión

Para obtener los detalles del host individual, Ansible llamará a a un script de inventario con el argumento --host=:

$ ansible-inventory -i inventory/hosts --host=vagrant2
Nota

Ansible incluye un script que funciona como un script de inventario dinámico para el inventario estático proporcionado con el argumento de la línea de comandos -i: ansible-inventory.

La salida debe contener cualquier variable específica del anfitrión, incluidos los parámetros de comportamiento, así

{
    "ansible_host": "127.0.0.1",
    "ansible_port": 2200,
    "ansible_ssh_private_key_file": "~/.vagrant.d/insecure_private_key",
    "ansible_user": "vagrant"
} 

La salida es un único objeto JSON; los nombres son los nombres de las variables y los valores son los valores de las variables.

Listado de grupos

Los scripts dinámicos de inventario deben ser capaces de listar todos los grupos y detalles sobre los hosts individuales. En el repositorio de GitHub que acompaña a este libro, hay un script de inventario para los hosts de Vagrant llamado vagrant.py. Ansible lo llamará así para obtener una lista de todos los grupos:

$ ./vagrant.py --list

De la forma más sencilla, el resultado podría ser el siguiente:

{"vagrant": ["vagrant1", "vagrant2", "vagrant3"]}

Esta salida es un único objeto JSON; los nombres son nombres de grupos de Ansible, y los valores son matrices de nombres de host.

Como optimización de, el comando --list puede contener los valores de las variables de host para todos los hosts, lo que ahorra a Ansible la molestia de hacer una invocación separada a --host para recuperar las variables de los hosts individuales.

Para aprovechar esta optimización, el comando --list debe devolver una clave llamada _meta que contenga las variables de cada host, de esta forma:

"_meta": {
    "hostvars": {
      "vagrant1": {
        "ansible_user": "vagrant",
        "ansible_host": "127.0.0.1",
        "ansible_ssh_private_key_file": "~/.vagrant.d/insecure_private_key",
        "ansible_port": "2222"
      },
      "vagrant2": {
        "ansible_user": "vagrant",
        "ansible_host": "127.0.0.1",
        "ansible_ssh_private_key_file": "~/.vagrant.d/insecure_private_key",
        "ansible_port": "2200"
      },
      "vagrant3": {
        "ansible_user": "vagrant",
        "ansible_host": "127.0.0.1",
        "ansible_ssh_private_key_file": "~/.vagrant.d/insecure_private_key",
        "ansible_port": "2201"
      }
    }

Escribir un guión de inventario dinámico

Una de las características útiles de Vagrant es que puedes ver qué máquinas se están ejecutando actualmente utilizando el comando vagrant status. Suponiendo que tengamos un archivo Vagrant con el aspecto del Ejemplo 4-3, si ejecutamos vagrant status, la salida sería como la del Ejemplo 4-11.

Ejemplo 4-11. Salida de vagrant status
$ vagrant status
Current machine states:

vagrant1                 running (virtualbox)
vagrant2                 running (virtualbox)
vagrant3                 running (virtualbox)

This environment represents multiple VMs. The VMs are all listed
above with their current state. For more information about a specific
VM, run 'vagrant status NAME'.

Como Vagrant ya lleva la cuenta de las máquinas por nosotros, no hace falta que las enumeremos en un archivo de inventario de Ansible. En su lugar, podemos escribir un script de inventario dinámico que consulte a Vagrant qué máquinas se están ejecutando. Una vez que hayamos configurado un script de inventario dinámico para Vagrant, aunque modifiquemos nuestro archivo Vagrant para ejecutar diferentes números de máquinas Vagrant, no necesitaremos editar un archivo de inventario Ansible.

Vamos a trabajar con un ejemplo de creación de un script de inventario dinámico que recupere los detalles sobre los hosts desde Vagrant. Nuestro script de inventario dinámico necesitará invocar el comando vagrant status. La salida que se muestra en el Ejemplo 4-11 está diseñada para que la lean los humanos. Podemos obtener una lista de hosts en ejecución en un formato que sea más fácil de parsear para los ordenadores con la bandera --machine-readable, de esta forma:

$ vagrant status --machine-readable

El resultado es el siguiente:

1620831617,vagrant1,metadata,provider,virtualbox
1620831617,vagrant2,metadata,provider,virtualbox
1620831618,vagrant3,metadata,provider,virtualbox
1620831619,vagrant1,provider-name,virtualbox
1620831619,vagrant1,state,running
1620831619,vagrant1,state-human-short,running
1620831619,vagrant1,state-human-long,The VM is running. To stop this 
VM%!(VAGRANT_COMMA) you can run `vagrant halt` to\nshut it down 
forcefully%!(VAGRANT_COMMA) or you can run `vagrant suspend` to 
simply\nsuspend the virtual machine. In either case%!(VAGRANT_COMMA) 
to restart it again%!(VAGRANT_COMMA)\nsimply run `vagrant up`.
1620831619,vagrant2,provider-name,virtualbox
1620831619,vagrant2,state,running
1620831619,vagrant2,state-human-short,running
1620831619,vagrant2,state-human-long,The VM is running. To stop this 
VM%!(VAGRANT_COMMA) you can run `vagrant halt` to\nshut it down 
forcefully%!(VAGRANT_COMMA) or you can run `vagrant suspend` to 
simply\nsuspend the virtual machine. In either case%!(VAGRANT_COMMA) 
to restart it again%!(VAGRANT_COMMA)\nsimply run `vagrant up`.
1620831620,vagrant3,provider-name,virtualbox
1620831620,vagrant3,state,running
1620831620,vagrant3,state-human-short,running
1620831620,vagrant3,state-human-long,The VM is running. To stop this 
VM%!(VAGRANT_COMMA) you can run `vagrant halt` to\nshut it down 
forcefully%!(VAGRANT_COMMA) or you can run `vagrant suspend` to 
simply\nsuspend the virtual machine. In either case%!(VAGRANT_COMMA) 
to restart it again%!(VAGRANT_COMMA)\nsimply run `vagrant up`.
1620831620,,ui,info,Current machine states:\n\nvagrant1                  
running (virtualbox)\nvagrant2         running (virtualbox)\nvagrant3
running (virtualbox)\n\nThis environment represents multiple VMs. The VMs 
are all listed\nabove with their current state. For more information about 
a specific\nVM%!(VAGRANT_COMMA) run `vagrant status NAME`

Para obtener detalles sobre una máquina Vagrant concreta, digamos, vagrant2, ejecutaríamos lo siguiente:

$ vagrant ssh-config vagrant2

El resultado es el siguiente:

Host vagrant2
  HostName 127.0.0.1
  User vagrant
  Port 2200
  UserKnownHostsFile /dev/null
  StrictHostKeyChecking no
  PasswordAuthentication no
  IdentityFile /Users/lorin/.vagrant.d/insecure_private_key
  IdentitiesOnly yes
  LogLevel FATAL

Nuestro script de inventario dinámico tendrá que llamar a estos comandos, analizar los resultados y generar el JSON adecuado. Podemos utilizar la biblioteca Paramiko para analizar la salida de vagrant ssh-config. En primer lugar, instala la biblioteca Paramiko de Python con pip:

$ pip3 install --user paramiko

Aquí tienes una sesión interactiva de Python que muestra cómo utilizar la biblioteca Paramiko para hacer esto:

$ python3
>>> import io
>>> import subprocess
>>> import paramiko
>>> cmd = ["vagrant", "ssh-config", "vagrant2"]
>>> ssh_config = subprocess.check_output(cmd).decode("utf-8")
>>> config = paramiko.SSHConfig()
>>> config.parse(io.StringIO(ssh_config))
>>> host_config = config.lookup("vagrant2")
>>> print (host_config)
{'hostname': '127.0.0.1', 'user': 'vagrant', 'port': '2200', 'userknownhostsfile': 
'/dev/null', 'stricthostkeychecking': 'no', 'passwordauthentication': 'no', 
'identityfile': ['/Users/bas/.vagrant.d/insecure_private_key'], 'identitiesonly': 
'yes', 'loglevel': 'FATAL'}

El Ejemplo 4-12 muestra nuestro script vagrant.py completo.

Ejemplo 4-12. vagrant.py
#!/usr/bin/env python3
""" Vagrant inventory script """
# Adapted from Mark Mandel's implementation
# https://github.com/markmandel/vagrant_ansible_example

import argparse
import io
import json
import subprocess
import sys

import paramiko


def parse_args():
    """command-line options"""
    parser = argparse.ArgumentParser(description="Vagrant inventory script")
    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument('--list', action='store_true')
    group.add_argument('--host')
    return parser.parse_args()


def list_running_hosts():
    """vagrant.py --list function"""
    cmd = ["vagrant", "status", "--machine-readable"]
    status = subprocess.check_output(cmd).rstrip().decode("utf-8")
    hosts = []
    for line in status.splitlines():
        (_, host, key, value) = line.split(',')[:4]
        if key == 'state' and value == 'running':
            hosts.append(host)
    return hosts


def get_host_details(host):
    """vagrant.py --host <hostname> function"""
    cmd = ["vagrant", "ssh-config", host]
    ssh_config = subprocess.check_output(cmd).decode("utf-8")
    config = paramiko.SSHConfig()
    config.parse(io.StringIO(ssh_config))
    host_config = config.lookup(host)
    return {'ansible_host': host_config['hostname'],
            'ansible_port': host_config['port'],
            'ansible_user': host_config['user'],
            'ansible_private_key_file': host_config['identityfile'][0]}


def main():
    """main"""
    args = parse_args()
    if args.list:
        hosts = list_running_hosts()
        json.dump({'vagrant': hosts}, sys.stdout)
    else:
        details = get_host_details(args.host)
        json.dump(details, sys.stdout)


if __name__ == '__main__':
    main()

Dividir el inventario en varios archivos

Si quieres tener tanto un archivo de inventario normal como un script de inventario dinámico (o, en realidad, cualquier combinación de archivos de inventario estáticos y dinámicos), sólo tienes que ponerlos todos en el mismo directorio y configurar Ansible para que utilice ese directorio como inventario. Puedes hacerlo mediante el parámetro inventory en ansible.cfg o utilizando la bandera -i en la línea de comandos. Ansible procesará todos los archivos y fusionará los resultados en un único inventario.

Esto significa que puedes crear un directorio de inventario para utilizarlo con Ansible en la línea de comandos con hosts que se ejecuten en Vagrant, Amazon EC2, Google Cloud Platform, Microsoft Azure, ¡o donde los necesites!

Por ejemplo, la estructura de directorios de Bas tiene este aspecto:

  • inventario/aws_ec2.yml
  • inventario/azure_rm.yml
  • inventario/grupo_vars/vagrant
  • inventario/group_vars/staging
  • inventario/grupo_vars/producción
  • inventario/anfitriones
  • inventario/vagrant.py

Añadir entradas en tiempo de ejecución con add_host y group_by

Ansible te permitirá añadir hosts y grupos al inventario durante la ejecución de un libro de jugadas. Esto es útil cuando se gestionan clusters dinámicos, como Redis Sentinel.

añadir_host

El módulo add_host añade un host al inventario; esto es útil si utilizas Ansible para aprovisionar nuevas instancias de máquinas virtuales dentro de una nube de infraestructura como servicio.

Invocar el módulo tiene el siguiente aspecto:

- name: Add the host
  add_host
    name: hostname
    groups: web,staging
    myvar: myval

Especificar la lista de grupos y variables adicionales es opcional.

Aquí tienes el comando add_host en acción, creando una nueva máquina Vagrant y configurándola:

---
- name: Provision a Vagrant machine
  hosts: localhost
  vars:
    box: centos/stream8

  tasks:
    - name: Create a Vagrantfile
      command: "vagrant init {{ box }}"
      args:
        creates: Vagrantfile

    - name: Bring up the vagrant machine
      command: vagrant up
      args:
        creates: .vagrant/machines/default/virtualbox/box_meta

    - name: Add the vagrant machine to the inventory
      add_host:
        name: default
        ansible_host: 127.0.0.1
        ansible_port: 2222
        ansible_user: vagrant
        ansible_private_key_file: >
          .vagrant/machines/default/virtualbox/private_key

- name: Do something to the vagrant machine
  hosts: default
  tasks:
    # The list of tasks would go here
    - name: ping
      ping:
...
Nota

El módulo add_host añade el host sólo durante la ejecución del libro de jugadas. No modifica tu archivo de inventario.

Cuando aprovisionamos dentro de nuestros libros de jugadas, nos gusta dividirlo en dos jugadas. La primera se ejecuta en localhost y aprovisiona los hosts, y la segunda los configura.

Ten en cuenta que utilizamos el argumento creates: Vagrantfile en esta tarea:

- name: Create a Vagrantfile
  command: "vagrant init {{ box }}"
  args:
    creates: Vagrantfile

Esto indica a Ansible que si el archivo Vagrantfile está presente, no es necesario volver a ejecutar el comando. Asegurarse de que el comando (potencialmente no idempotente) se ejecuta sólo una vez es una forma de conseguir la idempotencia en un libro de jugadas que invoque al módulo command. Lo mismo se hace con el módulo de comandos vagrant up.

agrupar_por

El módulo group_by de Ansible te permite crear nuevos grupos mientras se ejecuta un libro de jugadas. Cualquier grupo que crees se basará en el valor de una variable que se haya establecido en cada host, a la que Ansible se refiere como hecho.(El capítulo 5 trata los hechos con más detalle).

Si la recopilación de datos de Ansible está activada, Ansible asociará un conjunto de variables a un host. Por ejemplo, la variable ansible_machine será i386 para máquinas x86 de 32 bits y x86_64 para máquinas x86 de 64 bits. Si Ansible interactúa con una mezcla de hosts de este tipo, podemos crear grupos i386 y x86_64 con la tarea.

Si preferimos agrupar nuestros hosts por distribución de Linux (por ejemplo, Ubuntu o CentOS), podemos utilizar el hecho ansible_fact.distribution:

- name: Create groups based on Linux distribution
  group_by:
    key: "{{ ansible_facts.distribution }}"

En el Ejemplo 4-13, utilizamos group_by para crear grupos separados para nuestros hosts Ubuntu y CentOS, luego utilizamos el módulo apt para instalar paquetes en Ubuntu y el módulo yum para instalar paquetes en CentOS.

Ejemplo 4-13. Crear grupos ad hoc según la distribución de Linux
---

- name: Group hosts by distribution
  hosts: all
  gather_facts: true
  tasks:
    - name: Create groups based on distro
      group_by:
        key: "{{ ansible_facts.distribution }}"

- name: Do something to Ubuntu hosts
  hosts: Ubuntu
  become: true
  tasks:
    - name: Install jdk and jre
      apt:
        update_cache: true
        name:
          - openjdk-11-jdk-headless
          - openjdk-11-jre-headless

- name: Do something else to CentOS hosts
  hosts: CentOS
  become: true
  tasks:
    - name: Install jdk
      yum:
        name:
          - java-11-openjdk-headless
          - java-11-openjdk-devel

Conclusión

Esto es todo sobre el inventario de Ansible. Es un objeto muy flexible que ayuda a describir tu infraestructura y la forma en que quieres utilizarla. El inventario puede ser tan sencillo como un archivo de texto o tan complejo como puedas manejar.

En el capítulo siguiente se explica cómo utilizar las variables.

1 Este término ha sido popularizado por Randy Bias, de Cloudscaling.

Get Ansible: Up and Running, 3ª Edición now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.