Introducción a Ansible 2 – vol.1

Ansible

 

Ansible

Ansible es un motor de automatización IT radicalmente sencillo que automatiza el aprovisionamiento cloud, gestión de configuraciones, despliegue de aplicaciones, orquestación intra-servicios y muchas otras necesidades IT.

Diseñada para despliegues multi-capas desde el primer día, Ansible modela la infrastructura IT, describiendo cómo todos los sistemas se inter-relacionan, en lugar de gestionar un sistema cada vez.

No usa agentes ni infrastructuras de seguridad personalizadas, así que es simple de desplegar; y lo más importante, hace uso de un lenguaje sencillo, YAML, en la forma de Ansible Playbooks, que permiten describir los trabajos de automatización en una manera que se aproxima al Inglés.

Esta información está extraída de la documentación oficial de Ansible. Para más detalles docs.ansible.com.

¿Qué podemos hacer con Ansible?

Ansible es una herramienta de automatización de despliegues que podría incluirse en el saco de otras como Puppet o Chef, pero su principal carácterística es que es agentless, es decir, que no requiere de instalar un agente en los host administrados.

Ansible realiza un despliege de configuraciones, instalaciones y acciones sobre múltiples máquinas, permitiendo así una capacidad de gestión automatizada efectiva, rápida y de poco consumo de recurso. No requiere de una base de datos para almacenar las opciones o las capacidades, ni las tareas a realizar. Ansible se basa en ficheros de texto planos escritos en lenguaje YAML que se utilizarán para definir las máquinas, las variables y las tareas a realizar. Para realizar las tareas Ansible dispone de una serie de módulos capaces de interactuar con herramientas dentro de los sistemas administrados. Estos módulos crecen en cada revisión de Ansible y en el momento de la redacción de este post había más de 780.

La manera que tiene de realizar las configuraciones es mediante una conexión remota desde un host hasta los otros. Por defecto usa una conexión SSH. ¿Eso quiere decir que no podremos administrar máquinas Windows? Ansible permite administrar máquinas Windows a través de WinRM, aunque las opciones y los módulos de uso serán limitados

Aunque sea agentless existen dos maneras de convertirlo en agen-like-tool, su ejecución desde el host manager (el que tendría ansible instalado) o desde cada uno de los hosts administrados (que deberían, en este caso, tener ansible instalado). En cualquiera de los dos vamos a depender de alguna herramienta de scheduling que ejecute ansible, la más simple sería una tarea con el Cron del sistema.

 

Instalando Ansible

Existen múltiples maneras de instalar Ansible, dependiendo del sistema operativo:

  • Source Code: válido para Linux y MacOSX
  • Pip: válido para Linux y MacOSX
  • Packages: válido para las diferentes distribuciones de Linux
  • Windows: Windows no está soportado como sistema manager, por lo que ansible no se puede instalar.

 

En este tutorial voy a realizar la instalación sobre CentOS 7. Para la familia de distribuciones Red Hat deberemos instalar los repositorios EPEL, excepto en Fedora que se encuentra disponible en los repositorios oficiales.

Haré uso de 2 máquinas virtuales en las que realizaré las tareas. Una será Manager y otra será Node-1. Manager  es la única que tendría ansible instalado, y ambas necesitán Python.

Ansible depende de Python 2.6 o 2.7. El uso de Python 3 está bajo Technology Preview en  Ansible 2.2

$ sudo yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm

$ sudo yum install -y python ansible

$ ansible --version
ansible 2.2.0.0
 config file = /etc/ansible/ansible.cfg
 configured module search path = Default w/o overrides

En nuestro ejemplo vamos a hacer uso de la conexión SSH, así que lo primero que tendremos que hacer será crear una clave ssh y copiarla a las máquinas administradas. Una ventaja que vamos a encontrar es que el Manager Host también puede ser gestionado, así que copiaremos la clave pública a nuestra máquina.

