Páginas

miércoles, 18 de septiembre de 2019

Mejorar el canal DevOps: Cómo tratar la seguridad y proteger el canal de despliegue


En “The DevOps handbook” de Jez Humble, John Willis, Patrick Debois y Gene Kim publicado por IT Revolution Press en 2016, tratan dos temas en la parte VI del libro. Uno de ellos es como integrar los trabajos que incorporan seguridad a los sistemas en Dev y en Ops, el otro como proteger el entorno del canal de despliegue formado por servidores en los que residen los entornos en los que se construye y explota el software. En este post se hace un resumen de lo propuesto en este texto.

Tratando la seguridad

Un pretesto para no implementar los principios y patrones de DevOps es a menudo "la seguridad de la información y el cumplimiento de las normas no nos lo permiten". Paradójicamente Devops puede ser una de las mejores formas de integrar la seguridad de la información en el trabajo diario.

El ratio de ingenieros de desarrollo, de operaciones y de seguridad en una organización suele ser respectivamente de 100:10:1. Cuando seguridad no dispone de procesos de automatización y de gestión de la seguridad en el día a día de Dev y Ops, lo único que puede hacer es validar si lo desarrollado y desplegado cumple con ciertos niveles de seguridad. Esto no es muy útil ya que se obtiene información de cómo incorporar seguridad cuando todo está ya construido y es difícil o imposible de resolver.
Para que esto no ocurra se recomienda tomar las siguientes acciones:
  • Integrar a seguridad en las demos

Invitando a los especialistas en seguridad a las demos cuando finalizan los sprints, se consigue que asesoren a los desarrolladores en etapas tempranas de la construcción del código. De esta forma, el desarrollo se hará teniendo en cuenta las  necesidades de seguridad. Esto es más sencillo y barato que esperar hasta que todo está construido y después modificarlo para incorporar seguridad.
  • Integrar a ingenieros de seguridad en seguimiento de defectos y en reuniones Post-Morten

Tras el acaecimiento de un fallo se debe organizar una reunión post-morten para analizar qué motivos lo provocaron y que solución se ha adoptado. Mediante la información recopilada en estas reuniones la organización va atesorando una fuente de conocimiento que la ayudará a trabajar mejor en el futuro. El objetivo es que no se vuelvan a provocar fallos parecidos y si se producen saber cúal puede ser la solución a aplicar.
Con el objetivo de tener en cuenta en los aspectos de seguridad, es adecuado invitar a ingenieros de seguridad a estas reuniones post-morten.
  • Añadir mecanismos y herramientas que ayuden a asegurar que las aplicaciones y entornos están seguros.

Se tienen que proporcionar librerías y servicios de seguridad que proporcionen autenticación, autorización, gestión de contraseñas, encriptación de datos, etc. Se pueden incluir:

    - Modos de acceso seguro (por ejemplo, 2FA, bcrypt password. logging)
    - Herramientas para almacenar y proteger tokens, contraseñas, etc. (Por ejemplo: usando                        herramientas cómo Vault, sneaker, Keywhiz, credstash, Trousseau, Red October)
    - Paquetes para securizar (por ejemplo, NTP para sincronización de la hora de servidores,                     versiones seguras de OpenSSL, OSSEC para detección de intrusos)

  • Integrar a seguridad en el canal DevOps 

Antes de DevOps para conseguir aplicaciones seguras se revisaban las cuestiones de seguridad una vez que el desarrollo estaba completado. Normalmente el resultado eran informes con decenas o cientos de hojas en los que se indicaban las vulnerabilidades de la aplicación. Muchas veces abordar los cambión necesarios para eliminar todas estas vulnerabilidades era imposible. Solucionar esto se consigue integrando a los especialistas de seguridad en etapas tempranas del desarrollo y la implantación  que asesoren a Dev y Ops. 
  • Asegurar la seguridad de la aplicación

Consiste en establecer los medios de aseguramiento necesarios para que cuando el código llegue a producción esté en las condiciones óptimas y cumpla con todas las normas de seguridad. Para ello es oportuno establecer validaciones y verificaciones que pueden ser:
- Análisis estáticos. Con alguna herramienta revisar el código buscando debilidades, puertas traseras y código potencialmente malicioso.
- Análisis dinámicos. Los análisis dinámicos monitorizan elementos tales como la memoria, el comportamiento funcional, los tiempos de respuesta y el rendimiento del sistema. Estos análisis emulan lo que suele hacer el software malicioso y detecta vulnerabilidades.
- Búsquedas de amenazas en dependencias. Los sistemas hacen uso de librerías de terceras partes. Es necesario comprobar que dichas librerías están libres de vulnerabilidades.
- Integridad del código y firma del desarrollador. El repositorio de código es muy vulnerable, los commits deberían estar firmados para asegurarnos que han sido realizados por las personas autorizadas.
  • Asegurar la seguridad de la cadena de suministro de software

Los desarrolladores utilizan múltiples librerías que realizan funcionalidades que necesitan. Realmente en gran parte construyen sus sistemas ensamblando partes de librerías de terceros. La cadena de suministro que proporciona esas librerías también hay que asegurarla. Es necesario controlar que durante el proceso de adquisición de estas librerías no se produzcan brechas en la seguridad.
  • Asegurar la seguridad del entorno

