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
- 2. Entorno
- 3. Instalación del entorno
- 4. Gestión de dependencias en un rol
- 5. Gestión del cambio de tareas
- 6. Conclusiones
- 7. Referencias
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.