$ ssh-keygen 
Generating public/private rsa key pair.
Enter file in which to save the key (/home/centos/.ssh/id_rsa): 
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /home/centos/.ssh/id_rsa.
Your public key has been saved in /home/centos/.ssh/id_rsa.pub.
The key fingerprint is:
30:7e:59:4c:70:97:6c:67:5b:94:29:ef:f4:48:9b:a8 centos@manager.ansible.test.com
The key's randomart image is:
+--[ RSA 2048]----+
|        ..o... .+|
|         + .+.oo.|
|        o o. ooo |
|        . o o oo |
|         . S oo+.|
|          . . +..|
|               . |
|               E |
|                 |
+-----------------+

$ ssh-copy-id -i .ssh/id_rsa.pub localhost
The authenticity of host 'localhost (::1)' can't be established.
ECDSA key fingerprint is b5:47:7b:dd:d7:16:07:0e:97:5a:bd:6b:21:e9:b9:e6.
Are you sure you want to continue connecting (yes/no)? yes
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys

Number of key(s) added: 1

Now try logging into the machine, with: "ssh 'localhost'"
and check to make sure that only the key(s) you wanted were added.

$ ssh-copy-id -i .ssh/id_rsa.pub node-1.ansible.test.com
The authenticity of host 'node-1.ansible.test.com (192.168.150.110)' can't be established.
ECDSA key fingerprint is 9c:de:24:4e:ff:2a:55:05:35:b3:76:41:86:d3:58:21.
Are you sure you want to continue connecting (yes/no)? yes
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed</pre>
<pre>/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys

Number of key(s) added: 1

Now try logging into the machine, with: "ssh 'node-1.ansible.test.com'"
and check to make sure that only the key(s) you wanted were added.

Configurando Ansible

Ansible tiene su fichero de configuración por defecto en /etc/ansible/ansible.cfg. Existen multitud de opciones agrupadas en bloques. Estos son los bloques y las opciones más empleadas:

  • [defaults]: las opciones de configurción por defecto para la ejecución de Ansible
    • inventory: define la ubicación del fichero de inventorio, que por defecto es /etc/ansible/hosts
    • sudo_user: el usuario con el que sudo hará login, por defecto root
    • forks: el número de procesos paralelos de ansible, por defecto 5
    • timeout: el tiempo de espera por una conexión SSH, por defecto 10 segundos
    • log_path: la ubicación del fichero de logs, por defecto /var/log/ansible.log
    • nocows: si su valor es 0 y tenemos cowsay instalado, veremos uno de los animales informando de los playbooks, por defecto es 1
  • [privilege_escalation]: opciones relativas al escalado de privilegios
    • become: si está a True, el usuario con el que conectemos intentará escalar privilegios, por defecto False
    • become_method: el método a emplear para escalar privilegios, por defecto sudo
    • become_user: el usuario al que se escalará, por defecto root
  • [ssh_connection]: opciones relativas a la conexión SSH
    • ssh_args: las opciones que usará ansible en la ejecución de SSH, por defecto -C -o ComtrolMaster=auto -o ControlPersist=60s
    • control_path: ansible hace uso de multiplex para reducir la cantidad de conexiones, esta opción define el fichero socket a crear/usar, por defecto %(directory)s/ansible-ssh-%%h-%%p-%%r
    • scp_if_ssh: el mecanismo a emplear para transferir ficheros, por defecto smart que tratará de usar sftp y si falla lo intentará con scp
  • [colors]: define los colores de los distintos mensajes de ansible

Aunque esta es la ubicación por defecto del fichero de configuración, puede ser sobrescrito  por ~/.ansible.cfg y éste a su vez por ${CWD}/ansible.cfg.

 

 

El fichero de inventario

En este fichero se definen las máquinas a ser gestionadas. Las máquinas pueden ser identificadas por su IP o por su hostname. Además se pueden crear grupos con máquinas similares. Los hosts independientes deben estar al comienzo del fichero, antes de cualquier grupo. Tomemos un momento este ejemplo:

mkdir ~/project1 && cd ~/project1
cat <<EOF> inventory
another-manager.ansible.test.com
node-2.ansible.test.com

[manager]
manager.ansible.test.com

[webservers]
manager.ansible.test.com
node-1.ansible.test.com
EOF

Podemos ver la definición de dos hosts al principio, another-manager y node-2. También un grupo llamado managed que contiene un sólo host, no tiene por qué definirse al principio. Y otro grupo llamado webservers que contiene dos hosts. Un host puede existir en múltiples grupos. La idea de los grupos es tener  varias máquinas con  configuraciones idénticas, como en este ejemplo que los webservers podrían instalar Apache y podremos desplegar el mismo contenido en todas. Iremos viendo esto más adelante.

 

Primera ejecución

Una vez definido nuestro fichero de inventario, vamos a realizar una primera ejecución. La manera de hacerlo es:

ansible -i <path/to/custom/inventory> <group|host> -m <module> -a “<module arguments>”

Voy a usar un módulo muy sencillo, el módulo ping, que no requiere argumentos y que intentará ver si ansible puede acceder a los hosts

$ cd project1
$ ansible -i inventory manager -m ping
manager.ansible.test.com | SUCCESS => {
 "changed": false, 
 "ping": "pong"
}

$ ansible -i project1/inventory webservers -m ping
manager.ansible.test.com | SUCCESS => {
 "changed": false, 
 "ping": "pong"
}
node-1.ansible.test.com | SUCCESS => {
 "changed": false, 
 "ping": "pong"
}

$ ansible -i project1/inventory another-manager.ansible.test.com -m ping
another-manager.ansible.test.com | UNREACHABLE! => {
 "changed": false, 
 "msg": "Failed to connect to the host via ssh: ssh: connect to host another-manager.ansible.test.com port 22: Connection refused\r\n", 
 "unreachable": true
}

$ ansible -i project1/inventory nonexistent-manager.ansible.test.com -m ping
 [WARNING]: No hosts matched, nothing to do

Este ejemplo ha mostrado cuatro posibles ejecuciones:

  1. Sobre el grupo manager, con un sólo host
  2. Sobre el grupo webservers, al que pertenecen dos hosts
  3. Sobre el host independiente another-manager, que está en el fichero de inventario, pero no se puede establecer conexión
  4. Sobre un host nonexistent-manager que no se encuentra en el fichero de inventario y que no será usado por ansible

 

¿Atascado? Ansible te ayuda

Ansible ofrece una gran documentación online para convertirte en un experto, pero también pone a nuestra disposición una amplia documentación offline. De hecho disponemos de un comando, ansible-doc, que nos mostrará los distintos módulos y sus usos. Veamos un ejemplo para obtener información de un módulo que nos permita ejecutar un comando en las máquinas remotas.

$ ansible-doc -h
Usage: ansible-doc [options] [module...]

Options:
 -h, --help show this help message and exit
 -l, --list List available modules
 -M MODULE_PATH, --module-path=MODULE_PATH
 specify path(s) to module library (default=None)
 -s, --snippet Show playbook snippet for specified module(s)
 -v, --verbose verbose mode (-vvv for more, -vvvv to enable
 connection debugging)
 --version show program's version number and exit

$ ansible-doc -l|grep command

alternatives        Manages alternative programs for common commands 
asa_command         Run arbitrary commands on Cisco ASA devices. 
at                  Schedule the execution of a command or script file via the at command. 
command             Executes a command on a remote node 
dellos10_command    Run commands on remote devices running Dell OS10 
dellos6_command     Run commands on remote devices running Dell OS6 
dellos9_command     Run commands on remote devices running Dell OS9 
eos_command         Run arbitrary commands on an Arista EOS device 
expect              Executes a command and responds to prompts 
haproxy             Enable, disable, and set weights for HAProxy backend servers using socket commands. 
...

