Dependencia de roles y gestión de errores con Ansible

0
6617

En este tutorial veremos cómo gestionar las dependencias de los roles de un playbook de ansible, y como configurar una tarea para que sea idempotente en múltiples ejecuciones del playbook.

Índice de contenidos

1. Introducción

Recientemente he leído Ansible up and running: Automating configuration management and deployment the easy way de Lorin Hochstein en el que he aprendido algunas cosas chulas que quería compartir con vosotros. Vamos al lío 😀

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro Retina 15′ (2.5 Ghz Intel Core I7, 16GB DDR3).
  • Sistema Operativo: Mac OS El Capitán 10.11.2
  • Vagrant 1.8.1
  • Ansible 1.9.3

3. Instalación del entorno

La instalación del entorno es muy simple: hay que instalar Vagrant y Ansible y elegir el directorio de trabajo en el que deseamos realizar la prueba. En este tutorial explico como instalar estas herramientas.

4. Gestión de dependencias en un rol

Al crear un playbook de Ansible en mi proyecto, aprovisionabamos la máquina con los roles ordenados, de forma que las dependencias fueran en un módulo anterior al módulo que las necesitaba. Esto puede suponer un problema ya que necesitas tener un conocimiento previo del funcionamiento del aprovisionamiento para ejecutar tareas específicas.

Por ejemplo, si instalamos Sonar con PostgreSQL y tú quieres probar (o reutilizar) el módulo de Sonar en otro proyecto, necesitarías saber que depende del rol que instala PostgreSQL para su funcionamiento.

Con la gestión de dependencias de los roles lo que se pretende es justo evitar eso. Si quieres ejecutar un rol, ese rol sabe todas las dependencias que necesita para que pueda ser ejecutado fácilmente. Vamos a realizar un ejemplo donde comprobamos la gestión de dependencias del rol sonar. Para ello nos creamos nuestra carpeta Ansible dónde van tanto nuestros playbooks como nuestros roles, y nos servimos de Vagrant para provisionar las máquinas.

En el directorio elegido ejecutamos el comando Vagrant init para configurar el aprovisionamiento de nuestra máquina virtual y lo dejamos de la siguiente manera:

Vagrantfile
Vagrant.configure(2) do |config|
  config.vm.box = "ubuntu/trusty64"

  config.vm.network "forwarded_port", guest: 9000, host: 9000

  config.vm.provider "virtualbox" do |vb|
    # Customize the amount of memory on the VM:
    vb.memory = "2048"
  end

  config.vm.define "prueba" do |prueba|
    prueba.vm.hostname = "prueba"
    prueba.vm.provision "ansible" do |ansible|
      ansible.verbose = 'vvv'
      ansible.playbook = "ansible/playbook.yml"
      ansible.inventory_path = "ansible/environments/development/inventory"
    end
  end
end

En este fichero le decimos la box de la que queremos partir, hacemos la redirección al puerto 9000 de la máquina virtual y configuramos la memoria que va a tener la máquina virtual. Por último, definimos el aprovisionamiento indicando el playbook y el inventory que vamos a utilizar.

Comenzamos creando el playbook:

ansible/playbook.yml
---

# file: playbook.yml

- hosts: sonar
  sudo: yes
  gather_facts: no
  roles:
      - sonar

Este es un playbook sencillo en el que se ejecuta sólo un rol. Vamos a crear este rol ahora 😀

ansible/roles/sonar/tasks/main.yml
---

# file: /roles/sonar/tasks/main.yml

- name: download sonar
  get_url: url="{{sonar.download_url}}" dest="/tmp/{{sonar.archive}}"
  tags: sonar

- name: create sonar group
  group: name=sonar state=present
  tags: sonar

- name: create sonar user
  user: name=sonar comment="Sonar" group=sonar
  tags: sonar

- name: extract sonar
  unarchive: src="/tmp/{{sonar.archive}}" dest=/opt copy=no
  tags: sonar

- name: move sonar to its right place
  shell: mv /opt/{{sonar.version_dir}} {{sonar.home}} chdir=/opt
  tags: sonar

- name: change ownership of sonar dir
  file: path="{{sonar.home}}" owner=sonar group=sonar recurse=yes
  tags: sonar

- name: copy sonar properties
  template: src=sonar.properties dest="{{sonar.home}}/conf/sonar.properties"
  tags: sonar

- name: make sonar runned by sonar user
  replace: dest="{{sonar.home}}/bin/linux-x86-64/sonar.sh" regexp="#RUN_AS_USER=(.*)$" replace="RUN_AS_USER=sonar"
  tags: sonar

- name: add sonar links for service management
  file: src="{{sonar.home}}/bin/linux-x86-64/sonar.sh" dest="{{item}}" state=link
  with_items:
    - /usr/bin/sonar
    - /etc/init.d/sonar
  tags: sonar

