Patron de Diseño en Puppet

Diseño en Puppet – Roles y perfiles Por Craig Dunn, Artículo original Actualizado el 15 de febrero Desde que escribí este post alguno de los conceptos se hicieron bastante populares y generaron comentarios y preguntas en la comunidad. Recientemente di una charla sobre este tema en el Puppet Camp en Estocolmo,

Recent Posts

Diseño en Puppet – Roles y perfiles

Por Craig Dunn, Artículo original

Actualizado el 15 de febrero

Desde que escribí este post alguno de los conceptos se hicieron bastante populares y generaron comentarios y preguntas en la comunidad. Recientemente di una charla sobre este tema en el Puppet Camp en Estocolmo, y espero haberlo explicado mejor que lo que leerán a continuación. Las diapositivas están disponibles aquí, y pronto subiré un video de YouTube.

Introducción

Has instalado Puppet, bajado algunos módulos del Forge, probablemente has escrito algunos tú mismo. Y ahora qué? Comenzaste aplicando esos módulos a tus nodos y vas camino a la súper genialidad de las implementaciones automáticas. Nos adelantamos más o menos un año y vemos que tu infraestructura ha crecido considerablemente, las necesidades de tu empresa se han vuelto diversas y complejas, y tus arquitectos han diseñado soluciones técnicas para resolver problemas comerciales, con poca consideración de cómo podrían ser implementadas. Se ven geniales en los diagramas, pero tú tienes que adaptarlas a Puppet. Por experiencia propia, esto generalmente lleva a querer forzar las cosas, y la declaración if se convierte en la herramienta a la cual recurrir porque no puedes hacerlo de otra forma. Seguramente estás pensando que es momento de descartar lo que tienes y refactorizar. Es tiempo de pensar en modelos de diseño a un nivel superior para no sufrir tanto.

En la comunidad existen muchas guías muy útiles acerca de los patrones de diseño para módulos de Puppet, el manejo de información configurable y estructura de clase; pero todavía veo gente luchando para vincular los componentes de sus manifiestos de Puppet en una unidad. Me parece que esto es un problema de falta de diseño de alto nivel del código. Este post intenta explicar uno de estos modelos de diseño al cual llamo ”Roles/Perfiles” el cual ha funcionado bastante bien a mi criterio para resolver algunos de los problemas más comunes encontrados cuando la infraestructura crece en tamaño y complejidad y en consecuencia, la necesidad de un buen diseño de código se vuelve primordial.

El modelo de diseño expuesto aquí no es en absoluto una sugerencia de cómo se debe diseñar en Puppet; es un ejemplo de modelo que yo he usado y me ha dado resultado. He visto muchos diseños muy variados, algunos buenos y otros malos, éste es uno de varios (estoy muy interesado en conocer otros modelos). El punto de esto es demostrar los beneficios de agregar una capa de abstracción delante de tus módulos.

Qué es lo que intentamos resolver

He pasado mucho tiempo intentando dar con lo que veo como las fallas de diseño más comunes en el código de manifiestos de Puppet. Una fuente de problemas es que los usuarios pierden mucho tiempo diseñando módulos geniales y luego los incluyen directamente al nodo. Esto puede funcionar, pero cuando se trata de infraestructuras grandes y complejas, se torna engorroso y acabas con mucha lógica a nivel de nodo en los manifiestos.

Considera una red que consiste en múltiples tipos de servidores. Éstos tendrán en común algo de su configuración, algunos subconjuntos de servidores también compartirán otra parte de su configuración mientras otras serán aplicables sólo a ese tipo de servidor. En este ejemplo muy simple tenemos tres tipos de servidores: Un servidor web de desarrollo (www1), que requiere una instancia mysql local y un nivel de logueo de PHP seteado en debug; un servidor web de producción (www2), que no utiliza una instancia mysql local, requiere memcache, y tiene un nivel de logueo PHP estándar; por último, un servidor de correo (smtp1). Si tienes una relación nodo/módulo plana sin un nivel de abstracción, tu archivo de nodos comienza a verse así:

node www1 { 
 include networking
 include users
 include tomcat
 include jdk
include mysql
 include memcache
 include apache
 class { "php": 
    loglevel  => "debug"
 }
}
 
node www2 { 
 include networking
 include users
 include tomcat
 include jdk
 include memcache
 include apache
 include php
}

node smtp1 { 
 include networking
  include users
  include exim
}

Nota: Si ya estás pensando en ENCs, lo vamos a ver luego.

Como puedes ver, los módulos de red y de usuarios son universales en todas las cajas; Apache, Tomcat y JDK se usan en todos los servidores web. Algunos servidores web tienen mysql y opciones de nivel de logueo PHP que varían según el tipo de servidor que sea.

En este punto, la mayoría de la gente intenta simplificar sus manifiestos usando herencia de nodo; en este ejemplo que es tan simple podría ser suficiente, pero es viable hasta cierto punto. Si tu entorno crece a cientos o incluso miles de servidores, está formado por más de veinte o treinta tipos diferentes de servidores (algunos con atributos compartidos y diferencias sutiles) y se extiende por múltiples entornos, es probable que el resultado final sea una maraña inmanejable de herencia de nodos. Los nodos pueden heredar de un solo nodo, lo que será restrictivo en algunos casos extremos.

Agregar un nivel de abstracción superior

Una forma que encontré para minimizar la complejidad de la definición de los nodos, y para que el manejo de los matices entre los diferentes tipos de servidores y escenarios excepcionales sea mucho más fácil, es añadir una capa (o en este caso, dos capas) de separación entre los nodos y los módulos que terminan llamando. Los llamo roles y perfiles.

Piensa por un momento en cómo representarías estos servidores sin haber escrito un manifiesto de Puppet. No dirías “www1 es un servidor que contiene mysql, tomcat, apache, PHP con nivel de logueo debug, configuración de red y usuarios” en un diagrama de red de alto nivel. Dirías, en todo caso, “www1 es un servidor web de desarrollo”, que es en realidad toda la información que quiero aplicar directamente al nodo.

Luego de analizar todos nuestros nodos, llegamos a tres definiciones distintas de lo que un servidor puede ser: un servidor web de desarrollo, un servidor web de producción, y un servidor de correo. Estos son los roles del servidor, que describen lo que el servidor representa en el mundo real. En este modelo de diseño sólo puede haber un rol, no puede ser dos cosas al mismo tiempo. Si tu empresa tiene un caso extremo en que los servidores web QA sean iguales a un servidor de producción, pero incorporan software extra para pruebas de rendimiento, entonces acabas de definir otro rol, un servidor web QA.

Ahora prestemos atención a qué debe contener un rol. Si estuvieras describiendo el rol “servidor web de desarrollo” lo más probable sería decir “Un servidor de desarrollo tiene un stack de Tomcat, un servidor web y un servidor de base de datos local”. En este punto, comenzamos a definir los perfiles.

A diferencia de los roles, que son nombrados con una representación de funciones de servidor más humana, un perfil incorpora componentes individuales para representar un stack lógico de tecnología. En el ejemplo anterior, el perfil “stack de Tomcat” está compuesto por Tomcat y JDK, mientras que el perfil del servidor web está compuesto por httpd, memcache y php. En Puppet, estos componentes de niveles inferiores están representados por módulos.

Ahora, las definiciones de los módulos se ven mucho más simples y representan sus roles en el mundo real.

node www1 { 
include role::www::dev
}
 
node www2 { 
include role::www::live
}
 
node smtp1 { 
include role::mailserver
}

Los roles son, simplemente, colecciones de perfiles que proveen un mapeo razonable entre la lógica humana y la tecnológica. En este caso, los roles pueden verse así:

class role { 
 include profile::base
}
 
class role::www inherits role { 
# Todos los servidores WWW obtienen tomcat
include profile::tomcat
}
 
class role::www::dev inherits role::www { 
 include profile::webserver::dev
include profile::database
}

class role::www::live inherits role::www { 
include profile::webserver::live
}

class role::mailserver inherits role { 
include profile::mailserver
}

Depende de ti elegir o no usar herencia de clases como yo lo hice; algunos evitan la herencia por completo y otros abusan de ella. Personalmente, creo que funciona para establecer roles y perfiles, y para minimizar la duplicación.

Estos perfiles incluidos anteriormente, se verían como los siguientes:

class profile::base { 
include networking
include users 
}
 
class profile::tomcat { 
class { "jdk": } 
class { "tomcat": } 
}
 
class profile::webserver { 
*# Configuración para todos los servidores web*
class { "httpd": } 
class { "php": } 
 class { "memcache": } 
}
 
class profile::webserver::dev inherits profile::webserver { 
 Class["php"] { 
    loglevel   => "debug"
 }
}
 
class profile::webserver::live inherits profile::webserver { 
*# Cualquier cosa específica de servidores web de producción aquí*
}
 
class profile::database {
 class { "mysql": } 
}
 
class profile::mailserver { 
 class { "exim": } 
}

En resumen, las “reglas” envuelven mi diseño se pueden simplificar en:

  • Un nodo incluye uno y sólo un rol.
  • Un rol incluye uno o más perfiles para definir el tipo de servidor
  • Un perfil incluye y administra módulos para definir un stack lógico de tecnología
  • Los módulos manejan recursos
  • Los módulos deben ser sólo responsables del manejo de los componentes para los que fueron escritos.

Aclaremos qué se quiere decir con “módulos”

He hablado sobre perfiles y roles como si fueran clases especiales, y los módulos otra cosa. En realidad, todas estas clases pueden y deben ser modularizadas. Yo realizo una distinción lógica entre los módulos de perfil y de rol, respecto del resto de las cosas (por ejemplo, módulos que proveen recursos).

Otras cosas útiles para hacer con perfiles

Hasta ahora he demostrado el uso de perfiles como colecciones de módulos, pero también tiene otros usos. Como regla de oro, no defino ningún recurso directamente desde los roles o perfiles, ya que ése es trabajo de los módulos. Sin embargo, realizo recursos virtuales y ocasionalmente, un encadenamiento de recursos que pueda resolver problemas que de otra forma hubiera significado editar módulos y otras funcionalidades que no se ajustan a el scope de un módulo individual. Agregar alguna de estas funcionalidades al nivel modular, reducirá la reusabilidad y portabilidad del módulo.

Digamos que, hipotéticamente, tengo un módulo; llamémoslo foo en nombre de la originalidad. El módulo foo provee un tipo de servicio llamado foo; en mi implementación tengo otro módulo llamado mounts que declara algunos tipos de recursos de mounts. Quiero que todos los recursos de mount sean iniciados antes de que el servicio foo, ya que si éste inicia sin los sistemas de archivos montados, el servicio fallará. Iré incluso más allá y diré que ese foo es un módulo del Forge que realmente no quiero (y no debería tener que) editar; entonces ¿dónde coloco esta configuración? Aquí es donde tener el nivel de abstracción de los perfiles es útil. El módulo foo está perfectamente codificado, es el caso de uso determinado a partir del propio stack que requiere que los puntos de montaje existan antes que el servicio foo; entonces como el stack está definido en el perfil, allí es donde debo especificarlo. Un ejemplo de esto:

class profile::foo { 
  include mounts
  include foo
  Mount <| |>  ->  Service['foo']
}

Se sabe que los buenos módulos son aquellos que no necesitan ser editados. Veo bastante seguido gente reacia a usar módulos Forge porque su configuración requiere configuraciones periféricas o dependencias no incluidas en el módulo. Los módulos existen para administrar recursos directamente relacionados a los componentes para los que fueron escritos; por ejemplo, alguien puede elegir editar un módulo de mysql del Forge porque su configuración tiene dependencias en que MMM sea instalado después de MySQL (puramente hipotético). El módulo mysql no es el lugar para hacerlo; mysql y mmm son entidades separadas y deben ser configuradas y contenidas dentro de sus propios módulos; vincularlas es algo que se define en el stack, y nuevamente, este es el lugar en donde entran tus perfiles..

class profile::database { 
  class { "mysql": }
  class { "mmm": } 
  Package["mysql"] -> Package["mmm"]
}

Este enfoque es también potencialmente útil para los que utilicen Hiera. Aunque Hiera y Puppet van a estar mucho más unidos a partir de Puppet 3.0, al momento la gente que escribe módulos para el Forge tiene que hacerlos funcionar con o sin Hiera, y la gente que usa Hiera tiene que editar los módulos que no están habilitados. Toma un módulo hipotético del Forge llamado fooserver, este módulo expone una clase parametrizada que tiene una opción para port, yo quiero proveer esta variable desde Hiera pero el módulo no lo soporta; puedo agregar esta función en el perfil sin necesidad de editar el módulo.

class profile::fooserver { 
  $fooport = hiera("fooserver_port")
  class { "fooserver": 
      port  => $fooport 
  }
}

¿Qué pasa con el uso de un ENC?

Seguramente te estarás preguntando por qué no he mencionado el uso de un ENC (External Node Classifier). Los ejemplos anteriores no utilizan ningún tipo de ENC, pero la lógica detrás de agregar una capa de separación entre los nodos y los módulos es la misma. Puedes decidir usar un ENC para determinar qué rol incluir en un nodo, o puedes construir/configurar un ENC para ejecutar toda la lógica y devolver la lista de componentes (módulos) a incluir. Prefiero usar un ENC en lugar de definiciones de nodos para determinar qué rol incluir, y mantener los roles reales y la lógica de los perfiles dentro de Puppet. Mi razón principal para esto es que obtengo un control mucho mejor de cosas como el encadenamiento de recursos, sustitución de clases e integración con cosas como por ejemplo Hiera al nivel de los perfiles, y esto ayuda superar algunos casos extremos y difíciles, y requerimientos complejos.

Sumario

Ninguno de los ejemplos anteriores es inalterable, lo que espero haber demostrado que agregando una capa de abstracción en el diseño del código Puppet se pueden obtener beneficios significativos que evitarán inconvenientes cuando se comience a lidiar con configuraciones extremadamente complejas, diversas y a larga escala. Esto incluye:

  • Reducción de la complejidad de configuración en el nivel de los nodos.
  • Terminología de roles de mundo real, mejora la visibilidad a simple vista de qué es lo que hace el servidor.
  • La definición de stacks lógicos de tecnología (perfiles) da una gran flexibilidad para casos extremos.
  • Los perfiles proveen un área para agregar funcionalidad que abarca múltiples módulos, como el encadenamiento de recursos.
  • Los módulos pueden ser granulares y seculares, y pueden unirse en perfiles; en consecuencia, reducir la necesidad de editar módulos directamente.
  • Duplicación de código reducida.

Utilizo Hiera para manejar toda la información de configuración de entorno, de la cual no daré detalles en este post. Por lo tanto, en un nivel alto, mi diseño Puppet puede ser representado como:

Como dije anteriormente, esta no es la forma de diseñar en Puppet, pero sí un ejemplo. El propósito de este post es explorar el diseño de alto nivel para implementaciones de Puppet más grandes y complejas. Me encantaría conocer otros modelos de diseño que la gente haya usado con resultados exitosos o no, y qué problemas esto te ha resuelto (o dado ); así que por favor contáctame y cuéntame tus propios ejemplos.