$ ansible-doc command
> COMMAND

 The [command] module takes the command name followed by a list of space-delimited arguments. The given command will be executed on all
 selected nodes. It will not be processed through the shell, so variables like `$HOME' and operations like `"<"', `">"', `"|"', `";"' and
 `"&"' will not work (use the  module if you need these features).

Options (= is mandatory):

- chdir
 cd into this directory before running the command
 [Default: None]
- creates
 a filename or (since 2.0) glob pattern, when it already exists, this step will *not* be run.
 [Default: None]
- executable
 change the shell used to execute the command. Should be an absolute path to the executable.
 [Default: None]
= free_form
 the command module takes a free form command to run. There is no parameter actually named 'free form'. See the examples!
 [Default: None]
- removes
 a filename or (since 2.0) glob pattern, when it does not exist, this step will *not* be run.
 [Default: None]
- warn
 if command warnings are on in ansible.cfg, do not warn about this particular line if set to no/false.
 [Default: True]

$ ansible-doc -s command
- name: Executes a command on a remote node
 action: command
 chdir # cd into this directory before running the command
 creates # a filename or (since 2.0) glob pattern, when it already exists, this step will *not* be run.
 executable # change the shell used to execute the command. Should be an absolute path to the executable.
 free_form= # the command module takes a free form command to run. There is no parameter actually named 'free form'. See the examples!
 removes # a filename or (since 2.0) glob pattern, when it does not exist, this step will *not* be run.
 warn # if command warnings are on in ansible.cfg, do not warn about this particular line if set to no/false.

Así que, supongamos que queremos ejecutar el comando /usr/bin/hostname sólo si el fichero /home/centos/project1/inventory existe, esta sería su ejecución y resultado:

$ cd project1
$ ansible -i inventory webservers -m command -a '/usr/bin/hostname removes=/home/centos/project1/inventory'
node-1.ansible.test.com | SUCCESS | rc=0 >>
skipped, since /home/centos/project1/inventory does not exist

manager.ansible.test.com | SUCCESS | rc=0 >>
manager.ansible.test.com

Organizando tareas con playbooks

Los playbooks son ficheros de texto plano escritos en YAML (YAML Ain’t Markup Language). Su analogía deportiva, con los libros de jugadas que usan los entrenadores de fútbol americano o baloncesto, explica claramente su comportamiento. Son ficheros que contendráns jugadas (plays) que realizarán una serie de tareas. Estas tareas se ejecutan en el orden que están escritas. Un fichero playbook puede contener varios plays, que se ejecutarán en el orden escrito. Ansible sólo puede ejecutar un playbook a la vez, pero podemos enlazarlos con otros, algo que veremos más adelante.

Las tareas (tasks) harán uso de los módulos que vimos en las ejecuciones ad-hoc anteriores. Cada play debe definir los hosts sobre los que se ejecutarán las tareas.

Los playbooks deberán crearse de manera que las tareas no realicen cambios innecesarios en el sistema cuando son ejecutados más de una vez; a esto se le llama idempotencia. Prácticamente todos los módulos suelen actuar de manera idempotente. Por ejemplo, el módulo yum permite instalar un paquete, pero si el paquete ya se encuentra instalado, por una ejecución anterior del playbook, no se intentará reinstalar, sino que esa tarea se saltará. Algunas opciones de los módulos no son idempotentes, así que mejor entender con la documentación qué hace cada opción.

Hay dos módulos que rompen la idempotencia, el módulo command y el módulo shell, por lo que, en la medida de lo posible, evitaremos su uso.

YAML tiene un marcador de comienzo de documento () y otro de finalización (), ambos pueden omitirse cuando escribimos nuestro playbook, aunque por ser elegante, yo los usaré en los ejemplos. Otra característica es que el playbook contendrá una lista de plays; en YAML una lista comienza con ().Cada play estará definido bajo la especificación de un diccionario YAML, donde tendremos atributos: valores. Los atributos más comunes se dividen en 5 categorías:

  1. Nombre
    1. El atributo name definirá el nombre del play
  2. Equipos
    1. El atributo hosts contendrá una lista de hosts o grupos sobre los que se ejecutarán todas las tareas de ese play
  3. Usuario
    1. El atributo remote_user permitirá elegir el usuario con el que se realizará la conexión ssh
    2. El atributo become con valor True/False determinará si se escalan o no privilegios en la máquina remota
    3. El atributo become_method determina el comando para escalar privilegios, por defecto sudo
    4. El atributo become_user determina el usuario al que se escalará
  4. Variables
    1. El atributo vars definirá una lista de variables a ser usada en las tareas del play
  5. Tareas
    1. El atributo tasks contendrá una lista de tareas a ejecutar. Cada tarea tendrá un nombre único en el play, y un módulo con sus correspondientes opciones.

A la hora de escribir playbooks debemos tener en cuenta las reglas de formato de YAML, así como, más importante, la identación. YAML hace uso de un sistema de identación para separar los distintos diccionarios, listas, etc.

Un consejo muy útil, para mí, cuando editemos ansible playbooks (o cualquier otro fichero yaml) es usar VIM como editor y añadir esta línea en  ~/.vimrc:

autocmd FileType yaml,yml setlocal ai ts=2 sw=2 et colorcolumn=3,5,7,9,11
  • autocmd declara el uso automático de comandos tras abrir un fichero
  • FileType determina la extensión de fichero sobre la que se aplicarán los comandos. Separadas por comas podemos declarar múltiples extensiones
  • setlocal hace que los commandos estén disponibles para la ventana o el bufer actual
  • ai o autoindent. Cuando una identación se produce, la siguiente línea mantiene la misma identación
  • ts or tabstop determina el número de columnas que se identarán cuando la tecla Tab se presiona. Por defecto 8
  • sw or shiftwidth aplica la identación definida en tabstop cuando usamos “<” o “>” en el modo comando de VIM
  • et or expandtab aplica la identación definida en tabstop cuando usamos la tecla Tab en el modo inserción de VIM
  • colorcolumn colorea las columnas correspondientes a los números declarados. Esto ayuda a evitar errores de identación

Vamos a crear un playbook, que iremos ampliando poco a poco.

$ cd project1
$ cat playbooks/prepare-WebServers.yaml
---
- name: Install and configure webservers services
  hosts: webservers
  remote_user: centos
  become: True
  tasks:
  - name: Install Apache package
    yum:
      name: httpd
      state: installed
...

Para ejecutar un playbook utilizaremos un comando diferente. ansible es el comando para ejecutar módulos ad-hoc, es decir, un módulo por cada ejecución. ansible-playbook.

$ cd project1
$ ansible-playbook -i inventory playbooks/prepare-WebServers.yaml 

Antes de ejecutar un playbook, podemos hacer uso de dos opciones que nos ayudarán a prevenir problemas. La opción –syntax-check chequea la sintaxis antes de ejecutar el playbook.

$ cd project1
$ ansible-playbook --syntax-check -i inventory playbooks/prepare-WebServers.yaml 
ERROR! Syntax Error while loading YAML.


The error appears to have been in '/home/centos/project1/playbooks/prepare-WebServers.yaml': line 3, column 8, but may
be elsewhere in the file depending on the exact syntax problem.

The offending line appears to be:

- name:Install and configure webservers services
 hosts: webservers
 ^ here

En ocasiones los errores no son muy claros, en este caso no se encuentra en la línea hosts como parece, sino en la línea previa, hace falta un espacio en blanco tras name:

Una vez corregido, podremos hace uso de la opción -C, que ejecutará el playbook en un “dry-run“, es decir, actuará como si lo ejecutase sólo para verificar que puede hacerlo

$ cd project1
$ ansible-playbook -C -i inventory playbooks/prepare-WebServers.yaml 

PLAY [Install and configure webservers services] *******************************

TASK [setup] *******************************************************************
ok: [manager.ansible.test.com]
ok: [node-1.ansible.test.com]

TASK [Install Apache package] **************************************************
changed: [node-1.ansible.test.com]
changed: [manager.ansible.test.com]

PLAY RECAP *********************************************************************
manager.ansible.test.com : ok=2 changed=1 unreachable=0 failed=0 
node-1.ansible.test.com : ok=2 changed=1 unreachable=0 failed=0 

$ rpm -q httpd
package httpd is not installed

Una vez verificado, vamos a ejecutarlo

$ cd project1
$ ansible-playbook -i inventory playbooks/prepare-WebServers.yaml 

PLAY [Install and configure webservers services] *******************************

TASK [setup] *******************************************************************
ok: [manager.ansible.test.com]
ok: [node-1.ansible.test.com]

TASK [Install Apache package] **************************************************
changed: [node-1.ansible.test.com]
changed: [manager.ansible.test.com]

PLAY RECAP *********************************************************************
manager.ansible.test.com : ok=2 changed=1 unreachable=0 failed=0 
node-1.ansible.test.com : ok=2 changed=1 unreachable=0 failed=0 

$ rpm -q httpd
httpd-2.4.6-40.el7.centos.4.x86_64

Vamos a ampliar el playbook. Añadiremos varias tareas y las separaremos en bloques. El playbook se expresa por sí mismo:

$ cat playbooks/prepare-WebServers.yaml 
---
- name: Install and configure webservers services
  hosts: webservers
  remote_user: centos
  become: True
  tasks:

  - block:
    - name: Install Apache package
      yum:
        name: httpd
        state: installed
    - name: Install Firewalld package
      yum:
        name: firewalld
        state: installed

  - block:
    - name: Start and enable Apache service
      service:
        name: httpd
        enabled: true
        state: started
    - name: Start and enable Firewalld service
      service:
        name: firewalld
        enabled: true
        state: started

  - block:
    - name: Enable firewall rules for Apache
      firewalld:
        state: enabled
        service: http
        permanent: true
        immediate: true

  - block:
    - name: create index file
      shell: echo 'Hello World!' > /var/www/html/index.html
...

Y al ejecutarlo:

$ ansible-playbook -i inventory playbooks/prepare-WebServers.yaml 

PLAY [Install and configure webservers services] *******************************

TASK [setup] *******************************************************************
ok: [node-1.ansible.test.com]
ok: [manager.ansible.test.com]

TASK [Install Apache package] **************************************************
changed: [manager.ansible.test.com]
changed: [node-1.ansible.test.com]

TASK [Install Firewalld package] ***********************************************
changed: [manager.ansible.test.com]
changed: [node-1.ansible.test.com]

TASK [Start and enable Apache service] *****************************************
changed: [node-1.ansible.test.com]
changed: [manager.ansible.test.com]

TASK [Start and enable Firewalld service] **************************************
changed: [manager.ansible.test.com]
changed: [node-1.ansible.test.com]

TASK [Enable firewall rules for Apache] ****************************************
ok: [node-1.ansible.test.com]
ok: [manager.ansible.test.com]

TASK [create index file] *******************************************************
changed: [manager.ansible.test.com]
changed: [node-1.ansible.test.com]

PLAY RECAP *********************************************************************
manager.ansible.test.com : ok=7 changed=5 unreachable=0 failed=0 
node-1.ansible.test.com : ok=7 changed=5 unreachable=0 failed=0

$ curl node-1.ansible.test.com
Hello World!

 

Y hasta aquí esta introducción a Ansible 2. En un post futuro veremos algunos aspectos tales como variables, tareas enlazadas, playbooks enlazados, roles y… Ansible Galaxy. Nos vemos.

One thought on “Introducción a Ansible 2 – vol.1

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.