- name: ensure sonar is running and enabled as service
  service: name=sonar state=restarted enabled=yes
  tags: sonar

- name: create database for sonar
  postgresql_db: name="{{datasource.sonar_dbname}}" encoding=UTF-8 lc_collate=es_ES.UTF-8 lc_ctype=es_ES.UTF-8
  sudo: yes
  sudo_user: postgres
  tags:
      - sonar
      - sonardb

- name: add user to database
  postgresql_user: db="{{datasource.sonar_dbname}}" name="{{datasource.sonar_dbuser}}" password="{{datasource.sonar_dbpassword}}" priv=ALL
  sudo: yes
  sudo_user: postgres
  tags:
      - sonar
      - sonardb

Este es un rol con el que descargamos sonar, creamos su usuario y su grupo y nos aseguramos de que esté funcionando como servicio. Este rol necesita de un fichero de configuración que tenemos definido en el siguiente fichero:

ansible/roles/sonar/templates/sonar.properties
sonar.jdbc.username={{datasource.dbuser}}
sonar.jdbc.password={{datasource.dbpassword}}

sonar.jdbc.url=jdbc:postgresql://localhost:5432/{{datasource.dbname}}

sonar.jdbc.maxActive=20
sonar.jdbc.maxIdle=5
sonar.jdbc.minIdle=2
sonar.jdbc.maxWait=5000
sonar.jdbc.minEvictableIdleTimeMillis=600000
sonar.jdbc.timeBetweenEvictionRunsMillis=30000

sonar.web.context=/sonar

sonar.web.port=9000

Este fichero contiene las propiedades del Sonar, así como la configuración para que la base de datos no se cree en memoria.

Pues ya tendríamos nuestro sonar montado, aunque si nosotros tratáramos de ejecutar este rol fallaría porque le falta la configuración del idioma, el programa unzip para descomprimir el sonar y PostgreSQL. Nosotros necesitamos crear estos roles pero no queremos modificar el playbook, ya que nosotros lo que queremos instalar es Sonar. Si Sonar necesita alguna dependencia debería de ser este quien las gestionara. ¿Cómo hacemos eso? añadiendo un nuevo fichero dentro del rol sonar que sea el que conozca la dependencia:

ansible/roles/sonar/meta/main.yml
---

# file: roles/sonar/meta/main.yml

dependencies:
    - {role: locales}
    - {role: unzip}
    - {role: java8}
    - {role: postgres}

Con este fichero le estamos indicando que para poder ejecutar el rol de sonar necesita ejecutar antes los siguientes roles. La ventaja es que esta información está dentro del playbook de sonar de forma que si no tiene ese rol creado no será capaz de ejecutar sonar por falta de dependencia.

Creamos el rol de locales encargado de actualizar los idiomas y aplicarlos:

ansible/roles/locales/tasks/main.yml
---
# file: roles/locales/tasks/main.yml

- name: ensure apt cache is up to date
  apt: update_cache=yes

- name: ensure the language packs are installed
  apt: name={{item}}
  with_items:
  - language-pack-en
  - language-pack-es

- name: reconfigure locales
  command: sudo update-locale LANG=en_US.UTF-8 LC_ADDRESS=es_ES.UTF-8 LC_COLLATE=es_ES.UTF-8 LC_CTYPE=es_ES.UTF-8 LC_MONETARY=es_ES.UTF-8 LC_MEASUREMENT=es_ES.UTF-8 LC_NUMERIC=es_ES.UTF-8 LC_PAPER=es_ES.UTF-8 LC_TELEPHONE=es_ES.UTF-8 LC_TIME=es_ES.UTF-8

- name: ensure apt packages are upgraded
  apt: upgrade=yes

Instalamos Java en la máquina virtual porque es un prerrequisito de Sonar:

ansible/roles/java8/tasks/main.yml
---

# file: /roles/java8/tasks/main.yml

- name: add Java repository to sources
  apt_repository: repo='ppa:webupd8team/java'
  tags: java

- name: autoaccept license for Java
  debconf: name='oracle-java8-installer' question='shared/accepted-oracle-license-v1-1' value='true' vtype='select'
  tags: java

- name: update APT package cache
  apt: update_cache=yes
  tags: java

- name: install Java 8
  apt: name=oracle-java8-installer state=latest install_recommends=yes
  tags: java

- name: set default environment variable
  apt: name=oracle-java8-set-default
  tags: java

Creamos el rol de unzip para instalarlo:

ansible/roles/unzip/tasks/main.yml
---

# file: roles/unzip/tasks/main.yml

- name: install unzip command
  apt: name=unzip state=present

Creamos también el rol de postgresql:

ansible/roles/postgres/tasks/main.yml
---
# file: /roles/postgres/task/main.yml