Se trata de establecer controles que monitoricen las instancias de producción, para controlar que todos los entornos están en el estado esperado. Esto se consigue a través de pruebas automatizadas para asegurar que las configuraciones de bases de datos, sistemas operativos, redes, etc. son las adecuadas. Estas pruebas buscan vulnerabilidades.
  • Vigilar la información de seguridad añadida a la telemetría de producción 

A veces los controles de seguridad son inefectivos para detectar brechas en el momento oportuno. Esto puede deberse a que tenemos puntos ciegos en nuestra monitorización o porque nadie examina la telemetría a diario.

Marcus Sachs, uno de los investigadores de brechas en seguridad de Verizon Data dijo en 2010, "Año tras año, en la vasta mayoría de las brechas en la seguridad de los datos, las organizaciones las detectan pasados meses. Y lo que es peor las brechas se detectan desde fuera de la organización porque algún socio o cliente es víctima de alguna transacción fraudulenta. Una de las causas de que esto ocurra es que nadie en la organización revisa regularmente los ficheros de log."
  • Crear telemetria para controlar la seguridad de los entornos

Dentro del programa de mediciones a obtener, es interesante obtener medidas dedicadas a mantener los sistemas seguros. Se pueden medir:
- Cambios a los sistemas operativos
- Cambios a los grupos de seguridad
- Cambios a las configuraciones
- Cambios a la infraestructura de la nube 
- Intentos XSS (“cross-site scripting attacks”)
- Intentos SQLi (“SQL injection attacks”)
- Errores de servidor Web (4XX y 5XX)

Estas medidas pueden servir para detectar vulnerabilidades. Por ejemplo, se han detectado en marzo el doble de cambios en configuraciones que en febrero. Esta información sirve para indicar que hay que vigilar si es correcto que se haya duplicado el número de cambios. 
  • Proteger los entornos y servidores

La infraestructura que soporta la integración continua y el despliegue continuo presenta una nueva área de vulnerabilidad a los ataques. Por ejemplo si alguien compromete algún servidor que aloja las credenciales para el sistema de control de versiones, puede ocurrir que alguien robe el código fuente. Peor todavía si en estos servidores hay permiso de escritura, un atacante podría inyectar cambios maliciosos en el repositorio de código.

Protegiendo el canal de despliegue

  • Integrar seguridad y cumplimiento en el proceso de cambios

En las organizaciones suele existir un proceso para gestionar los cambios y que estos no disturben el normal suceder de los acontecimientos. ITIL clasifica los cambios en tres tipos: estándar, normal o urgente. Cada uno de ellos implica un nivel de riesgo distinto. Los cambios pequeños que implican poco riesgo se deberían automatizar o por lo menos eliminar la necesidad de aprobaciones por parte de comités o personas antes de su implantación, esto se suele convertir en cuellos de botella o traducir en tiempos de entrega largos. Los normales, sí se tratan a través de algún proceso de aprobación y los urgentes tendrán un proceso aparte donde se intente que no conduzcan a resultados no deseados.
  •  Re categorizar la mayoría de los cambios de bajo riesgo como cambios estándar

Teniendo un canal DevOps que funciona adecuadamente, el área de TI tendrá buena reputación. Los despliegues y puestas en marcha se harán de forma rápida y segura. Se dispondrá de una historia de cambios de éxito y en el caso de problemas de un tiempo de resolución pequeño.
Estando en esta situación es bueno intentar que clasifiquemos de estándar todos los cambios que podamos. Esto es porque no hay mucho riesgo de que produzcan problemas y si los producen se resolverán de forma inmediata.
  • Qué hacer cuando los cambios se catalogan como cambios normales

Para aquellos cambios que no se puedan clasificar de estándar se seguirá realizando las aprobaciones que se requieran, pero es importante asegurarse de que se automatiza todo lo que se pueda para que los despliegues sean lo más rápidos posible.
Para su correcta gestión debemos añadir a estos cambios toda la información que sea necesaria para que las aprobaciones sean lo más ágiles posible.
Es importante mantener un historial de cambios ejemplar, de esta forma podemos ganar la confianza de la organización y convertir una gran mayoria de los cambios en cambios estándar.
  • Reducir la división de tareas

La separación de tareas a menudo reduce el feedback que los ingenieros reciben de su trabajo. Esto hace que sea más difícil que se sientan responsables de su trabajo. Se debe intentar donde se pueda evitar la separación de tareas, en su lugar preparar formas de trabajo colaborativas, por ejemplo pair programming, peer review, sesiones de trabajo de ingenieros dev y ops conjuntas.

  • Proveer documentación y pruebas para auditores y agentes responsables de cumplimiento

Bill Shinn es arquitecto de soluciones de seguridad de Amazon Web Services y dice " En DevOps se trata de tender un puente entre Dev y Ops. Además existe el reto todavía más grande de tender el puente entre DevOps y los auditores y agentes de cumplimiento. Por ejemplo, cuantos auditores entienden el código y cuantos desarrolladores se han leído la norma NIST 800-37 del instituto de estándares y tecnología de Estados Unidos.
Hay que buscar los medios para suministrar la información a auditores y agentes dentro del canal DevOps.

Conclusiones


Los principios DevOps se pueden aplicar a la seguridad de la información, haciendo que el trabajo de los ingenieros de seguridad se integre en el de los de desarrollo y operaciones. Esto se consigue haciendo que la seguridad sea parte del trabajo diario de todos. Además se integran los controles de seguridad en los planes de métricas que debe utilizar el canal DevOps para mantener los entornos en un estado protegido y con un bajo perfil de riesgo.

Que con buenas prácticas DevOps la seguridad mejore hace que cuidemos mejor de los datos, también que sea posible recuperarse de los incidentes que producen las brechas de seguridad antes de que sean catastróficos y lo más importante de todo que la seguridad de nuestos datos y sistemas sea mejor que ha sido nunca.

viernes, 13 de septiembre de 2019

Mejorar el canal DevOps: Empleando técnicas de aprendizaje.


Aprender de los éxitos y los fracasos permite mejorar en todos los aspectos de la vida. Emplear esta filosofía es aplicable a los equipos que realizan las actividades en el canal DevOps para conseguir gestionar el ciclo de vida de los sistemas, las aplicaciones, los micro-servicios, las apps, las bases de datos, los sistemas operativos, las redes y demás componentes que conforman la plataforma tecnológica de la información en las organizaciones.

En “The DevOps handbook” de Jez Humble, John Willis, Patrick Debois y Gene Kim publicado por IT Revolution Press en 2016 se proponen tres formas para mejorar el canal DevOps. La primera forma está resumida en el post “Cómo mejorar el canal DevOps. Primeros pasos.” de este blog. La segunda forma está resumida en el post “Mejorar el canal DevOps: Manejando el feedback.” La tercera y última es la que se va a resumir en este post. 

Los autores de este libro recomiendan para mejorar el canal DevOps empleando técnicas de aprendizaje:

  •  Facilitar e inyectar aprendizaje en el día a día 
  •  Inyectar fallos en producción para crear resistencia a fallos
  • Convertir descubrimientos locales en mejoras globales
  •  Reservar tiempo para conseguir aprender y mejorar

Facilitar e inyectar aprendizaje en el día a día

Para que los equipos puedan aprender de sus errores hay que implantar una cultura de “no búsqueda de culpables”.  Es decir, cuando las respuestas a incidentes y accidentes se gestionan principalmente con la búsqueda de culpables se dificulta la realización de las investigaciones adecuadas, promoviendo más bien el miedo que la atención plena de las personas que están realizando trabajos. Teniendo en cuenta que estos trabajos suelen ser críticos para la organización, que los ejecutores de los mismos vivan atemorizados no es útil. Que las personas estén atemorizadas promueve organizaciones más burocráticas que resolutivas, cultivando el secretismo, la evasión de responsabilidades y la auto-protección.”

En lugar de buscar culpables es mejor recopilar información tras los fallos, se pueden realizar reuniones post-morten. Es importante difundir los detalles del  motivo y resolución del fallo para que cualquiera pueda consultarlos.

Se debe fomentar la investigación constante analizando el motivo de cada fallo por pequeño que este sea. A medida que se van solucionando fallos se deben investigar fallos más leves actuando de esta manera de forma sostenida y tratando cada vez fallos de más bajo nivel.

Los fallos son inevitables, cuantos más cambios se hacen más probable es cometer fallos. Esto no quiere decir que no debamos hacer cambios, al revés los cambios son la única forma de mejorar pero pueden tener efectos colaterales. Entonces lo más cabal es promover los cambios y actuar calibrando el riesgo que hay que asumir. Cómo ejemplo:

“Hubo una gran caída en Netflix  que fue causada por un error francamente tonto.  El error fue provocado por un ingeniero que había tirado Netflix 2 veces en los últimos 18 meses. Pero este ingeniero es de mucha utilidad para Netflix. En estos 18 meses había trasformado el estado de las operaciones mejorando la automatización de los procesos. Su trabajo ha permitido hacer despliegues más seguros en producción”. Es decir el ingeniero provocó problemas pero por qué estaba haciendo cambios que finalmente han reportado muchos beneficios.

Inyectar fallos en producción para crear resistencia a fallos

Otras técnicas que pueden ayudar a aprender son: provocar errores a propósito para aprender a resolverlos e institucionalizar días de trabajo dedicados a experimentar fallos. A este propósito existe una herramienta llamada Chaos Monkey construida por Netflix que simula caídas de servidores sobre sistemas en producción. Trabajando en los problemas generados  las instalaciones consiguen sistemas más robustos preparados para cualquier eventualidad.

Convertir descubrimientos locales en mejoras globales

Cuando se descubre algo de interés en un punto de la organización, es importante hacerlo llegar al resto. Para ello es mejor buscar medios que sirvan para que todos los miembros de la organización puedan acceder a la información. Por ejemplo, es mejor utilizar un chat que la utilización del email para compartir información acerca de la resolución de incidentes. De esta forma todo el mundo está al tanto de los incidentes que han acaecido y aprenden de su solución.

Otra forma de difundir conocimiento es automatizar procesos que sean exitosos. De esta manera es más fácil hacerlos llegar a todos los rincones de la organización.

Utilizar un solo repositorio de código para toda la organización consigue que todos los equipos compartan el código. Google lo hace para más 25.000 desarrolladores. Para que el trabajo fluya de forma consistente es necesario establecer políticas de uso del repositorio. Por ejemplo, para el uso de ramas, para la gestión de dependencias, la gestión de versiones, etc.

También las pruebas automatizadas son útiles para transmitir información.  Las pruebas unitarias sirven de documentación del código. Usar TDD entre otras ventajas consigue que las pruebas automatizadas existan y estén al día respecto al código que testean
.
Por otra parte, los desarrolladores deberían acompañar a operaciones en el despliegue y puesta en marcha del software. Cuando los desarrolladores conocen la problemática del área de operaciones, diseñan sistemas que colaboran en su puesta en producción.

Disponer de herramientas de automatización que  por ejemplo extraigan el código del repositorio, lo desplieguen en el entorno de pre-producción, ejecuten las pruebas unitarias, realicen un análisis de código estático y marquen cómo disponible el código para producción o no. Estas herramientas facilitan que los desarrolladores puedan tener un self-service de puesta en producción, es decir ellos mismos liberan software en producción.

También es interesante acordar un marco arquitectónico entre desarrollo y operaciones. Este marco no debe ser rígido, pero si marcar los límites a no traspasar si los desarrolladores quieren acogerse a las áreas soportadas por operaciones. El marco arquitectónico se refiere a las tecnologías empleadas para desarrollar: lenguajes, bases de datos, frameworks, sistemas operativos, etc. Para ilustrar mejor esta idea, considerar que respecto al uso de distintas tecnologías ponemos las que actualmente están soportadas separadas por una boyas de las que no. Los desarrolladores son libres de traspasar la boya, pero ahí tendrán menos soporte que dentro del perímetro señalizado. La metáfora de las boyas sirve para indicar que entre las tecnologías soportadas y las no soportadas no hay un muro in-traspasable.

Reservar tiempo para conseguir aprendizaje y mejorar

Blitz improvement proviene del ciclo Kaizen cuyo objetivo es la mejora. El término Blitz improvement se puede traducir como “bombardeo de mejoras”. Aplicar esta técnica, consiste en reservar un tiempo para que los equipos ideen mejoras para problemas conocidos. Existen muchos casos de éxito asociados al uso de esta técnica.

También se puede dedicar un tiempo periódicamente a eliminar deuda técnica, se trata de dedicar cierto tiempo a trabajar en los sistemas sin ampliar ni modificar funcionalidades. 

Formas de aprender también se pueden conseguir organizando eventos en los que los profesionales enseñen lo que saben a otros y escuchen lo que otros les puedan enseñar. Además es interesante participar en eventos DevOps externos a la organización, compartir experiencias con otros será enriquecedor.

Difundir las buenas prácticas que se hayan puesto en funcionamiento en algún lugar de la organización a todos los demás se puede hacer asignando consultores internos especializados a equipos que necesiten aplicar dichas prácticas. También se pueden contratar consultores externos con especialidad en alguna técnica en la que los equipos no tengan experiencia.

Conclusiones

La tercera forma, explora prácticas que ayudan a crear una cultura de aprendizaje y experimentación. Aprender de los incidentes, crear repositorios únicos para todos los desarrolladores y compartir los descubrimientos entre todos es esencial cuando se trabaja en sistemas complejos, ayudando a caminar hacia una cultura más justa de trabajo en las cuales no se busquen culpables y hacer sistemas más seguros y robustos.



lunes, 9 de septiembre de 2019

Mejorar el canal DevOps: Manejando el feedback.


En el contexto de este post feedback o retroalimentación significa: recoger datos de la ejecución de las tareas realizadas en el área de Tecnologías de la Información, analizar estos datos para obtener conclusiones objetivas sobre el resultado de las actividades y en base a estos datos planificar maneras de mejorar.

Por otro lado el canal DevOps es el conjunto de procesos y actividades que se realizan en una instalación para cubrir el ciclo de vida del software, desde su concepción hasta su puesta en producción

 Sentando las bases

La Real Academia Española de la Lengua define retroalimentación cómo “el retorno de parte de la energía o de la información de salida de un circuito o un sistema a su entrada”.

Wikipedia  define retroalimentación cómo “un mecanismo por el cual una cierta proporción de la salida de un sistema se redirige a la entrada, con señales de controlar su comportamiento”.

Un ejemplo, se dispone de una web de comercio electrónico de venta de libros. Se planifica mejorar la web con el objetivo de vender más libros. Para ello:
  • Se realiza el conteo de libros vendidos cada día.
  • Se calcula el número medio de libros vendidos a la semana.
  • Se aplica algún cambio sobre la web. Por ejemplo se decide incorporar descuentos para los mejores clientes.
  • Se compara el número medio de libros vendidos en las semanas anteriores y posteriores a la mejora.
  • Si se han vendido más libros después del cambio, podemos en general sacar la conclusión de que el cambio ha sido un éxito.
En esencia esta es la base. Viendo la retroalimentación desde este punto de vista, esta puede ayudar a mejorar el canal DevOps o cualquier otro proceso en las organizaciones.

¿Qué sé puede hacer para mejorar el canal DevOps?

En el libro “The DevOps handbook” de Jez Humble, John Willis, Patrick Debois y Gene Kim publicado por IT Revolution Press en 2016 proponen tres formas para mejorar el canal DevOps. La primera forma está comentada en el post “Cómo mejorar el canal DevOps. Primeros pasos.” de este blog. La segunda forma consiste en manejar adecuadamente la retroalimentación. Para realizar una correcta gestión de la retroalimentación, el libro la enfoca desde distintos puntos de vista:

Crear telemetría para permitir encontrar y después resolver problemas

Un hecho real es que pequeños cambios pueden desestabilizar un sistema y conducir a caídas o fallos globales. No es fácil que alguien tenga suficiente conocimiento para tener una visión global de la instalación y poder saber donde se encuentra el problema. Los sistemas suelen ser complejos y el fallo puede estar en muchos sitios distintos. Para resolver el problema es necesario tener información y esta información está disponible si se han previsto los mecanismos necesarios para guardarla.
Uno de los descubrimientos del informe “State of DevOps” de 2015 fue que los equipos de operaciones que consiguen solucionar los problemas más rápidamente, emplean dos técnicas: el uso de un sistema de control de versiones y otra haciendo mediciones y monitorizándolas proactivamente en el entorno de producción.

Analizar la telemetría para anticiparse a los problemas y alcanzar los objetivos

Es deseable prever los problemas para evitar que se produzcan. Esto se puede conseguir monitorizando el funcionamiento de los sistemas y produciendo alertas cuando se detecte alguna anomalía.
Se pueden usar medias, desviaciones estándar, patrones de comportamiento de los datos, cualquier mecanismo puede ser bueno para detectar si se está produciendo una anomalía e intentar resolverla antes de que provoque caídas o efectos no deseados.

Habilitar feedback para que desarrollo y operaciones puedan desplegar código de forma segura

Es necesario que los equipos de desarrollo sepan las consecuencias del despliegue de los cambios que efectúan en el código. Para ello es conveniente que los desarrolladores participen en las tareas de puesta en marcha.
Se pueden establecer medios para que los desarrolladores puedan desplegar en producción los sistemas por sí mismos. La clave está en la automatización, hay muchas herramientas disponibles para poder controlar automáticamente múltiples aspectos del código antes de desplegarlo: pruebas automatizadas a todos los niveles, cobertura de pruebas, análisis estático de código, gestión de dependencias, etc. Todas estas herramientas contribuyen a qué la puesta en producción pueda estar en manos de los equipos de desarrollo.

Integrar desarrollo conducido por la hipótesis y A/B testing en el trabajo diario

Hay funcionalidades que se ponen en producción y nadie las utiliza. Poner en producción código que no es útil a los usuarios es negativo, porque es trabajo inútil y porque además es un código que sin ser útil hay que mantener.  
El A/B testing puede minimizar esta problemática. A/B testing consiste en poner en producción distintas versiones de una misma funcionalidad. Cada versión se le ofrece a un grupo de usuarios diferente. Luego se estudia que versión funcionó mejor y es la que se implanta.
Un paso más allá es el desarrollo conducido por la hipótesis. Este consiste en partiendo de una necesidad detectada por los expertos en negocio, realizar un experimento de implementación (no completa) y ponerlo en producción de forma controlada. Con la aceptación o no aceptación de los usuarios de las funcionalidades suministradas en el experimento, se valida si la hipótesis es una necesidad real o no. En función del resultado del experimento se actúa continuando con la implementación o abandonando la hipótesis.

Crear procesos de coordinación y revisión que incrementen la calidad del trabajo

Con el objetivo de anticiparse a los problemas que un cambio puede producir en producción, es adecuado establecer los medios de revisión y coordinación necesarios.
Ahora bien, no se trata de implantar burocráticos procesos realizados por terceras partes, ajenas al proceso de construcción y despliegue del código. Los procesos realizados por terceros tienden a alargar los procesos de despliegue y a no ser efectivos. Es difícil averiguar los problemas que tiene un código al que se es ajeno.
Es más útil preparar procesos de revisión entre compañeros o iguales (peer review). Un ejemplo de revisión entre compañeros es el pair programming, en esta técnica dos desarrolladores trabajan sobre el mismo código. De esta forma el código cuando está terminado ya está revisado y validado por dos programadores.

Conclusiones

Se ha abordado la retroalimentación desde distintos puntos de vista:
  • Retroalimentación recogiendo datos sobre ejecución de tareas y análisis de los mismos para mejorar
  • Retroalimentación a los equipos de desarrollo no ajenos a las consecuencias de la implantación de su código en producción
  • Retroalimentación sobre los efectos de un cambio con A/B testing
  • Retroalimentación para asegurar hipótesis antes de implementar mediante desarrollo dirigido por la hipótesis
  • Retroalimentación partiendo de revisiones para obtener información de las consecuencias de la implantación
Todas estas formas de aprovechar las ventajas de la retroalimentación pueden ser fuente de inspiración para la mejora del canal DevOps.

miércoles, 28 de agosto de 2019

Kata TDD: Batalla naval sobre los mares de la tierra.


Una Kata en programación es una demostración de una técnica en este caso TDD. Durante la kata el programador experimenta con un ejemplo sencillo si el uso de TDD le resulta práctico y beneficioso para conseguir un código de mejor calidad. La idea de la Kata y el código aquí mostrados están basados en el libro Test-Driven Java Development de Viktor Farcic y Alex García publicado por Packt Publishing en 2015. 

Para comprender este post, es necesario tener conocimientos básicos de: qué es TDD, un lenguaje de programación orientado a objetos (aquí se usa java) y entender que es un marco de pruebas unitarias XUnit (aquí se usa TestNG).

Nuestro trabajo es crear un programa que pueda mover barcos alrededor de los mares de la tierra.
Partimos de clases de soporte que ya están construidas y realizan ciertas funcionalidades útiles para mover los barcos. Para obtener el código del que se parte en esta Kata acceder a https://bitbucket.org/vfarcic/tdd-java-ch04-ship.git en el que se encontrará un proyecto gradle. 

Las clases de soporte son: Direction, Location, Planet y Point. Cada una tiene sus correspondientes clases de prueba cuyo nombre es igual al de la clase que prueba con el sufijo Spec.  El objetivo de las clases Spec es realizar las pruebas unitarias de las clases de soporte suministradas y además servir como especificación o documentación de las mismas, por eso se sufijan con Spec de Specification. Además existe la clase Ship que inicialmente está vacía y es donde se va construir el código con su correspondiente clase de prueba o especificación ShipSpec.

Siguiendo la técnica TDD, se va a proceder a realizar ciclos red-gree-refactor abordando las funcionalidades a implementar en lotes de trabajo muy pequeños.

Lote 1

Se necesita conocer la localización del barco para poder moverlo. Además se necesita saber si el barco está mirando hacía: el norte, el sur, el este o el oeste. Se parte de que un barco tiene un punto inicial (x,y) en el que está situado y una dirección hacia la que mira su proa (N, S, E o W).
Entre las clases de soporte está la clase Point que tiene el constructor.

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

En la clase  Direction existe un método Direction. 

public enum Direction {
    NORTH(0, 'N),
    EAST(1, 'E'),
    SOUTH(2, 'S'),
    WEST(3, 'W'),
    NONE(4, 'X');
}

Además hay una clase Location con el siguiente constructor.

    public Location(Point point, Direction direction) {
        this.point = point;
        this.direction = direction;
    }

Usando la técnica TDD se empieza el primer ciclo red-green-refactor construyendo la  prueba para saber dónde está el barco y hacia donde está mirando. 

@Test
public class ShipSpec {
    public void whenInstantiatedThenLocationIsSet() {
        Location location = 
           new Location(new Point(21,13),Direction.NORTH);      
        Ship ship = new Ship(location);      
        assertEquals(ship.getLocation(), location);   
    }

} 

La prueba verifica que el objeto Location que hemos pasado al constructor del barco ha sido almacenado y se puede acceder a él a través del método getLocation() de Ship.
La implementación consiste en: poner un atributo location a los objetos de tipo Ship, un argumento de tipo Location al constructor de Ship y una función getLocation() que devuelva la location en la que se encuentra el barco.

    private final Location location;
    public Location getLocation() {
        return location;
    }


    public Ship(Location location) {
        this.location = location;
    }


En la parte de refactorización del ciclo decidimos añadir una anotación @BeforeMethod para crear una instancia de un objetos de tipo Ship. Esta instancia la necesitaremos en todos los casos de prueba.

@Test
public class ShipSpec {
 
    private Ship ship;
    private Location location;
 
    @BeforeMethod
        public void beforeTest() {         
         Location location = new Location(
             new Point(21, 13), Direction.NORTH);
        ship = new Ship(location);
    }
 
    public void whenInstantiatedThenLocationIsSet() {
          assertEquals(ship.getLocation(), location);
    }
}

Lote 2

Implementar los comandos que muevan el barco adelante (forward) y atrás (backward). La clase de soporte Location tiene los métodos forward y backward, que implementan esta funcionalidad cambiando la localización del barco según se ordene ir hacia delante (forward) o hacia detrás (backward).

Para la prueba. ¿Qué ocurre cuando, por ejemplo, el barco está mirando hacia el norte y se ordena mover el barco hacia adelante? Su localización sobre la coordenada Y debe incrementarse en una unidad. Otro ejemplo sería cuando el barco está mirando hacia el este, debería incrementarse su localización en X en una unidad.

    public void givenNorthWhenMoveForwardThenYIncreases() {
        ship.moveForward();
        assertEquals(ship.getLocation().getPoint().getY(), 13);
    }
 
    public void givenEastWhenMoveForwardThenXIncreases() {
        ship.getLocation().setDirection(Direction.EAST);
        ship.moveForward();
        assertEquals(ship.getLocation().getPoint().getX(), 22);
    }
 
Esta prueba es correcta. Esto se debe a que la prueba requiere tener conocimiento de qué métodos tiene la clase Location. Es decir getPoint() y getX() son métodos de Location y no se deberían usar en la prueba de Ship. Otro problema que tiene este enfoque es que si Location cambia, hay que buscar todos los lugares en los que se usan sus funciones además de en su propia prueba unitaria.
Aquí lo correcto es asumir que el código al que llamamos tiene ya sus propias pruebas. Es decir, Location ya tiene sus propias pruebas unitarias. Estas pruebas se deben ejecutar cada vez que se incorpora o modifica código al proyecto, asegurando que el nuevo código no rompe lo construido (pruebas de regresión).

Una forma más correcta de escribir esta prueba es.

    public void whenMoveForwardThenForward() {
        Location expected = location.copy();
        expected.forward();
        ship.moveForward();
        assertEquals(ship.getLocation(), expected);
    }
 
Cómo Location ya tiene el método forward, todo lo que necesario es asegurar que se invoca al método de la forma adecuada. Para ello, se crea un objeto de la clase Location llamado expected, se invoca al método forward() y se compara la Location expected con la localización del barco después ordenar que se mueva hacia adelante con moveForward().
Ahora se procede la implementación para llegar a la parte green del ciclo. Consiste aquí en codificar en la clase Ship el método moveForward de la siguiente manera

    public boolean moveForward() {
        return location.forward();
    }
 
Para backward realizamos otro ciclo red-green-refactor similar. La prueba.
     public void whenMoveBackwardThenBackward() {
        Location expected = location.copy();
        expected.backward();
        ship.moveBackward();
        assertEquals(ship.getLocation(), expected);
    }
 
La implementación

    public boolean moveBackward() {
        return location.backward();
    }

Lote 3

Implementar los comandos que mueven el barco a derecha e izquierda. Siguiendo el diseño de mover adelante y atrás, realizamos dos ciclos red-green-refactor uno para la izquierda y otro para la derecha. La  prueba para el movimiento hacia la izquierda.

    public void whenTurnLeftThenLeft() {
        Location expected = location.copy();
        expected.turnLeft();
        ship.turnLeft();
        assertEquals(ship.getLocation(), expected);
    }
 
Y la implementación
    public void turnLeft() {
        location.turnLeft();
    }
 
Y para la derecha otro ciclo con la prueba

    public void whenTurnRightThenRight() {
        Location expected = location.copy();
        expected.turnRight();
        ship.turnRight();
        assertEquals(ship.getLocation(), expected);
    }
 
Y la implementación

    public void turnRight() {
        location.turnRight();
    }

Lote 4

El barco puede recibir una cadena de caracteres con comandos. Esto es ‘lrfb’ es equivalente a: left (izquierda), right (derecha), forward (adelante) y backward (atrás). Se empieza por la prueba para el comando f.

    public void whenReceiveCommandsFThenForward() {
        Location expected = location.copy();
        expected.forward();
        ship.receiveCommands("f");
        assertEquals(ship.getLocation(), expected);
    }
 
La implementación

    public void receiveCommands(String commands) {
        if (commands.charAt(0) == 'f') {
            moveForward();
        }
    }
 
La prueba y la implementación es similar para los comandos r, l y b. Teniendo implementado cada comando por separado, ahora vamos a implementar una prueba para probar un comando compuesto ‘rflb’.

    public void whenReceiveCommandsThenAllAreExecuted() {
        Location expected = location.copy();
        expected.turnRight();
        expected.forward();
        expected.turnLeft();
        expected.backward();
        ship.receiveCommands("rflb");
        assertEquals(ship.getLocation(), expected);
    }
 
La implementación la hacemos modificando el método receiveCommands()
    public void receiveCommands(String commands) {
        for (char command : commands.toCharArray()) {
            switch(command) {
                case 'f':
                    moveForward();
                    break;
                case 'b':
                    moveBackward();
                    break;
                case 'l':
                    turnLeft();
                    break;
                case 'r':
                    turnRight();
                    break;
            }
        }
    }

Lote 5

La tierra es una esfera. Cuando nos movemos en un mapa plano llega un momento que no podemos avanzar más pero en una esfera eso no es así, siempre se puede avanzar para cualquier dirección. Lo implementado hasta ahora no tiene esto en cuenta, para tenerlo en cuenta se considera el mapa una plantilla con cuadrícula. De esta forma, cuando se llega a un borde, se pasa al borde contrario para simular el movimiento en la esfera.

La cuadrícula tiene una longitud y anchura máximas. Entonces se va a implementar como llegando al borde se pasa al borde contrario. Se usa la clase de soporte Planet.  Se prepara una prueba en la que al constructor del barco además de enviarle la localización le enviamos planet. El objetivo a la larga es conocer los límites de la cuadrícula del mapa para un determinado planeta. Ahora solo se comprueba que el barco sepa el planeta en el que se encuentra.

    public void whenInstantiatedThenPlanetIsStored() {
        Point max = new Point(50, 50);
        Planet planet = new Planet(max);
        ship = new Ship(location, planet);
        assertEquals(ship.getPlanet(), planet);
    }
 
Para que esto funcione correctamente hay que modificar el constructor de Ship, pero esto puede romper pruebas que ya están usando el constructor solo con el argumento location. Para mantener en funcionamiento lo anterior se crea un nuevo constructor que sobrecarga el anterior.

    public Ship(Location location) {
        this.location = location;
    }
    public Ship(Location location, Planet planet) {
        this.location = location;
        this.planet = planet;
    }
 
Después de comprobar que todas las pruebas siguen funcionando, en la fase de refactoring se valora si dejar un único constructor. Para dejar con un único constructor cambiamos la prueba

public class ShipSpec {
    private Planet planet;
 
    @BeforeMethod
    public void beforeTest() {
        Point max = new Point(50, 50);
        location = new Location(new Point(21, 13), Direction.NORTH);
        planet = new Planet(max);
//        ship = new Ship(location);
        ship = new Ship(location, planet);
    }
    public void whenInstantiatedThenPlanetIsStored() {
//        Point max = new Point(50, 50);
//        Planet planet = new Planet(max);
//        ship = new Ship(location, planet);
        assertEquals(ship.getPlanet(), planet);
    }
}
 
Con esto el constructor de un solo argumento puede ser borrado. Usando esta secuencia de pasos las pruebas permanecen siempre en verde.

Ahora se procede a la implementación del movimiento de una parte de la cuadrícula a otra, es decir el salto al otro lado cuando se llega al límite. Para ello se usan las clases de soporte. El método forward de Location está sobrecargado con el método forward(Point max). Esté método se encarga de llegar a la otra parte de la cuadrícula cuando alcanzamos el límite máximo de cualquier extremo: norte, sur, este y oeste. Se prepara una prueba para comprobar que cuando el barco está mirando hacia el este y está en el límite máximo de la abscisa X si se ordena que avance, vuelva al valor 1 de X, esto simula que da la vuelta en la esfera terrestre.

/* The name of this method has been shortened due to line's length restrictions. The aim of this test is to check the behavior of ship when it is told to overpass the right boundary.
*/
    public void overpassEastBoundary() {
        location.setDirection(Direction.EAST);
        location.getPoint().setX(planet.getMax().getX());
        ship.receiveCommands("f");
        assertEquals(location.getX(), 1);
    }
 
La implementación que corresponde a esto es

    public boolean moveForward() {
//        return location.forward();
        return location.forward(planet.getMax());
    }
 
Ahora se realiza otro ciclo red-green-refactor con el método backward.

Lote 6

En los planetas no todo es agua y por ello es necesario implementar detección de tipo de superficie antes de hacer el movimiento. Si el comando encuentra una superficie no acuática, el barco aborta el movimiento y reporta el obstáculo.

En las clases de apoyo contamos con:
  • Qué el objeto Planet tiene un constructor que acepta una lista de obstáculos. Cada obstáculo es una instancia de la clase Point.
  • Los métodos Location.forward y Location.backward tiene versiones sobrecargadas que aceptan una lista de obstáculos. Estos métodos devuelven true si el movimiento se hizo y falso si falló.
  • Para construir el informe de estado se usa el método Ship.receiveCommands. Este método  debería devolver una cadena de caracteres del estado resultante de la ejecución de cada movimiento. O representa que el movimiento se hiza y X que no se hizo. Ejemplo: OOXO = OK, OK, Failure y OK.
Preparamos la prueba

public void whenReceiveCommandsThenStopOnObstacle() {
        List obstacles = new ArrayList<>();
        obstacles.add(new Point(location.getX() + 1, location.getY()));
        ship.getPlanet().setObstacles(obstacles);
        Location expected = location.copy();
        expected.turnRight();
        // Moving forward would encounter an obstacle
        // expected.forward(new Point(0, 0), new ArrayList());
        expected.turnLeft();
        expected.backward(new Point(0, 0), new ArrayList<>());
        ship.receiveCommands("rflb");
        assertEquals(ship.getLocation(), expected);
    }
    public void whenReceiveCommandsThenOForOkAndXForObstacle() {
        List obstacles = new ArrayList<>();
        obstacles.add(new Point(location.getX() + 1, location.getY()));
        ship.getPlanet().setObstacles(obstacles);
        String status = ship.receiveCommands("rflb");
        assertEquals(status, "OXOO");
    }
 
Y la implementación 

public boolean moveForward() {
//        return location.forward();
        return location.forward(planet.getMax(), planet.getObstacles());
    }
 
    public boolean moveBackward() {
//        return location.backward();
        return location.backward(planet.getMax(), planet.getObstacles());
    }
 
El código sobre el cual se ha realizado la explicación, está en https://bitbucket.org/vfarcic/tdd-java-ch04-ship/branch/req06-obstacles tal y como lo suministran los autores del libro mencionado al comienzo de este post.

Conclusión

Mediante esta Kata se muestra como el código de una aplicación va tomando forma mediante la técnica TDD, en definitiva inicialmente el código no es nada y toma forma dependiendo exclusivamente de las decisiones del programador. Quizás este post le sirva al programador para reflexionar sobre las ventajas que obtendría si usa esta técnica de construcción de código.
En la opinión la autora de este post, se obtienen múltiples ventajas:
  • Pruebas unitarias automatizadas para todo el código
  • Pruebas unitarias bien construidas (principio FIRST)
  • Especificaciones actualizadas
  • Código construido para ser probado
  • Facilidades para el posterior mantenimiento del sistema
  • No hay código superfluo. Solo se codifica lo imprescindible para superar la prueba
Soto del Real. Madrid a 28 de agosto de 2019