- name: ensure PostgreSQL are installed
  apt: name={{item}} update_cache=Yes
  with_items:
      - postgresql-{{postgres.version}}
      - postgresql-contrib-{{postgres.version}}
      - python-psycopg2

- name: ensure client's encoding is UTF-8
  lineinfile:
      dest: /etc/postgresql/{{postgres.version}}/main/postgresql.conf
      backup: yes
      insertafter: "#client_encoding = sql_ascii"
      line: "client_encoding = utf8"

- name: ensure PostgreSQL is running
  service: name=postgresql state=restarted enabled=yes

# Only for develoment
- name: ensure PostgreSQL is accesible from other hosts
  lineinfile:
      dest: /etc/postgresql/{{postgres.version}}/main/postgresql.conf
      insertafter: "#listen_addresses = 'localhost'"
      line: "listen_addresses = '*'    # Development environment!!!"

- name: ensure user can access database from other hosts
  lineinfile:
      dest: /etc/postgresql/{{postgres.version}}/main/pg_hba.conf
      line: "host    {{datasource.dbname}}           {{datasource.dbuser}}           all                     md5    # Development environment!!!"

- name: ensure database is created
  sudo_user: postgres
  postgresql_db:
    name: "{{datasource.dbname}}"
    encoding: UTF-8
    lc_collate: es_ES.UTF-8
    lc_ctype: es_ES.UTF-8

- name: ensure user has access to database
  sudo_user: postgres
  postgresql_user:
    db: "{{datasource.dbname}}"
    name: "{{datasource.dbuser}}"
    password: "{{datasource.dbpassword}}"
    priv: ALL

- name: restart postgresql
  service: name=postgresql state=restarted

¡Qué no se nos olvide almacenar las variables de ejecución del playbook!

ansible/environments/ndevelopment/group_vars/sonar
---

postgres:
    version: 9.3

sonar:
    download_url: http://downloads.sonarsource.com/sonarqube/sonarqube-5.1.1.zip
    archive: sonar.zip
    version_dir: sonarqube-5.1.1
    home: /opt/sonar
    jdbc_driver: postgres
    jdbc_host: localhost
    jdbc_port: 5432

datasource:
    sonar_dbuser: sonar
    sonar_dbpassword: sonarpassword
    sonar_dbname: sonardb

Si ahora levantamos la máquina virtual veremos como primero se ejecutan las dependencias del módulo y por último se ejecuta sonar. Después podremos acceder a través de nuestro navegador preferido ejecutando localhost:9000/sonar

5. Gestión del cambio de tareas

Ahora vamos a ver otra cosa chula de ansible. Normalmente cuando ejecutamos un playbook queremos que este sea idempotente, es decir, que deje la máquina exactamente en el mismo estado independientemente de las veces que se ejecute ese playbook en el host remoto. La mayoría de los módulos de ansible soporta la idempotencia, aunque si nosotros ejecutamos comandos directamente en el servidor remoto estos no suelen ser idempotentes. Como prueba, si nosotros volvemos a provisionar la máquina que acabamos de crear, nos daría un error en una tarea de sonar precisamente porque no es idempotente. Esta tarea ejecuta el comando mv a un directorio y cuando lo realiza por segunda vez, como el directorio ya se encuentra allí, nos da un error.

Ansible también nos ofrece una solución a esto por medio de las cláusulas «changed_when» y «failed_when» donde puedes especificar de forma exacta cuando consideras que esa tarea ha fallado o ha cambiado el estado de la máquina remota. Este método es mucho menos intrusivo que poner directamente un «ignore_errors: yes» ya que nosotros decidimos qué es cambio y qué es fallo.

Para verlo en funcionamiento cambiemos la tarea «move sonar to its right place» del rol de sonar para que quede la siguiente manera:

ansible/roles/sonar/tasks/main.yml
- name: move sonar to its right place
  shell: mv /opt/{{sonar.version_dir}} {{sonar.home}} chdir=/opt
  register: result
  failed_when: '"Directory not empty" not in result.stderr'
  tags: sonar

En primer lugar necesitamos registrar la salida del comando, en nuestro caso en una variable result. Luego indicamos como condición del fallo que la tarea sea considerado un error si en la salida pone algo diferente a que el directorio no esté vacío, porque esto quiere decir que la tarea ya se realizó previamente.

Volvemos a ejecutar el aprovisionamiento de ansible (vagrant provision) y comprobamos como ahora no falla.

6. Conclusiones

Espero que este tutorial sirva para organizar mejor nuestros playbook y para gestionar los errores en tareas no idempotentes, como ejecutar comandos directamente en la máquina remota. Puedes obtener el código del ejemplo desde mi repositorio de github.

7. Referencias

  • Manejo de errores con ansible.

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad