• Saltar a la navegación principal
  • Saltar al contenido principal
  • Saltar al pie de página
Bluetab

Bluetab

an IBM Company

  • Soluciones
    • DATA STRATEGY
    • DATA READINESS
    • DATA PRODUCTS AI
  • Assets
    • TRUEDAT
    • FASTCAPTURE
    • Spark Tune
  • Conócenos
  • Oficinas
    • España
    • Mexico
    • Perú
    • Colombia
  • talento
    • España
    • TALENT HUB BARCELONA
    • TALENT HUB BIZKAIA
    • TALENT HUB ALICANTE
    • TALENT HUB MÁLAGA
  • Blog
  • ES
    • EN

Practices

Databricks sobre Azure – Una perspectiva de Arquitectura (parte 1)

febrero 15, 2022 by Bluetab

Databricks sobre Azure - Una perspectiva de arquitectura (parte 1)

Francisco Linaje

AWS Solutions Architect

Gabriel Gallardo Ruiz

Senior Data Architect

Databricks tiene como objetivo dotar de un entorno intuitivo al usuario no especializado para que pueda desarrollar las diferentes funciones en materia de ingeniería y ciencia de datos, dotando además de una capa de gobierno y gestión del dato.

Nuestro objetivo con este artículo, no es tanto describir y analizar cómo emplear estas herramientas, sino ver como son integradas desde un punto de vista de arquitectura dentro del proveedor de Azure.

Databricks como solución Lakehouse

La plataforma Databricks sigue el paradigma de Lakehouse, en el que se combinan las bondades del Data Warehouse con las del Data Lake, permitiendo tener un buen rendimiento tanto en sus consultas analiticas gracias a la indexacion, como transaccionalidad a través de Delta Lake,  sin perder la flexibilidad de una arquitectura de datos abierta y escalable, junto a una mejor gobernanza del dato y del acceso a los recursos y servicios del lago, permitiendo de una forma general disponer de una arquitectura menos compleja y más integrada.

Este artículo se dividirá en dos entregas.

  • La primera, tendrá como objetivo explicar cómo Databricks organiza y despliega su producto en Azure, así como las diferentes configuraciones en materia de comunicación/seguridad entre Databricks y otros servicios de Azure.
  • La segunda, estará centrada en la capa de seguridad del dato y escalabilidad de la infraestructura así como monitorización, despliegue y recuperación ante errores.


Primera entrega:

  • Arquitectura alto nivel
  • Planes y tipos de carga de trabajo
  • Networking
  • Identidad y Gestión de accesos

Segunda entrega (próximamente):

  • DR
  • Cifrado
  • Escalabilidad
  • Logging y monitorización
  • Despliegue

Glosario

  • Azure Data Lake: Permite almacenar múltiples formatos de datos en un mismo lugar para su explotación y análisis, actualmente Azure dispone la versión Gen2 .
  • All Purpose Compute: Diseñado para entornos colaborativos en los que se recurra de forma simultánea al clúster por parte de Data Engineers y Data Scientist.
  • Azure Key Vault: Servicio administrado de Azure que permite el almacenamiento seguro de secretos.
  • Azure Virtual Network (VNET): Red virtual aislada lógicamente en Azure.
  • Continuous integration and continuous delivery CI/CD: Conjunto de herramientas y pautas automatizadas que permiten aplicar una integración y puesta en producción continua
  • Data Lake: Paradigma de almacenamiento distribuido de datos provenientes de multitud de fuentes y formatos, estructurados, semi estructurados y sin estructurar.
  • Identity Provider (IdP): Entidad que mantiene la información de identidad de los individuos dentro de una organización.
  • Jobs Compute: Enfocado a procesos orquestados mediante pipelines gestionados por data engineers que puedan conllevar autoescalado en ciertas tareas
  • Jobs Light Compute: Diseñado para procesos cuya consecución no sea crítica y no conlleve una carga computacional muy elevada.
  • Network Security Group o NSG: Especifican las reglas que regulan el tráfico de entrada y salida de la red y los clusters en Azure.
  • Notebook: Interfaz web para ejecutar código en un cluster, abstrayendo del acceso al mismo.
  • PrivateLink: Permite el acceso privado (IP privada) a Azure PaaS a través de la VNET del usuario, de la misma forma que los service endpoints el tráfico se enruta a través del backbone de Azure.
  • Azure role-based access control (RBAC): Sistema de autorización integrado en Azure Resource Manager que permite asignar permisos granulares sobre recursos a usuarios de Azure.
  • Security Assertion Markup Language (SAML): Estándar abierto utilizado para la autenticación. Basado en XML, las aplicaciones web utilizan SAML para transferir datos de autenticación entre dos entidades, el Identity Provider y el servicio en cuestión.
  • Secure Cluster Connectivity (SCC):  Comunicación a través de túnel inverso SSH entre Control Plane y cluster. Permite no tener puertos abiertos ni IPs públicas en las instancias.
  • Service Endpoints: Componente de red que permite conectar una VNET con los diferentes servicios dentro de Azure a través de la propia red de Azure.
  • Service Principal: Entidad creada para que la administración y gestión de tareas que no queden asociadas a un miembro de la organización en particular si no a un servicio
  • Secret Scope: Colección de secretos identificados por un nombre.
  • Single Sign On (SSO): Permite a los usuarios la autenticación a través de un Identity Provider (IdP) proporcionado por la organización, requiriendo de compatibilidad con SAML 2.0.
  • Workspace: Entorno compartido para acceder a todos los activos de Databricks. En este se organizan los diferentes objetos (notebooks, librerias, etc…) en carpetas y se administran los accesos a recursos computacionales como clusters y jobs. 

Arquitectura

Databricks como producto

Databricks permanece integrado dentro de Azure como servicio propio a diferencia de otros proveedores, permitiendo el despliegue de una forma más directa y sencilla ya sea desde la propia consola o mediante templates.

Dentro de los servicios ofrecidos por Databricks destacan: 

  • Databricks SQL: ofrece una plataforma para realizar consultas SQL ad-hoc contra el Data Lake, así como múltiples visualizaciones de los datos con dashboards.
  • Databricks Data Science & Engineering: proporciona un workspace que permite la colaboración entre diferentes roles (ingenieros de datos, científicos de datos, etc) para el desarrollo de diferentes pipelines para la ingesta y explotación del Data Lake.
  • Databricks Machine Learning: ofrece un entorno para el desarrollo y explotación de modelos de machine learning end-to-end.

Además Databricks ofrece Spark como framework de programación distribuida, así como integración con Delta Lake y soporte a transacciones ACID para datos estructurados y no estructurados, unificación de fuentes batch y streaming.

Databricks ofrece también una solución en material de orquestación y despliegue de jobs de una forma productiva, permitiendo además paralelismo entre ellos, hasta 1000 de forma concurrente. Pudiendo emplearse solo dentro del workspace de Data Science & Engineering

Diagrama de procesos orquestados (fuente: Databricks)

Dentro de las ventajas añadidas ofrecidas por Databricks se encuentra el empleo de Databricks File System (DBFS), se trata de un sistema de ficheros distribuido de acceso para los clusters.

  • Permite montar puntos de almacenamiento para acceder a los objetos sin necesidad de credenciales específicos para su uso.
  • Evita la necesidad de emplear urls para acceder a los objetos, facilitando el acceso vía directorios y semántica.
  • Otorga una capa de persistencia al almacenar en el file system los datos, evitando que estos se pierdan cuando el cluster es finalizado.

Databricks Repos: ofrece integración y sincronización con repositorios GIT, incluyendo una API para el empleo de pipelines de CI/CD. Los proveedores Git actuales incluidos son:

  • GitHub
  • Bitbucket
  • GitLab
  • Azure DevOps

 

Overview de la arquitectura

En esta sección analizaremos cómo se realiza el despliegue de Databricks dentro de la cuenta del cliente en su proveedor cloud, este caso Azure.

Databricks se compone principalmente de dos capas; una de control (interna) y otra de datos (externa/cliente).

Diagrama a alto nivel de la arquitectura (fuente: Databricks)

En la imagen anterior podemos ver como la capa de control permanece en la suscripción de databricks, bajo su control, diseño y administración interna siendo compartida esta por todos los usuarios.

 Los principales servicios contenidos, son:

  • Notebooks: Todos los notebook, resultados y configuraciones permanecen encriptados
  • Job Scheduler
  • Rest API
  • Metastore: Hive metastore gestionado por databricks
  • Cluster manager: Solicita máquinas virtuales para los clúster que se  lanzarán en el plano de datos

La capa de datos se encuentra dentro de la suscripción del cliente y por lo tanto será administrada por él. En esta capa encontramos los jobs y clusters empleados para la ejecución de las ETLs, así como los datos empleados en ellas.

Es importante señalar que Databricks disponibiliza dos interfaces de red en cada nodo desplegado, en una de ellas se direccionara el tráfico hacia el plano de control y en la otra el tráfico interno entre nodos (driver – ejecutores)

Databricks ofrece dos métodos principales para realizar el despliegue del plano de datos y que posteriormente comentaremos en profundidad:

  • Por un lado tenemos Databricks managed VNET, siendo este el despliegue dado por defecto donde Databricks se encarga de desplegar los recursos necesarios dentro de la cuenta del cliente. 
  • Por otro lado tenemos un segundo tipo de despliegue Databricks VNET injection donde es el cliente el que disponibiliza los recursos mínimos necesarios para el correcto funcionamiento y comunicación contra el control-plane.

En ambos casos, la topología de red en el Data Plane se compondrá de dos subredes.

  1. Container subnet o “private” subnet
  2. Host subnet or “public” subnet
Arquitectura de Databricks en Azure (fuente: Databricks)

Secure Cluster Connectivity [2]

En contextos de seguridad más restrictivos, será posible asignar como puerta de enlace un NAT  gateway u otro dispositivo egress traffic como un balanceador de carga, firewall, etc, para eliminar la necesidad de asignar direcciones IP públicas a los hosts.

Conexión del workspace con SCC (fuente: Databricks)

Planes y tipos de carga de trabajo

Además del coste de la infraestructura empleada para el procesamiento y el almacenamiento en Azure, Databricks añade un sobrecargo extra expresado en DBU (unidades de procesamiento) en función del tipo de instancia levantada y su tamaño, así como el tipo de workload empleado. 

Se distinguen 2 tipos principales: 

  • Jobs Cluster: Ejecución de pipelines programadas no iterativas, distinguiéndose según el tamaño del cluster aprovisionado en ligero o normal. Los jobs suelen emplearse creando clusters efímeros y siendo eliminados después de la ejecución de los mismos.
  • All purpose: Clusters empleados para trabajar de forma iterativa (MANDATORY) permitiendo ejecutar  y desarrollar diferentes notebooks concurrentemente.

Además, según el tipo de cuenta contratada Standard o Premium se realizarán cargos adicionales sobre el coste de la DBU.

AZURE PLAN
Standard
Premium
One platform for your data analytics and ML workloads
Data analytics and ML at scale across your business
Job Light Compute
$0,07/DBU
$0,22/DBU
Job Compute
$0,15/DBU
$0,30/DBU
SQL Compute
N/A
$0,22/DBU
All-Purpose Compute
$0,40/DBU
$0,55/DBU

Coste imputado por DBU respecto a los factores computacionales y arquitectónicos

WORKLOAD TYPE (STANDARD TIER)
FEATURE
Jobs Light Comput
Jobs compute
All-purpose compute
Managed Apache Spark
Job scheduling with libraries
Job scheduling with notebooks
Autopilot clusters
Databricks Runtime for ML
Managed MLflow
Delta Lake with Delta Engine
Interactive clusters
Notebooks and collaboration
Ecosystem integrations

Características por tipo de carga de trabajo plan Standard

WORKLOAD TYPE (STANDARD TIER)
FEATURE
Jobs Light Comput
Jobs compute
All-purpose compute
Role Based Access Control for clusters, jobs,
notebooks and tables
JDBC/ODBC Endpoints Authentication
Audit Logs
All Standard Plan Features
Azure AD credential passthrough
Conditional Authentication
Cluster Policies
IP Access List
Token Management API

Características por tipo de carga de trabajo plan Premium

Es importante señalar que además podrán obtenerse descuentos de hasta el 37% en los precios por DBU, realizando compras aprovisionadas de estas (DBCU o Databricks Commit Units) para 1 o 3 años.

Networking

En este apartado explicaremos los dos diferentes tipos de despliegue ya comentados anteriormente y sus peculiaridades en materia de conexión y acceso al plano de control, así como al control del tráfico entrante/saliente.

 

Red administrada por Databricks

En esta alternativa, Azure permite a Databricks desplegar el plano de datos sobre nuestra suscripción, disponibilizando los recursos que permitirán la conexión contra el plano de control y el despliegue de jobs, clusters y otros recursos.

  • La comunicación entre el plano de datos y plano de control independientemente de tener activado Secure Cluster Connectivity (SCC) se realizará por el backbone interno de Azure, sin enrutar tráfico sobre la red pública.
  • Secure Cluster Connectivity (SCC) podrá ser activado para  poder trabajar sin IPs públicas.
  • El tráfico inbound/outbound de los clusters estará controlado mediante diferentes reglas por el network security group NSG que no podrán ser modificables por el usuario.


Red administrada por el cliente (VNET injection) [1]

Databricks ofrece la posibilidad de poder desplegar el plano de datos sobre una VNET propia administrada por el cliente. Esta solución ofrece mayor versatilidad y control sobre los diferentes componentes de nuestra arquitectura.

  • La comunicación entre el Data Plane y Control Plane se realizará sobre el backbone interno de Azure de la misma forma que en la red administrada por Databricks vista anteriormente, además de la misma forma podremos activar SCC.
  • En este caso al ser nuestra propia VNET tendremos control sobre las reglas definidas en nuestros NSG.
NSG provisionado por Databricks por delegación en la VNET del cliente (fuente: Databricks).
  • Se deberá ser propietario de la VNET para poder permitir que se delegue a Databricks su configuración o el despliegue de recursos [3].
  • Podremos habilitar cualquier componente de arquitectura que consideremos dentro de nuestra VNET ya que estará administrada por nosotros:
    • Conectar Azure Databricks a otros servicios de Azure de una forma más segura empleando service endpoints o private endpoints.
    • Conectarse a tus recursos on-premise empleando user-defined routes.
    • Permite desplegar un network virtual appliance para inspeccionar el tráfico
    • Custom DNS
    • Custom egress NSG rules
    • Aumentar el rango CIDR de la máscara de red para la VNET entre /16 – /24 y /26 para las subredes.
Diagrama de conexión mediante PrivateLink con servicios nativos de Azure (fuente: Databricks)

Dentro de las peculiaridades de ambos despliegues, es importante señalar:

  • No es posible reemplazar una VNet existente en un workspace por otra, si fuera necesario se deberá crear un nuevo workspace con una nueva VNET.
  • Tampoco es posible añadir SCC a un workspace una vez ya ha sido creado, si fuera necesario también deberá volver a crear el workspace.

Conexiones contra el plano de control

Conexión plano de control y de datos en Databricks (fuente: Databricks)

Tal y como ya hemos comentado anteriormente, toda comunicación con el plano de control se realiza por dentro del backbone de Azure por defecto [2]. También se debe destacar:

  • A nivel de red, toda conexión que se realiza contra el plano de control al crear un cluster en el plano de datos se realiza vía HTTPS (443) y sobre una dirección IP diferente a la empleada para otros servicios de aplicación Web o APIs.
  • Cuando el plano de control  lanza nuevos jobs o realiza otras tareas de administración del clúster, estas solicitudes se envían al clúster a través de este túnel inverso.
  • Para realizar las conexiones entre el plano de control y de datos, se habilitará una dirección IP pública sobre la subred pública aunque el tráfico posteriormente sea enrutado dentro del backbone, además no se dejarán puertos abiertos o se asignarán direcciones IP públicas sobre los clusters.
  • Si en nuestro caso de uso se deben de emplear unas condiciones más restrictivas de seguridad, Databricks ofrece la posibilidad de activar la opción de secure cluster connectivity permitiendo eliminar toda dirección IP pública para realizar la conexión entre el plano de control y de datos,  para ello se servirá:
    • Por defecto en la red administrada por Databricks (managed VNET)  se habilita un NAT para poder realizar esta comunicación.
    • Si el cliente despliega la infraestructura sobre su propia red (VNET Injection deployment) este deberá aportar un dispositivo de red para el tráfico saliente, que podra ser un NAT Gateway, Load Balancer, Azure Firewall o un dispositivo de terceros.

Identidad y Gestión de accesos

Databricks ofrece diferentes herramientas para administrar el acceso a nuestros recursos y servicios de Azure de una forma sencilla e integrada en la propia plataforma.

Podremos encontrar herramientas como filtrados de IP, SSO, permisos de uso sobre los servicios de Databricks, acceso a secretos, etc.

IP access lists

Databricks permite a los administradores definir listas de acceso IP para restringir el acceso a la interfaz de usuario y la API a un determinado conjunto de direcciones IP y subredes, permitiendo el acceso solo desde las redes de la organización. Los administradores sólo podrán realizar la gestión de IP access list con el API REST.

Single sign on (SSO) 

Mediante Azure Active Directory podremos configurar SSO para todos nuestros usuarios de Databricks evitando la duplicación en la gestión de identidades.

System for Cross-domain Identity Management (SCIM)

Permite a través de un IdP (actualmente Azure Active Directory) crear usuarios en Azure Databricks y otorgarles un nivel de permisos y permanecer sincronizados, se debe disponer de un plan PREMIUM. Si los permisos son revocados los recursos ligados a este usuario no son eliminados.

Acceso a recursos

El acceso principal a los diferentes servicios de Databricks vendrá dado por los entitlements donde se indicará si el grupo/usuario tendrá acceso a cada uno de ellos (cluster creation, Databricks SQL, Workspaces)

Por otro lado, dentro de Databricks se pueden emplear ACLs para configurar el acceso a los diferentes recursos como clusters,tablas, pools, jobs y objetos del workspace (notebooks, directorios, modelos, etc). Otorgar esta granularidad sobre el acceso a los recursos solo está disponible mediante el plan PREMIUM, por defecto todos los usuarios tendrán acceso a los recursos.

Estos permisos se encuentran gestionados desde el usuario administrador u otros usuarios con permisos delegados.

Existen 5 niveles de permisos con sus múltiples implicaciones dependiendo del recurso al que se apliquen; No permissions, can read, can run, can edit, can manage.

A continuación, se indican los permisos asociados al recurso que se desea emplear. En caso de que dos políticas puedan solaparse, la opción más restrictiva primará sobre la otra.

Permisos según el nivel asociado al usuario sobre los directorios del workspace (Fuente: Databricks)
Permisos según el nivel asociado al usuario sobre el notebook (Fuente: Databricks)
Permisos según el nivel asociado al usuario sobre el repositorio (Fuente: Databricks)

Azure Datalake Storage ADLS

A través de Azure Active Directory (Azure AD) puedes realizar la autenticación directamente desde Databricks con Azure Datalake Storage Gen1 y 2, permitiendo al cluster de Databricks acceder a estos recursos directamente sin necesidad de tener un service principal. Requiere del plan  PREMIUM y activar credentials passthrough en opciones avanzadas en el momento de creación del cluster en Databricks. Disponible en clusters estándar y de alta simultaneidad.

Activación de credentials passthrough en la opciones de configuración del cluster (Fuente: Databricks)

Credential passthrought es un método de autenticación que emplea la identidad (Azure AD) utilizada para la autenticación en Databricks para conectarte al Datalake. El acceso a los datos será controlado a través de los roles de RBAC (permisos a nivel de usuario) y ACLs (permisos a nivel de directorio y archivo) configuradas.

Las listas de control de acceso (ACL) controlan el acceso al recurso comprobando si la entidad que desea acceder tiene los permisos adecuados.


Secretos
[5]

Accesos

Por defecto, todos los usuarios sin importar el plan contratado pueden crear secretos y acceder a ellos (MANAGE permission). Solo a través del plan PREMIUM es posible configurar permisos granulares para controlar el acceso. La gestión de estos se podrá realizar a través de Secrets API 2.0 o Databricks CLI (0.7.1 en adelante).

La gestión de los secretos se realiza a nivel de scope (colección de secretos identificados por un nombre), en concreto una ACL controla la relación entre el principal (usuario o grupo), el scope y el nivel de permiso. Por ejemplo: cuando un usuario accede al secreto desde un notebook vía Secrets utility el nivel de permiso se aplica en base a quien ejecuta el comando.

Por defecto, cuando un scope es creado se le aplica un nivel de permiso MANAGE, sin embargo el usuario que crea el scope podrá añadir permisos granulares.

Distinguimos 3 niveles de permiso en Databricks-backed scopes:

  • Manage: Puede modificar las ACLs y además tiene permisos de lectura y escritura sobre el scope.
  • WRITE: tiene permisos de lectura y escritura sobre el scope.
  • READ: solo tiene permisos de lectura sobre el scope y los secretos a los que tiene acceso.

Los usuarios administradores de un workspace tienen acceso a todos los secretos de todos los scopes

Almacenamiento

Los secretos podrán ser referenciados desde los scopes que a su vez harán referencia a sus respectivos vaults donde los secretos son almacenados.
Existen dos tipos de soporte de almacenamiento para los secretos:

  • Databricks-backed
  • Azure Key Vault

Podremos emplear Databricks-backed como soporte de almacenamiento de los secretos sin la necesidad de un plan PREMIUM, sin embargo ya sea tanto para emplear Azure Key Vault o por otro lado el empleo de permisos granulares en ambos soportes, sí será necesario contratar el plan PREMIUM.

Es importante destacar que si el Key Vault existe en un tenant diferente al que alberga el workspace de Databricks, el usuario que crea el scope debe de tener permisos para crear service principals sobre el key vault del tenant, de lo contrario se arrojará el siguiente error.

Unable to grant read/list permission to Databricks service principal to KeyVault 

Debido a que Azure Key Vault es ajeno a Databricks solo será posible realizar operaciones de lectura por defecto y no pueden ser administradas desde Secrets API 2.0, deberá emplearse por contra Azure SetSecrets REST API o desde el portal de Azure UI.

Es importante señalar, que todos los usuarios tendrán acceso a los secretos de un mismo Key Vault aunque se encuentren en diferentes scopes. Se considera buena práctica replicar los secretos en diferentes Key Vaults según subgrupos se tengan aunque puedan ser redundantes.

Ahora mediante RBAC [4] (role-based access control) ya es posible mediante diferentes roles controlar el acceso a los secretos del Vault desde Azure.

Por último, simplemente indicar que los scopes podrán consumirse desde la librería dbutils, si el valor es cargado correctamente aparece referenciado como REDACTED.

dbutils.secrets.get(scope = "nombre_scope_databricks", key = "nombre_secreto") 

Conexiones on-premise

Por último, indicar que es posible establecer una conexión on-premise para nuestro plano de datos en Azure, para ello es indispensable que este se encuentre alojado en nuestra propia red mediante VNET injection.

Arquitectura de Databricks en Azure (fuente: Databricks)

Azure define como principal método el uso de Transit Virtual Network para establecer esta conexión on-premise, siguiendo los siguientes pasos:

  1. Crear un Network Gateway (VPN o ExpressRoute) entre la red de tránsito y on-premise, para ello deberemos crear tanto el Customer Gateway en el lado on-premise como el Virtual Gateway en el lado de Azure.
  2. Deberemos establecer el peering entre el plano de datos y la red de tránsito. Una vez establecido, Azure Transit configura todas las rutas, sin embargo no se incluyen las de retorno hasta el plano de control para los clusters de Databricks, para ello se deberán configurar las user-defined routes y asociarlas a las subredes del plano de datos.

Otras soluciones alternativas también podrían emplearse mediante el uso de Custom DNS o el empleo de un virtual appliance o firewalls.

Referencias

[1] Customer Managed VNET Databricks Guide. [link] (Enero 26, 2022)

[2] Secure Cluster Connectivity. [link]  (Enero 26, 2022)

[3] Subnet Delegation. [link] (Enero 3, 2022)

[4] Role-based access control [link] (Octubre 27, 2021)

[5] Databricks secrets scopes [link] (Enero 26, 2022)

Navegación

Glosario

Arquitectura

Planes y tipos de carga de trabajo

Networking

Identidad y Gestión de accesos

Referencias

Autores

¿Quieres saber más de lo que ofrecemos y ver otros casos de éxito?
DESCUBRE BLUETAB

Francisco Linaje

AWS Solutions Architect

Gabriel Gallardo Ruiz

Senior Data Architect

SOLUCIONES, SOMOS EXPERTOS

DATA STRATEGY
DATA FABRIC
AUGMENTED ANALYTICS

Te puede interesar

Espiando a tu kubernetes con kubewatch

septiembre 14, 2020
LEER MÁS

Análisis de vulnerabilidades en contenedores con trivy

marzo 22, 2024
LEER MÁS

Serverless Microservices

octubre 14, 2021
LEER MÁS

Mi experiencia en el mundo de Big Data – Parte II

febrero 4, 2022
LEER MÁS

LakeHouse Streaming en AWS con Apache Flink y Hudi (Parte 2)

octubre 4, 2023
LEER MÁS

Starburst: Construyendo un futuro basado en datos.

mayo 25, 2023
LEER MÁS

Publicado en: Blog, Blog, Destacado, Practices, Tech

Cómo preparar la certificación AWS Data Analytics – Specialty

noviembre 17, 2021 by Bluetab

Cómo preparar la certificación AWS Data Analytics - Specialty

Sergi Lehkyi

Data Engineer

Sobre la certificación

El examen de especialidad AWS Data Analytics se centra principalmente en los servicios de AWS relacionados con datos, cubriendo los dominios:

  • Recolección de datos.
  • Gestión de datos y almacenamiento.
  • Procesamiento.
  • Análisis y visualización.
  • Seguridad.

El examen trata en profundidad los siguientes servicios de AWS:

  • Amazon S3
  • Amazon Redshift
  • Amazon Kinesis
  • Amazon EMR
  • Amazon ElasticSearch
  • Amazon Athena
  • AWS Glue
  • Amazon QuickSight
  • AWS Lake Formation
  • Amazon Managed Streaming for Apache Kafka (Amazon MSK)

*Todos los servicios de datos que puedan interactuar con los anteriores (SageMaker, Backup, Glacier, GuardDuty etc.)

Se puede encontrar más en la página web de [AWS Datalakes and Analytics], también aquí está la [guía oficial del examen] y [preguntas de muestra].

Todo el contenido de este artículo es válido para el examen de 2021. Recomendamos que revises si existe alguna actualización del examen.


Sobre el coste

El coste del examen es de $300 (€270 + 21% IVA si estás en España). Además, si ya has realizado otros exámenes de AWS, obtienes un 50% de descuento en el siguiente, por lo que éste examen sale por solo €163,35. Antes de la prueba oficial se puede hacer un examen de práctica que cuesta $40 (€36 + 21% de IVA si estás en España) y nuevamente, si ya obtuviste cualquier otra certificación de AWS, el coste es cero.


Dónde y cómo

Puedes realizar el examen en línea o ir a un centro de pruebas. Recomendamos ir a un centro de pruebas oficial, principalmente por la estabilidad de la conexión a internet. Es obligatorio el uso de mascarilla en la realización presencial. Sobre la documentación, es importante presentar dos acreditaciones.


Preparación

Con aproximadamente 2 horas al día (seis días a la semana) se puede preparar para el examen en un periodo de 6-8 semanas, siempre contando con experiencia previa en los servicios mencionados.

Recursos útiles durante la preparación:

[AWS Certified Data Analytics Specialty Exam Study Path from Tutorials Dojo] – es una lista bastante completa de temas que debes verificar antes del examen, con algunas preguntas de muestra, hojas de referencia para los servicios de análisis y algunos escenarios comunes en las preguntas del examen.

[AWS Analytics Overview] – un documento técnico con la descripción general de todos los servicios de análisis de AWS.

[Data Lakes and Analytics] – otra descripción general de los servicios de análisis de AWS.

[Data Analytics Fundamentals] – curso oficial de AWS, recomendable para comenzar la preparación.

[Exam Readiness: AWS Certified Data Analytics – Specialty] – curso oficial de AWS que te ayudará a cubrir todos los temas cubiertos en el examen.

[Visualizing with QuickSight] – un plan de estudio para mejorar tu comprensión sobre QuickSight. Imprescindible para dominar la parte de visualización del examen.

[AWS Hadoop Fundamentals] – aunque está un poco fuera de alcance, ayuda a comprender mejor Hadoop y cómo se integra en AWS EMR. Si conoces perfectamente Hadoop, este curso no es necesario.

Otros cursos de [AWS Training] cubriendo S3, RDS, SageMaker, etc. serán buenos para expandir su conocimiento ya que hay algunas preguntas que tocan estos temas y los cursos son realmente breves, pero informativos. En especial, echa un vistazo a todo lo relacionado con S3.

[Tutorials Dojo’s AWS Certified Data Analytics Specialty Practice Exams 2021] – esencial para poner a prueba tus conocimientos y descubrir dónde están tus puntos débiles. También es un buen indicador de su preparación para el examen; es recomendable obtener un 90% antes del examen.


Conclusión

Los exámenes de certificación de AWS necesitan esfuerzo y dedicación pero además, tener experiencia práctica ayuda mucho para enfrentarte a los mismos. Por ejemplo, si trabajas habitualmente con AWS Glue, seguramente no te hará falta estudiar mucho acerca de este servicio porque ya conoces sus capacidades y funcionalidad a partir de tu trabajo del día a día. Muchas veces esta experiencia es más relevante de cara al examen que revisar algunos posibles escenarios teóricos: si te has enfrentado directamente con los problemas estarás mucho más seguro de cara al examen.

Espero que este pequeño resumen te ayude a preparar la certificación y consigas mejorar tus capacidades profesionales en el análisis de datos. Anímate a realizarla y veras que, aunque requiere esfuerzo, es una meta perfectamente alcanzable.

¿Quieres saber más de lo que ofrecemos y ver otros casos de éxito?
DESCUBRE BLUETAB
Sergi Lehkyi
Data Engineer

En mi camino profesional he pasado por desarrollo web, administración de bases de datos, ciencia de datos y últimamente estoy enfocado en las tecnologías y soluciones de Cloud, especialmente AWS.

SOLUCIONES, SOMOS EXPERTOS

DATA STRATEGY
DATA FABRIC
AUGMENTED ANALYTICS

Te puede interesar

Gobierno de Datos: ¿tendencia o necesidad?

octubre 13, 2022
LEER MÁS

MDM como ventaja competitiva en las organizaciones

junio 18, 2024
LEER MÁS

Características esenciales que debemos tener en cuenta al adoptar un paradigma en la nube

septiembre 12, 2022
LEER MÁS

Snowflake, el Time Travel sin DeLorean para unos datos Fail-Safe.

febrero 23, 2023
LEER MÁS

Introducción a los productos de HashiCorp

agosto 25, 2020
LEER MÁS

LakeHouse Streaming en AWS con Apache Flink y Hudi

abril 11, 2023
LEER MÁS

Publicado en: Blog, Practices, Tech

Serverless Microservices

octubre 14, 2021 by Bluetab

Serverless Microservices

Francisco Linaje

AWS Solutions Architect

En esta práctica cloud veremos como construir microservicios dentro de AWS siguiendo el paradigma serverless. Este tipo de solución permite disponer de sistemas completamente administrados por AWS donde nosotros no deberemos preocuparnos por disponibilizar los recursos o administrarlos, simplemente especificaremos dentro de su configuración las políticas de ejecucion y escalado si se necesitasen, el pago por tanto es exclusivamente por uso.

Requisitos

Para poder realizar esta práctica deberemos disponer de las siguientes instalaciones que nos permitirán poder desarrollar los microservicios y operar el entorno.

  • Terraform: Desde donde crearemos y desplegaremos nuestro backend e infra.
  • Un usuario con credenciales de acceso al cli de AWS (access_key y secret_key) y permisos necesarios para operar los servicios empleados en la práctica.
  • AWS cli v2: Permitirá configurar los credenciales con aws configure, opcionalmente se pueden realizar exports del access_key y secret_key.
  • Un IDE en este caso visual-studio-code
  • Conocimiento básico en Api Rest y AWS
  • Postman

Overview

Se plantea un escenario donde diferentes usuarios mediante aplicaciones multiplataforma acceden a diferentes recursos dentro de la app consumiendo una API Rest segura desplegada en AWS, para ello deberán en primer lugar autenticarse contra el pool de cognito para obtener un token JWT que les permitirá consumir los diferentes endpoints de la API.

Autenticación y Autorización

Para poder ofrecer seguridad a la API y que sus servicios solo sean consumidos por usuarios autorizados, debemos emplear un broker que nos pueda ofrecer autenticacion y a su vez autorizacion sobre estos servicios.

En primer lugar deberemos de entender el flujo que sigue un usuario para poder ser autenticado y autorizado para consumir un microservicio. En el siguiente diagrama se pueden ver como en una serie de pasos un usuario puede consumir el endpoint.

  1. El usuario se autentica mediante sus credenciales contra el broker.
  2. Si los credenciales son correctos, el broker generara un token JWT que serviría como mecanismo de autorización al poder validar que el usuario es quien dice ser.
  3. Este token deberá emplearse en todas las llamadas a la API Rest, para ello se enviará en el header de Authorization junto a un prefijo Bearer.
  4. Si el token es validado por el broker, se le otorgará acceso al consumo del microservicio.

¿Que es JWT o Json Web Token?

Json Web Token es un estándar abierto (RFC 7519) donde se define una forma autónoma de asegurar integridad en los datos enviados gracias a que va firmada digitalmente con HMAC (firma simetrica – misma clave) o RSA(firma asimetrica – par de claves privada/publica) y asi asegurar que el peticionario es una entidad segura. Además estos tokens pueden estar encriptados adicionalmente para ocultar los reclamos a terceros. El escenario principal donde se emplea es en la autorización del peticionario.

Cada token JWT consta de tres partes separados por un “.” de la forma header.payload.signed codificados en base64 por separado.

Header: Indica el tipo de token y su firma, en el caso de RS256 incluirá el kid que identifica la clave pública del emisor del Token que se empleara para verificar la firma. Mediante el iss del payload y el kid de la clave podremos ver en el navegador https:///.well-known/jwks.json la firma publica empleada para descifrar el token.

Payload: Contiene los claims que permiten verificar los atributos de la generación del token: iss (emisor), aud (audience), exp( expiracion) y otros campos que se puedan añadir adicionalmente para enviar información.

AWS Cognito

Cognito es un servicio completamente administrado que ofrece autenticación de usuarios para aplicaciones multiplataforma, además permite el empleo de identidades federadas como Google, Amazon, Facebook, etc para su registro.

Los usuarios podrán registrarse y loguearse contra un pool de usuarios que funciona como directorio de usuarios donde serán albergados los parámetros de autenticación: email, password, número de teléfono, etc. Se ofrecen además opciones de confirmación de usuario mediante código o enlace vía email o sms. Los usuarios autenticados recibirán un token de acceso que podrán emplear para recibir autorización a los microservicios de Api Gateway. Mediante otras opciones no planteadas en este caso de uso, se podrá recibir credenciales temporales STS de AWS para acceder directamente a servicios AWS mediante la función AssumeRoleWithWebIdentity mediante los pool de identidades.

Los usuarios emplearan la interfaz web de Cognito para autenticarse y recibir un código que podrán intercambiar por un token JWT que emplearán como autorización en los microservicios desplegados en Api Gateway.

Empezaremos creando nuestro pool de usuarios.

  • Asignaremos un nombre al pool.
  • Añadiremos el atributo de acceso y verificación, en este caso email.
  • Definiremos como se realiza la verificación: via enlace. Además si se hará por defecto via SNS “COGNITO_DEFAULT” o via SES “DEVELOPER”.
  • La recuperación de la cuenta se hará vía el email verificado, siendo la prioridad 1, como maxima prioridad en caso de añadir futuros métodos opcionales.
resource "aws_cognito_user_pool" "user_pool" {
        name = var.cognito.user_pool_name
        auto_verified_attributes = ["email"]
        username_attributes = ["email"]

        verification_message_template {
            email_subject_by_link = "APP Notification - Account Verification"
            email_message_by_link = "Please click the link to verify your email address: {##VERIFY EMAIL##}\n<br><br>\n"
            default_email_option = "CONFIRM_WITH_LINK"
        }

        email_configuration {
                email_sending_account = "COGNITO_DEFAULT"
        }

        account_recovery_setting {
            recovery_mechanism {
            name = "verified_email"
            priority =  1
            }
        }
    } 

Por otro lado, activaremos la interfaz propia de AWS a modo de pruebas para poder realizar el proceso de autenticado contra cognito sin tener que realizar un desarrollo del frontend propio.

  • Indicamos que recibiremos un code para poder intercambiarlo por un token JWT, teniendo como scope el email.
  • El proveedor de identidad sera por default Cognito.
  • Indicaremos una url callback de prueba desde donde nos indicarán el code dentro de la url de la forma “?code=”.
resource "aws_cognito_user_pool_client" "client" {
  name = var.cognito.app_client_name
  user_pool_id = aws_cognito_user_pool.user_pool.id
  supported_identity_providers = ["COGNITO"]
  callback_urls = var.cognito.callback_urls
  allowed_oauth_flows_user_pool_client = var.cognito.user_pool_client
  allowed_oauth_flows = ["code"]
  allowed_oauth_scopes = ["openid","email"]
}

resource "aws_cognito_user_pool_domain" "main" {
  domain       = var.cognito.domain_name
  user_pool_id = aws_cognito_user_pool.user_pool.id
} 

Autorización de los microservicios

En Api Gateway configuraremos un autorizador que recibirá el token JWT y comprobará la firma del token, como el emisor y audiencia añadidos en los propios scopes. El proceso de comprobación es automático permitiendo el acceso directo al servicio si este es válido o denegando mediante un unauthorized la request.

resource "aws_apigatewayv2_authorizer" "jwtAuth" {
  api_id           = aws_apigatewayv2_api.api.id
  authorizer_type  = "JWT"
  identity_sources = ["$request.header.Authorization"]
  name             = var.api.jwt_authorizer_name

  jwt_configuration {
    audience = [aws_cognito_user_pool_client.client.id]
    issuer   = "https://${aws_cognito_user_pool.user_pool.endpoint}"
  }
} 

Obtención del token JWT

En primer lugar, deberemos abrir la interfaz proporcionada por Cognito para realizar los procesos de sign-up, sign-in. La podremos encontrar dentro de la consola de AWS, en el servicio de Cognito, dentro de la configuración del cliente de aplicación «Lanzar interfaz de usuario alojada».

    1. Procederemos a registrar un usuario

    2. Nos pedira que confirmemos el usuario a través del enlace enviado a nuestra cuenta de correo introducida.

    3. El correo recibido tendrá la siguiente forma:

    4. Accedemos al enlace de VERIFY EMAIL

    5. Finalmente tendremos ya nuestro usuario confirmado en nuestro pool de usuarios de Cognito.

    6. Procedemos a logearnos en la interfaz de AWs empleada anteriormente y si los credenciales son correctos nos devolverá a la url de callback configurada cuando hemos creado el pool de usuarios con Terraform junto a un code que emplearemos posteriormente para obtener el token.

  1. Por último, para poder obtener el token debemos realizar una llamada al endpoint de Cognito /oauth2/token con los siguientes atributos en el body.
  • Metodo POST
  • Body
    • Aplicacion x-www-form-urlencoded.
    • Grant_type: authorization_code.
    • Client_id: tu id de la aplicación de Cognito.
    • Redirect_uri: url callback.
    • Code: código obtenido en la url de callback después de “?code=”

Obtendremos como respuesta el identity token (id_token) que contiene toda la informacion personal del usuario y es el que generalmente se empleará para la autorización y el access_token empleado principalmente para llamar a servicios externos sin incluir informacion personal del usuario. Dependiendo del caso de uso, si realmente la información aportada por el identity token no es necesaria, es recomendable emplear el access_token. Por ultimo el refresh token, se emplea principalmente para obtener un identity o access tokens nuevos.

Microservicios

Los microservicios seran desplegados en Api Gateway y tendrán como backend Lambda integrada como proxy y DynamoDB como bbdd. Todos estos servicios funcionan de forma completamente administrada siguiendo los objetivos serverless de esta práctica.

Vamos a definir brevemente estos servicios y ver cual es su papel dentro de la arquitectura.


AWS Api Gateway v2

Mediante Api Gateway podremos desarrollar APIs de una forma sencilla, segura y escalable, ademas de ofrecernos la integración con Lambda para poder operar sin aprovisionamiento. Solo funciona con HTTPs

Dispone de integración proxy para exponer por completo el request como input al backend. En el caso concreto del workshop se empleará Lambda Proxy Integration para poder consumir los parámetros desde el handler de la función vía el evento, para ello deberemos definir en la etapa de implementación POST como tipo, independientemente de que definamos el metodo HTTP del endpoint como GET.

Api Gateway es compatible con CloudFront como de CDN de la API, además es posible incorporar WAF como servicio de mitigación de ataques DDoS.

AWS actualmente dispone de dos versiones de Api Gateway, nosotros desplegaremos la última version, v2.

¿Que diferencias podemos encontrar? Los principales cambios introducidos con la version 2, se basan en:

  • Reducción de costes: 70%, 3.5$ vs 1$ por millón de peticiones.
  • Reducción de la latencia sobre el 50%.
  • Soporte a referencias cruzadas CORS.
  • JWT Authorizers a traves de OIDC y OAuth 2.0.
  • Disponible ruta y stages predeterminadas.
  • Integrado con SAM y CloudFormation.

Para poder crear nuestra Api deberemos crear los siguientes recursos

  • Api: indicando el nombre y su protocolo.
  • Un grupo de registros de Cloudwatch para la Api.
  • Un stage de implementación donde indicaremos el nombre de la implementacion y los parametros de los logs.
resource "aws_apigatewayv2_api" "api" {
  name          = var.api.api_name
  protocol_type = "HTTP"
}

resource "aws_cloudwatch_log_group" "api_gw" {
  name = "/aws/api_gw/${aws_apigatewayv2_api.api.name}"
  retention_in_days = 30
}

resource "aws_apigatewayv2_stage" "stage" {
  api_id = aws_apigatewayv2_api.api.id

  name        = var.api.stage_name
  auto_deploy = true


  access_log_settings {
    destination_arn = aws_cloudwatch_log_group.api_gw.arn

    format = jsonencode({
      requestId               = "$context.requestId"
      sourceIp                = "$context.identity.sourceIp"
      requestTime             = "$context.requestTime"
      protocol                = "$context.protocol"
      httpMethod              = "$context.httpMethod"
      resourcePath            = "$context.resourcePath"
      routeKey                = "$context.routeKey"
      status                  = "$context.status"
      responseLength          = "$context.responseLength"
      integrationErrorMessage = "$context.integrationErrorMessage"
    }
    )
  }
} 

Una vez creada la Api, crearemos los dos endpoints que emplearemos en este workshop: GET,POST

Para ambos indicaremos:

  • Tipo de autorización: JWT
  • Tipo de integración AWS_PROXY, para recibir integramente en la funcion lambda la request
  • Metodo de integración sera POST aun cuando el método del endpoint se declare como GET, ya que Lambda solo se activa a traves de peticiones POST.
  • Id del autorizador creado jwtAuth que tendrá como audience el id de la aplicación Cognito creada y su endpoint como issuer.
  • Se creará además un permiso de ejecución de la función Lambda especificada para su ejecución por parte de ApiGateway

 

GET

#### GET
resource "aws_apigatewayv2_integration" "get_item_app_integration" {
  api_id           = aws_apigatewayv2_api.api.id
  integration_type = "AWS_PROXY"
  description               = "Lambda GET example"
  integration_method        = "POST"
  integration_uri           = aws_lambda_function.get_item_app.invoke_arn
}

resource "aws_apigatewayv2_route" "get_item_app_route" {
  api_id = aws_apigatewayv2_api.api.id

  route_key = "GET /user"
  target    = "integrations/${aws_apigatewayv2_integration.get_item_app_integration.id}"
  authorization_type = "JWT"
  authorizer_id = aws_apigatewayv2_authorizer.jwtAuth.id
}

resource "aws_lambda_permission" "get_item_app_execution" {
  statement_id  = "AllowExecutionFromAPIGateway"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.get_item_app.function_name
  principal     = "apigateway.amazonaws.com"
  source_arn = "${aws_apigatewayv2_api.api.execution_arn}/*/*"
} 

POST

resource "aws_apigatewayv2_integration" "create_item_app_integration" {
  api_id           = aws_apigatewayv2_api.api.id
  integration_type = "AWS_PROXY"
  description               = "Lambda example"
  integration_method        = "POST"
  integration_uri           = aws_lambda_function.create_item_app.invoke_arn
}

resource "aws_apigatewayv2_route" "create_item_app_route" {
  api_id = aws_apigatewayv2_api.api.id

  route_key = "POST /user"
  target    = "integrations/${aws_apigatewayv2_integration.create_item_app_integration.id}"
  authorization_type = "JWT"
  authorizer_id = aws_apigatewayv2_authorizer.jwtAuth.id
}

resource "aws_lambda_permission" "create_item_app_execution" {
  statement_id  = "AllowExecutionFromAPIGateway"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.create_item_app.function_name
  principal     = "apigateway.amazonaws.com"
  source_arn = "${aws_apigatewayv2_api.api.execution_arn}/*/*"
} 

AWS Lambda

Para el desarrollo de la lógica de nuestros microservicios emplearemos AWS Lambda, servicio de computación completamente administrado con escalado automático. Donde se definen unos recursos de memoria y CPU para realizar la ejecución de cada función. Soporta de forma nativa lenguajes como Java, NodeJS, Python, etc. Las funciones son albergadas en un paquete de implementación del tipo zip alojadas en un bucket de S3 interno o creado por nosotros.

 

En primer lugar, crearemos el rol de ejecución, donde además definiremos que acciones se pueden ejecutar dentro de estas y sobre que servicios, en nuestro caso simplemente permitiremos acciones CRUD sobre la tabla DynamoDB especifica que crearemos posteriormente.

  • Crearemos el rol de Lambda y daremos permisos al servicio de Lambda para asumirlo
  • Crearemos dos políticas básicas que serán asignadas a este rol: ejecución (permiso para cargar registros en CloudWatch) y una segunda política custom donde estarán definidas las acciones que podrán realizarse sobre la tabla de DynamoDb empleada para el workshop.
resource "aws_iam_role" "lambda_exec_dev" {
  name = "serverless_lambda_dev"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = "sts:AssumeRole"
      Effect = "Allow"
      Sid    = ""
      Principal = {
        Service = "lambda.amazonaws.com"
      }
    }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "lambda_policy_attachment_dev" {
  role       = aws_iam_role.lambda_exec_dev.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

resource "aws_iam_role_policy_attachment" "lambda_dynamodb_policy_attachment_dev" {
  role       = aws_iam_role.lambda_exec_dev.name
  policy_arn = aws_iam_policy.lambda_dynamodb_policy_dev.arn
}

resource "aws_iam_policy" "lambda_dynamodb_policy_dev" {
  name        = "lambda_dynamodb_policy_dev"
  description = "Lambda DynamoDB access"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
          "dynamodb:Query",
          "dynamodb:GetItem",
          "dynamodb:PutItem",
          "dynamodb:UpdateItem",
          "dynamodb:BatchWriteItem",
          "dynamodb:BatchGetItem",
        ]
        Effect   = "Allow"
        Resource = [aws_dynamodb_table.app_table.arn]
      },
    ]
  })
  depends_on = [aws_dynamodb_table.app_table]
} 

Las funciones irán recogidas en un zip con el codigo Python que será subido a un bucket interno que crearemos expresamente para albergarlas. En cada función indicaremos su handler, runtime, rol de ejecución y el bucket/objeto donde poder encontrar el paquete de funciones.

data "archive_file" "lambda_functions_package" {
  type = "zip"

  source_dir  = "${path.module}/scripts/"
  output_path = "${path.module}/scripts/crud_lambdas.zip"
}

resource "aws_s3_bucket_object" "lambda_functions_package_object" {
  bucket = aws_s3_bucket.internal_dev.bucket
  key    = "crud_lambdas.zip"
  source = data.archive_file.lambda_functions_package.output_path
  etag = filemd5(data.archive_file.lambda_functions_package.output_path)
}

resource "aws_s3_bucket" "internal_dev" {
  bucket = var.bucket_name
  acl    = "private"
} 

Para su creacion simplemente indicaremos el nombre de la función Lambda, rol de ejecución, runtime, la función de ejecucíon, el bucket y el zip donde estan alojadas.

  • La función get_item_app nos devolvera el usuario buscado por id.
  • La función create_item_app permitira guardar el usuario en bbdd.
resource "aws_lambda_function" "get_item_app" {
  function_name = "get_user"
  handler       = "get_user.lambda_handler"
  runtime       = "python3.6"

  s3_bucket = aws_s3_bucket.internal_dev.bucket
  s3_key    = aws_s3_bucket_object.lambda_functions_package_object.key
  source_code_hash = data.archive_file.lambda_functions_package.output_base64sha256
  role = aws_iam_role.lambda_exec_dev.arn
}

resource "aws_lambda_function" "create_item_app" {
  function_name = "create_user"
  handler       = "create_user.lambda_handler"
  runtime       = "python3.6"

  s3_bucket = aws_s3_bucket.internal_dev.bucket
  s3_key    = aws_s3_bucket_object.lambda_functions_package_object.key
  source_code_hash = data.archive_file.lambda_functions_package.output_base64sha256
  role = aws_iam_role.lambda_exec_dev.arn
} 

Estas funciones Python emplearán la librería boto3 para poder realizar de una forma sencilla y rápida el conector contra la tabla de DynamoDB, estarán alojadas bajo el directorio de /scrips.

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('AppDummy') 

A partir de este conector mediante table.get_item() o table.create_item() podremos realizar nuestras operaciones GET y POST respectivamente. Si la acción se ejecuta correctamente lanzaremos un codigo 200 y devolveremos el objeto añadido/obtenido.

  • En la funcion create_user obtendremos los atributos del body de la request que se encontrarán en el propio evento al ser una función integrada como AWS_PROXY.
item = json.loads(event["body"])
user = item["User"]
...
table = dynamodb.Table('AppDummy')
    response = table.put_item(
        Item=user
    )
... 
  • En la función get_user encontraremos el atributo de búsqueda UserId dentro del evento en “queryStringParameters”.
id = str(event["queryStringParameters"]['UserId'])
...
response = table.get_item(
        Key={
            'UserId': id
        }
    )
... 

AWS DynamoDB

Por último, crearemos una tabla básica para albergar los atributos de nuestros usuarios, tendrá simplemente como PK el id de usuario en string para soportar alfanuméricos.

resource "aws_dynamodb_table" "app_table" {
  name           = "AppDummy"
  billing_mode   = "PAY_PER_REQUEST"
  hash_key       = "UserId"

  attribute {
    name = "UserId"
    type = "S"
  }
} 

Despliegue

Mediante Terraform desplegaremos nuestra infraestructura, primero deberemos lanzar un init que descargará los plugins y inicializará nuestro directorio de trabajo con los archivos de configuración de AWS, para posteriormente ejecutar un plan, si el despliegue de recursos planificado por el plan concuerda con lo que buscamos finalmente ejecutaremos un apply para desplegar toda nuestra infra y realizar las pruebas.

En esta práctica, nuestro estado permanecera en local, no configuraremos AWS como backend para los estados de Terraform

terraform init

terraform plan -var-file="env/dev.tfvars"

terraform apply -var-file="env/dev.tfvars" 

Pruebas

Una vez desplegado nuestro proyecto, comprobaremos el correcto funcionamiento de los microservicios que hemos programado. Además en ambos casos, deberemos añadir el campo authorization en el header de la peticion HTTP con el prefijo Bearer y el access_token obtenido anteriormente para poder ser autorizados a consumir el microservicio.

POST /user

En primer lugar, probaremos la peticion HTTP POST /user, debería añadir el usuario a la tabla de DynamoDB creada y enviando como respuesta un codigo 200 y el usuario añadido a la tabla.

GET /user

De la misma forma, consumiremos el microservicio que a partir del UserId nos devolverá la información del usuario, recordando que al ser una petición GET la información relativa a la consulta irá en el Query Params (propia url)

Conclusión

En esta práctica hemos podido aprender a como desarrollar una API Rest segura y completamente administrada dentro del entorno de AWS, sirviendonos de la última versión de Api Gateway que facilita la integración nativa con autorizadores de JWT.

La integración de Cognito con Api Gateway nos torga la capa de seguridad y administración de los usuarios. Respectivamente con Lambda y DynamoDB disponemos de la capa de lógica/persistencia de nuestra API. La integración nativa de todos estos servicios nos facilita el desarrollo de estas aplicaciones al disminuir la carga de trabajo dedicada tanto al desarrollo puro, como a la integracion de los distintos servicios involucrados y su administración, además gracias a Terraform disponemos de toda la infraestructura como código facilitando su futura evolución y disponibilización en otros entornos de una forma mucho más rápida y comprensible.

En futuras entradas veremos como desarrollar otros escenarios típicos que podemos encontrar en nuestro día a día dentro de AWS, con el fin de tener unas primeras herramientas para poder solventar futuros escenarios que se nos planteen.

Espero que la práctica haya sido de vuestro agrado e interés, os espero en futuras entregas!

Enlaces de interés

  • Documentación general de Cognito.
  • Documentación general de Api Gateway.
  • Documentación general de Lambda.
  • Documentación general de DynamoDB.
  • Documentación general de JWT.
Navegación

Introducción

Requisitos

Overview

Autenticación y Autorización

Microservicios

Despliegue

Pruebas

Conclusión

Enlaces de interés

¿Quieres saber más de lo que ofrecemos y ver otros casos de éxito?
DESCUBRE BLUETAB

Francisco Linaje

AWS Solutions Architect

SOLUCIONES, SOMOS EXPERTOS

DATA STRATEGY
DATA FABRIC
AUGMENTED ANALYTICS

Te puede interesar

¿Qué está pasando en el mundo de la AI?

marzo 6, 2023
LEER MÁS

5 errores comunes en Redshift

diciembre 15, 2020
LEER MÁS

LA BANCA Y LA ERA DEL OPEN DATA

abril 19, 2023
LEER MÁS

Cómo depurar una Lambda de AWS en local

octubre 8, 2020
LEER MÁS

Los Incentivos y el Desarrollo de Negocio en las Telecomunicaciones

octubre 9, 2020
LEER MÁS

Mitos y verdades de los ingenieros de software

junio 13, 2022
LEER MÁS

Publicado en: Blog, Practices, Tech

Desplegando una plataforma CI/CD escalable con Jenkins y Kubernetes

septiembre 22, 2021 by Bluetab

Desplegando una plataforma CI/CD escalable con Jenkins y Kubernetes

Lucas Calvo Berlanga

Cloud Engineer

En este artículo de la práctica cloud veremos cómo crear una plataforma de CI/CD de una forma totalmente automatizada. Para ello nos apoyaremos en una metodología GitOps para así realizar nuestros despliegues de una forma más sencilla, escalable e industrializada.

La idea de este taller es crearnos un cluster de GKE donde tengamos desplegado Jenkins como nuestra pieza central de CI/CD y que este vaya escalando en agentes de una forma totalmente automatizada para que según la demanda de ejecuciones de jobs nuestro cluster crezca o decrezca de una forma transparente para nosotros. Otra de las grandes ventajas de esta arquitectura es que podemos crear diferentes tipos de slaves para así cubrir todo tipo de ejecuciones dentro de nuestra compañía.

Los componentes/herramientas que usaremos en este proyecto serán los siguientes:

  • Terraform.
  • GKE.
  • Jenkins.
  • Prometheus.
  • Grafana.
  • Slack.

Objetivos

  1. Creación de la las vpcs donde se desplegará la infraestructura.
  2. Creación de la infraestructura, en nuestro caso GKE, de una forma totalmente automatizada.
  3. Despliegue de Jenkins como componente principal haciendo uso del provider del helm.
  4. Configuración de Jenkins usando el plugin de Jcasc.
  5. Despliegue de prometheus para la monitorización de nuestro sistema haciendo uso de helm.
  6. Despliegue de grafana para la monitorización de nuestro sistema haciendo uso de helm.
  7. Configuración de grafana haciendo uso de prometheus y unos dashboard configurados automáticamente.
  8. Revisión de toda la infraestructura levantada y chequeo de la monitorización.
  9. Ejecución de un Job de ejemplo para ver el flujo completo.
  10. Comprobar sistema de alertas tanto caída de sistemas como de jobs completos.
  11. Comprobar el escalado de nuestra infraestructura y de los componentes desplegados.

Introducción a Terraform

Terraform es una herramienta open-source para automatizar la creación de infraestructura como código. Para nuestro caso de uso usaremos Terraform tanto para desplegar la infraestructura, tanto GKE como la VPC donde se hará el despliegue de este último.

Terraform no solo nos permite desplegar infraestructura como código sino que también nos da la opción de usar otros provedores como el de Kubernetes para la creación de namespace (entre otras muchas cosas) o la posibilidad de realizar despliegues de otros componentes dentro del cluster de GKE con el proveedor de Helm. Terraform.

Introducción a GKE

GKE es el servicio de Kubernetes gestionado y autoescalado por GCP. Es donde se realizarán todos los despliegues tanto de Jenkins como de la monitorización que tendrá nuestra plataforma. GKE. Este componente lo vamos a automatizar haciendo uso de Terraform donde se harán las implementaciones necesarias para realizar el despliegue correctamente.

Introducción a Jenkins

Jenkins es nuestra pieza central de la plataforma de CI/CD. Jenkins es una herramienta de construcción, implementación y automatización de proyectos software. Para el despliegue de este componente nos apoyaremos en el provider de Helm de Terraform. Jenkins.

Introducción a Prometheus

Prometheus es un sistema de monitorización que usaremos para comprobar el estado de todos los pods desplegados en nuestra plataforma, así como para monitorizar el estado de nuestra infraestructura como tal(Picos de consumo, nodos caídos…). Para el despliegue de este componente nos apoyaremos en el provider de Helm de Terraform. Prometheus.

Introducción a Grafana

Grafana es nuestra herramienta de visualización de la monitorización. Nos decantamos por esta herramienta ya que se integra perfectamente con prometheus y nos permite crear dashboards personalizados de nuestra infraestructura. Para el despliegue de este componente nos apoyaremos en el provider de Helm de Terraform. Grafana.

Introducción a Slack

Por último, haremos uso de slack como herramienta de envió de alertas tanto en la ejecución de los jobs de Jenkins como alertas de monitorización de caídas en alguno de los pods desplegados. Slack

Preparación de entorno

Para la ejecución de la plataforma CI/CD será necesario hacer la instalación de estas herramientas:
  1. Terraform. Aquí se usa la versión v1.0. Se puede descargar aquí. Para instalar una versión anterior consultar aquí.
  2. Helm. Guía de instalación de Helm.
  3. GCP. Para la realización del taller será necesario la creación de una cuenta en GCP.

Clonación de repositorio

El código fuente está disponible en github.

git clone https://github.com/lucasberlang/gitops-kubernetes-jenkins/
cd gitops-kubernetes-jenkins 

Índice de ficheros

Dentro de la carpeta src tendremos todo el código necesario para hacer el despliegue de nuestra infraestructura de una forma automatizada. En este apartado haremos un breve resumen de lo que contienen cada uno de los ficheros para que nuestro proyecto funcione.

  • providers.tf

Definición de los proveedores que usaremos para hacer el despliegue con Terraform. En nuestro caso haremos uso del provider de Google, helm y kubernetes.

  • terraform.tfvars

Variables que usaremos dentro de los módulos de Terraform.

  • variables.tf

Definición de las variables que usaremos tanto en los módulos de Terraform como en los secretos que desplegaremos para hacer uso en Jenkins.

  • outputs.tf

Información del proyecto que nos interesa conocer.

  • main.tf

Contendrá la lógica realizada en Terraform para hacer el despliegue de los componentes de infraestructura que necesitamos, en este caso VPC y GKE. Para ello haremos uso de dos módulos desarrollados por Bluetab.

  • kubernetes_secrets.tf

Archivo que contendrá toda la información sensible que usaremos dentro de los despliegues que realizaremos (Grafana, Prometheus, Jenkins), como contraseñas, tokens, etc… .

  • helm_monitoring.tf

Archivo que contiene la configuración que se realizará con el proveedor de Helm para el despliegue de los componentes Prometheus y Grafana.

  • helm_jenkins.tf

Archivo que contiene la configuración que se realizará con el proveedor de Helm para el despliegue de los componentes Jenkins.

  • grafana.yaml

En este archivo contiene la configuración inicial que usará Helm cuando realicemos el despliegue de grafana.

  • prometheus.yaml

En este archivo contiene la configuración inicial que usará Helm cuando realicemos el despliegue de prometheus.

  • jenkins.yaml

En este archivo contiene la configuración inicial que usará Helm cuando realicemos el despliegue de jenkins.

  • vars.example.env

Archivo que contendrá todas las variables de entorno que usaremos en nuestro proyecto. En este caso le pasaremos las variables más sensibles como pueden ser la contraseña de grafana o el token usado en slack para no tener que subirlo al repositorio.

Configuración de Slack

  1. Primero de todo nos tendremos que registrar en Slack.
  2. Una vez que tengamos el registro se deberá seguir la guían de configuración de Slack con Jenkins. De este paso solo necesitaremos el token de Slack que pasaremos como variable de entorno TF_VAR_slack_token.
  3. Además, nos crearemos un canal en Slack el cual pasaremos posteriormente como variable de entorno, TF_VAR_slack_channel.
  4. Será necesario también coger el nombre del dominio de slack y pasárselo como variable,TF_VAR_team_domain.
  5. Por último nos crearemos un webhook para el envío de alertas desde Prometheus. Para ello podemos seguir esta guía. De este paso solo necesitaremos el endpoint para luego pasarlo como variable de entorno, TF_VAR_slack_api_url.

Configuración de variables

Antes de realizar la ejecución de nuestro proyecto debemos definir una serie de variables de entorno que necesitaremos para el correcto funcionamiento de este. Para ello haremos uso del archivo vars.example.env donde tenemos ya definidas las variables más importantes. Para efectos de la demo las únicas variables que se tendrán que modificar son las siguientes:

  • TF_VAR_project_id: el id del proyecto de la cuenta de GCP donde se realizará el despliegue de la infraestructura.
  • TF_VAR_vault_addr: se añadirá la dirección de vault para la autenticación en el despliegue del proyecto de prueba. En caso de no tener vault se podrá también configurar con las credenciales de Azure o crearse otra proyecto de ejemplo.
  • TF_VAR_vault_token: token de vault que se utilizará para autenticarse contra Azure en nuestro proyecto de demo. Igual que con la variable de arriba no será necesaria si se configura el proyecto demo para autenticarse con las credenciales de Azure.
  • TF_VAR_slack_api_url: El endpoint de Slack que configuraremos para que se envíen las alertas de nuestra plataforma.
  • TF_VAR_slack_channel: Canal de Slack donde se publicarán los mensajes de las alertas.
  • TF_VAR_slack_token: El token de Slack que configuraremos para que se envíen las alertas de nuestra plataforma.
  • TF_VAR_team_domain: El dominio de Slack que configuraremos para que se envíen las alertas de nuestra plataforma.
  • TF_VAR_user_grafana: Nombre del usuario de Grafana que usaremos para loguearnos.
  • TF_VAR_password_grafana: Contraseña para el usuario de Grafana definido anteriormente para hacer el login.

Una vez se hayan configuradas todas estas variables se tendrá que lanzar el siguiente comando para que queden como variables de entornos.

source vars.example.env 

Configuración VPC

Para la configuración de la VPC haremos uso del módulo corporativo desarrollado por Bluetab. Este módulo está publica en el repositorio de github y está totalmente documentado por si se tiene alguna duda de su funcionamiento o se quiere realizar alguna modificación en la VPC. Para que esto funcione lo único que debemos de hacer es instanciar nuestro módulo con las variables necesarias para hacerle funcionar:

module "network" {
  source = "git@github.com:lucasberlang/gcp-network.git"

  project_id         = var.project_id
  description        = var.description
  enable_nat_gateway = true

  intra_subnets = [
    {
      subnet_name           = "private-subnet01"
      subnet_ip_cidr        = "10.0.0.0/16"
      subnet_private_access = false
      subnet_region         = var.region
    }
  ]

  secondary_ranges = {
    private-subnet01 = [
      {
        range_name    = "private-subnet01-01"
        ip_cidr_range = var.ip_range_pods
      },
      {
        range_name    = "private-subnet01-02"
        ip_cidr_range = var.ip_range_services
      },
    ]
  }

  labels = var.labels
} 

Lo único necesario para ejecutar la creación de la VPC será el id del proyecto de GCP. Además de desplegar la VPC con la creación del módulo se realizará el despliegue de una subred a nuestra VPC con el direccionamiento 10.0.0.0/16. Este es totalmente modificable así como el nombre de la subred o la región en donde se despliega. También se han creado dos rangos secundarios de direccionamientos para los pods y servicios que desplegaremos en GKE.

Configuración GKE

Para el despliegue del proyecto se ha optado por usar infraestructura totalmente gestionado, en este caso haremos uso de GKE. Para automatizar el despliegue usaremos el módulo corporativo desarrollado por Bluetab. Este módulo está publica en el repositorio de github y esta totalmente documentado por si se tiene alguna duda de su funcionamiento o se quiere realizar alguna modificación en el GKE. Para que esto funcione lo único que debemos de hacer es instanciar nuestro módulo con las variables necesarias para hacerle funcionar:

module "gke" {
  source = "git@github.com:lucasberlang/gcp-gke.git"

  project_id              = var.project_id
  name                    = "gitops"
  regional                = true
  region                  = var.region
  network                 = module.network.network_name
  subnetwork              = module.network.intra_subnet_names.0
  ip_range_pods           = "private-subnet01-01"
  ip_range_services       = "private-subnet01-02"
  enable_private_endpoint = false
  enable_private_nodes    = false
  master_ipv4_cidr_block  = "172.16.0.0/28"
  kubernetes_version      = "latest"

  master_authorized_networks = [
    {
      cidr_block   = module.network.intra_subnet_ips.0
      display_name = "VPC"
    },
    {
      cidr_block   = "0.0.0.0/0"
      display_name = "shell"
    }
  ]

  node_pools = [
    {
      name         = "default-node-pool"
      machine_type = "n1-standard-4"
    },
  ]

  istio     = var.istio
  dns_cache = var.dns_cache
  labels    = var.labels
} 

Como se puede observar en el código la implementación del módulo es bastante intuitivo solo será necesario declarar algunas variables. Las variables más importantes son el tipo de instancia que usaran los node-pools y la subred donde se desplegará el cluster de GKE que para esto usaremos la red declarada anteriormente.

El funcionamiento de GKE en nuestra VPC será este:

Configuración Jenkins

Para la configuración de Jenkins como hemos dicho anteriormente usaremos el provider de Helm. Para hacer el despliegue nos crearemos un namespace llamado gitops donde se desplegará Jenkins. El fichero donde se hará esta configuración es helm_jenkins.tf.

resource "kubernetes_namespace" "jenkins" {
  metadata {
    name = "gitops"
  }
} 

Luego procederemos a hacer el despliegue de Jenkins con Helm.

data "local_file" "helm_chart_values" {
  filename = "${path.module}/templates/jenkins.yaml"
}

resource "helm_release" "jenkins" {
  name       = "jenkins"
  repository = "https://charts.jenkins.io"
  chart      = "jenkins"
  version    = "3.5.3"
  namespace  = kubernetes_namespace.jenkins.metadata.0.name
  timeout    = 180

  values = [data.local_file.helm_chart_values.content]

} 

Cogeremos el chart del repositorio oficial de Jenkins y le pasaremos el fichero jenkins.yaml como configuración inicial.

De este fichero vamos a destacar algunos puntos que hemos modificado para realizar una automatización de nuestro servicio.

 image: "jenkins/jenkins"
  imagePullPolicy: "Always"
  adminSecret: true
  adminUser: "admin"
  jenkinsUrl: "http://${kubernetes_endpoint}:80" 

Se ha realizado la modificación del nombre del adminUser para que sea más genérico y jenkinsUrl para que nos de el endpoint de Jenkins en el GKE cuando nos leguen las alertas de los jobs en el slack.

  containerEnv:
    - name: kubernetes_endpoint
      valueFrom:
        secretKeyRef:
            name: jenkins-k8s-config
            key: kubernetes_endpoint
    - name: gitlab_username
      valueFrom:
        secretKeyRef:
            name: gitlab-credentials
            key: gitlab_username
    - name: gitlab_ssh_key
      valueFrom:
        secretKeyRef:
            name: gitlab-credentials
            key: gitlab_ssh_key
    - name: vault_token
      valueFrom:
        secretKeyRef:
            name: vault-credentials
            key: vault_token
    - name: vault_addr
      valueFrom:
        secretKeyRef:
            name: vault-credentials
            key: vault_addr
    - name: arm_access_key
      valueFrom:
        secretKeyRef:
            name: azure-credentials
            key: arm_access_key
    - name: slack_token
      valueFrom:
        secretKeyRef:
            name: slack-credentials
            key: slack_token
    - name: team_domain
      valueFrom:
        secretKeyRef:
            name: slack-credentials
            key: team_domain            
  servicePort: 80
  serviceType: LoadBalancer 

Estos son todos los secrets que hemos definido en el archivo kubernetes_secrets.tf. Estos son necesarios para el correcto funcionamiento de Jenkins ya que haremos uso de muchos de estos secretos cuando realicemos la configuración con el plugin de Jcasc de Jenkins.

Además, en la configuración del serviceType lo hemos definido como LoadBalancer externo y el servicePort 80 para que sea visible desde el exterior.

installPlugins:
    - kubernetes
    - docker-custom-build-environment
    - ansicolor
    - aws-credentials
    - azure-credentials
    - gitlab-api
    - gitlab-branch-source
    - docker-java-api
    - github-branch-source
    - pipeline-graph-analysis
    ... 

Listado de todos los plugins que se instalarán por defecto en la configuración inicial de Jenkins.

Configuración inicial de Jcasc

Jcasc es el plugin de configuración como código de Jenkins. Lo usaremos para hacer una serie de configuraciones previas como pueden ser:

  • Configuración de los slaves:

Esta parte es donde definiremos la creación de un cloud de kubernetes donde se desplegarán todos nuestros pods cada vez que se lance una ejecución de un job en nuestro Jenkins. También configuraremos la imagen que se usará para desplegar cada slave y el namespace donde se realizará dicho despliegue. Para nuestro ejemplo usaremos una imagen ya modificada con la instalación de Terraform y algunos componentes como el SDK de Google o el cli de azure (lucasbluetab/jnlp-agent-Terraform-gcloud:latest). Además, se configurará los recursos que gaste cada vez que se levante un pod en cualquier ejecución de nuestros, consiguiendo así una infraestructura totalmente escalable.

      cloud: |
        jenkins:
          clouds:
            - kubernetes:
                name: "Terraform-executors"
                serverUrl: "https://kubernetes.default"
                jenkinsTunnel: "jenkins-agent:50000"
                jenkinsUrl: "http://jenkins:80"
                skipTlsVerify: true
                namespace: "gitops"
                templates:
                    - name: "jenkins-jnlp"
                      namespace: "gitops"
                      nodeUsageMode: NORMAL
                      label: "jnlp-exec"
                      containers:
                        - name: "jnlp"
                          image: "jenkins/jnlp-slave"
                          alwaysPullImage: false
                          workingDir: "/home/jenkins/agent"
                          ttyEnabled: true
                          command: ""
                          args: ""
                          resourceRequestCpu: "500m"
                          resourceLimitCpu: "1000m"
                          resourceRequestMemory: "1Gi"
                          resourceLimitMemory: "2Gi"
                      volumes:
                        - emptyDirVolume:
                            memory: false
                            mountPath: "/tmp"
                      idleMinutes: "1"
                      activeDeadlineSeconds: "120"
                      slaveConnectTimeout: "1000"
                    - name: "Terraform"
                      namespace: "gitops"
                      nodeUsageMode: NORMAL
                      label: "Terraform-exec"
                      containers:
                        - name: "Terraform"
                          image: "lucasbluetab/jnlp-agent-Terraform-gcloud:latest"
                          alwaysPullImage: false
                          workingDir: "/home/jenkins/agent"
                          ttyEnabled: true
                          command: "/bin/sh -c"
                          args: "cat"
                          resourceRequestCpu: "100m"
                          resourceLimitCpu: "500m"
                          resourceRequestMemory: "500Mi"
                          resourceLimitMemory: "1Gi"
                      volumes:
                        - emptyDirVolume:
                            memory: false
                            mountPath: "/tmp"
                      podRetention: "never"
                      activeDeadlineSeconds: "900"
                      slaveConnectTimeout: "1000" 
  • Configuración de las credenciales: se definirán todas las credenciales que se usarán dentro de Jenkins, en nuestro caso las credenciales de Jenkins y alguna extras que podremos configurar si es necesario.
      credentials: |
          credentials:
              system:
                  domainCredentials:
                  - credentials:
                    - basicSSHUserPrivateKey:
                        scope: GLOBAL
                        id: "GitLab"
                        username: ${gitlab_username}
                        passphrase: ""
                        privateKeySource:
                          directEntry:
                            privateKey: ${gitlab_ssh_key}
                    - string:
                        scope: GLOBAL
                        id: vaultUrl
                        secret: ${vault_addr}
                    - string:
                        scope: GLOBAL
                        id: vaultToken
                        secret: "${vault_token}"
                    - string:
                        scope: GLOBAL
                        id: azureARMKey
                        secret: "${arm_access_key}"
                    - string:
                        scope: GLOBAL
                        id: slackToken
                        secret: "${slack_token}"
                    - string:
                        scope: GLOBAL
                        id: teamDomain
                        secret: "${team_domain}" 
  • Configuración de Slack: Aquí se pasará la configuración que realizamos en Jenkins en nuestro caso le indicaremos el dominio de nuestro Slack, para nuestras pruebas Bluetabmundo, y la sala por defecto donde se escribirán todas las alertas que manden nuestros jobs. Para terminar, le pasaremos el token de Slack para que haga correctamente la conexión. Tanto el teamDomain como la room deberán ser modificados con los que habéis creado en el apartado de configuración de Slack.
      unclassified: |
        unclassified:
          slackNotifier:
            teamDomain: bluetabmundo
            room: "#jenkins"
            tokenCredentialId: slackToken 
  • Arreglo de bugs: Una de las ventajas más interesantes de Jcasc es que nos permite la utilización de scripts de groovy dentro de su configuración. En nuestro caso crearemos un script para quitar un bug que salta en Jenkins al usar el plugin de de env-injector. Básicamente este script nos quita un warning que no debería salir pero por un bug de la versión del plugin salta.
      scriptgroovy: |
        groovy:
          - script: >
              import jenkins.model.*;
              import jenkins.security.*;
              import hudson.security.*;
              import hudson.model.*;
              import static groovy.json.JsonOutput.*;
              import hudson.ExtensionList;

              ExtensionList<UpdateSiteWarningsConfiguration> configurations = ExtensionList.lookup(UpdateSiteWarningsConfiguration.class);
              println configurations;
              
              UpdateSiteWarningsConfiguration configuration = configurations.get(0);
              HashSet<UpdateSite.Warning> activeWarnings = new HashSet<>();
              
              activeWarnings.add('SECURITY-248');
              
              configuration.ignoredWarnings = activeWarnings;
              
              configuration.save(); 
  • Uso de Jobdsl: Con el plugin de Jcasc también lo podemos combinar con el plugin de Jobdsl que nos permite definirnos jobs cuando arranque nuestro Jenkins y así tener ya creado todos nuestros principales jobs.
      init-jobs: |
            jobs:
              - script: >
                  folder('Terraform')
              - script: >
                  multibranchPipelineJob('Terraform/azure-test') {
                      branchSources {
                          branchSource {
                              source {
                                  git {
                                      remote('https://github.com/lucasberlang/Terraform-azure-test.git')
                                  }
                              }
                              strategy {
                                  defaultBranchPropertyStrategy {
                                      props {
                                          noTriggerBranchProperty()
                                      }
                                  }
                              }
                          }
                      }
                      configure {
                          it / sources / data / 'jenkins.branch.BranchSource' / source / traits {
                            'jenkins.plugins.git.traits.BranchDiscoveryTrait'()
                          }
                      }
                      triggers {
                          periodic(60)
                      }
                  } 

Configuración Prometheus

Para la configuración de Prometheus como hemos dicho anteriormente usaremos el provider de Helm. Para hacer el despliegue nos crearemos un namespace llamado monitoring donde se desplegará Prometheus. El fichero donde se hará esta configuración es helm_monitoring.tf.

resource "kubernetes_namespace" "jenkins" {
  metadata {
    name = "gitops"
  }
} 

Luego procederemos a hacer el despliegue de Prometheus con Helm.

data "template_file" "file" {
  template = "${file("${path.module}/templates/prometheus.yaml")}"
  vars = {
    slack_api_url = "${var.slack_api_url}"
    slack_channel = "${var.slack_channel}"
  }
}

resource "helm_release" "prometheus" {
  chart      = "prometheus"
  name       = "prometheus"
  namespace  = kubernetes_namespace.monitoring.metadata.0.name
  repository = "https://charts.helm.sh/stable"

  values = [data.template_file.file.rendered]
} 

Para ello haremos unas modificaciones en el template prometheus.yaml sustituyendo las variables slack_api_url y slack_channel dentro del fichero por las variables de entorno que le pasamos en el paso inicial. Estas variables serán usadas para el envío de alertas si existiera algún problema en el cluster de GKE o el algún pod desplegado en este.

De este fichero vamos a destacar algunos puntos que hemos modificado para realizar una automatización de nuestro servicio.

  • Configuración del servicio: Configuraremos el servicio de Prometheus como un ClusterIP para que solo sea visible dentro del cluster.
    type: ClusterIP 
  • Configuración de las alertas de Slack: Añadiremos la configuración de envío de alertas por si se cae algún nodo de la infraestructura o hay alguna caída de servicio como puede ser un fallo en el pod de Jenkins. Si sucede alguno de los dos puntos anteriores nos llegará una alerta a nuestro canal de Slack.
alertmanagerFiles:
  alertmanager.yml:
    global:
      slack_api_url: ${slack_api_url}

    receivers:
      - name: slack-notifications
        slack_configs:
          - channel: ${slack_channel}
            send_resolved: true
            icon_url: https://avatars3.githubusercontent.com/u/3380462
            title: |
              [{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }} for {{ .CommonLabels.job }}
              {{- if gt (len .CommonLabels) (len .GroupLabels) -}}
                {{" "}}(
                {{- with .CommonLabels.Remove .GroupLabels.Names }}
                  {{- range $index, $label := .SortedPairs -}}
                    {{ if $index }}, {{ end }}
                    {{- $label.Name }}="{{ $label.Value -}}"
                  {{- end }}
                {{- end -}}
                )
              {{- end }}
            text: |
              {{ range .Alerts -}}
              *Alert:* {{ .Annotations.title }}{{ if .Labels.severity }} - `{{ .Labels.severity }}`{{ end }}
          
              *Description:* {{ .Annotations.description }}
          
              *Details:*
                {{ range .Labels.SortedPairs }} • *{{ .Name }}:* `{{ .Value }}`
                {{ end }}
              {{ end }}

    route:
      group_wait: 10s
      group_interval: 5m
      receiver: slack-notifications
      repeat_interval: 3h 

Aquí es donde se hará la sustitución de nuestro webhook de Slack(slack_api_url) configurado en el paso inicial y también la sustitución del canal donde va a escribir las alertas(slack_channel).

  • Configuración de recolección de logs: por defecto Prometheus hace la recolección de todos los logs de kubernetes al nivel de infraestructura. La única modificación que haremos es que recopile los logs del pod de Jenkins (Este funciona ya que hemos instalado el plugin de prometheus en Jenkins) así recibiremos información valiosa de Jenkins que luego explotaremos con Grafana.
      - job_name: 'jenkins'
        metrics_path: /prometheus
        static_configs:
          - targets: [jenkins.gitops.svc.cluster.local:80] 

Configuración Grafana

Como último paso realizaremos la configuración de Grafana. Para esto como hemos dicho anteriormente usaremos el provider de Helm. Para hacer el despliegue usaremos el namespace creado anteriormente llamado monitoring donde se desplegará Grafana. El fichero donde se realizaremos el despliegue de Grafana es helm_monitoring.tf.

Para realizar el despliegue con helm usaremos el siguiente código:

data "local_file" "helm_chart_grafana" {
  filename = "${path.module}/templates/grafana.yaml"
}

resource "helm_release" "grafana" {
  chart      = "grafana"
  name       = "grafana"
  namespace  = kubernetes_namespace.monitoring.metadata.0.name
  repository = "https://charts.helm.sh/stable"

  values = [data.local_file.helm_chart_grafana.content]
} 

Todas las configuraciones iniciales de Grafana vienen modificadas en el template grafana.yaml.

Las configuraciones más importantes que hemos realizado en dicho archivo son las siguientes:

  • Configuración del servicio: el servicio de Grafana se levantará como LoadBalancer para que sea visible desde el exterior y lo podamos revisar desde nuestro navegador web.
service:
  type: LoadBalancer
  port: 80
  targetPort: 3000
    # targetPort: 4181 To be used with a proxy extraContainer
  annotations: {}
  labels: {}
  portName: service 
  • Configuración de credenciales: para loguearnos en Grafana haremos uso del secret de kubernetes grafana-secrets que hemos definido con usuario y contraseña.
admin:
  existingSecret: grafana-credentials
  userKey: adminUser
  passwordKey: adminPassword 
  • Configuración del datasource: como configuración inicial también añadiremos Prometheus como fuente de datos en Grafana. Para ello tendremos que añadir la url interna de prometheus, en nuestro caso será el nombre del servicio (prometheus-server) más el namespace donde esta desplegado (monitoring) y por último la resolución del DNS en GKE.
datasources:
 datasources.yaml:
   apiVersion: 1
   datasources:
   - name: Prometheus
     type: prometheus
     url: http://prometheus-server.monitoring.svc.cluster.local
     isDefault: true 
  • Configuración de los dashboards: añadiremos unos dashboards inciales cuando se levante el servicio de Grafana. Estos dashboards serán los siguientes:

    • Jenkins-Performance: Encargado de la monitorización del servicio de Jenkins a nivel de ejecución de jobs y de uso de recursos.

    • Kubernetes-nodes: Monitorización de la infraestructura de GKE.

    • kubernetes-cluster: Monitorización de la infraestructura de GKE.

dashboards:
  default:
    Jenkins-Performance:
      gnetId: 14764
      revision: 1
      datasource: Prometheus
    Kubernetes-nodes:
      gnetId: 315
      revision: 3
      datasource: Prometheus
    Kubernetes-cluster:
      gnetId: 6417
      revision: 1
      datasource: Prometheus 

Despliegue de la infraestructura

Una vez realizado el paso de configuración de variables pasaremos a realizar el despliegue de toda la infraestructura (GKE y VPC) y de todos los componentes que vamos a desplegar (Jenkins, Prometheus y Grafana).

Para ello al tenerlo todo configurado con Terraform solo debemos ejecutar los siguientes comandos dentro de nuestro proyecto:

cd src/
terraform init
terraform apply 

Una vez ejecutado los comandos anteriores la salida esperada de nuestro proyecto será la siguiente:

Revisión de la infraestructura

Una vez desplegado toda la infraestructura empezaremos a hacer la revisión de todo lo desplegado para ello nos conectaremos al cluster de GKE de la siguiente forma:

gcloud container clusters get-credentials $cluster_name --region $region --project $project_id 

Para ello se deberá sustituir las variables cluster_name por el nombre del cluster, region por la región donde se ha desplegado el cluster de GKE y la variable project_id haciendo referencia al proyecto donde se desplegará.

Una vez conectado al cluster de GKE haremos una revisión de los nodos desplegados:

kubectl get nodes 

Esto nos devolverá:

Así como pods y servicios que tenemos disponibles:Así como pods y servicios que tenemos disponibles:

kubectl get pods,svc -n gitops
kubectl get pods,svc -n monitoring 

Con la siguiente salida:

Revisión de Jenkins

Una vez hemos verificado nuestra infraestructura vamos a realizar la revisión de Jenkins.

Con la información sacada del servicio de jenkins accederemos a la interfaz web donde nos tendremos que loguear, en nuestro caso 34.79.219.11.

Para sacar la contraseña de nuestro usuario de Jenkins tendremos que ejecutar el siguiente comando:

JENKINS_PASSWORD=$(kubectl get secret jenkins -n gitops -o jsonpath="{.data.jenkins-admin-password}" | base64 --decode);echo
echo $JENKINS_PASSWORD 

Esto nos devolverá la contraseña y ya podremos hacer el login:

Como se puede observar una vez dentro de Jenkins tenemos la configuración que hemos realizado con Jcasc y la creación del job que hemos definido con JobDsl.

Revisión de Grafana

Para la revisión de Grafana debemos acceder también a la interfaz web y loguearnos. La dirección de la interfaz web la podemos observar con el comando de kubectl lanzado anteriormente en nuestro caso es 34.140.61.173.

Para loguearnos usaremos la información que pasamos por nuestras variables de entorno, TF_VAR_password_grafana y TF_VAR_user_grafana.

Podemos comprobar que la configuración que hemos añadido al template de creación de datasources y dashboards es correcta.

  • Datasources:
  • Dashboards:

Job de prueba

Para nuestro ejemplo usaremos un proyecto que desplegará un resource group en Azure, se encuentra en el siguiente repositorio.

Lo más destacable de este proyecto es la definición de nuestro pipeline de ejecución:

pipeline {
  agent {
      label "terraform-exec"
  }
 stages {
  stage('checkout') {
   steps {
    container('terraform') {
     echo "La rama de la que se va a hacer el checkout es: master"
     git branch: "master", url: 'https://github.com/lucasberlang/terraform-azure-test.git'
    }
   }
  }
   stage('Review Terraform version') {
    steps {
    container('terraform') {
      sh 'terraform --version'
    }
    }
   }
   stage('Terraform init') {
    steps {
    container('terraform') {
      sh 'terraform init -upgrade'
    }
   }
   }
  stage('Terraform plan infrastructure') {
    steps {
    container('terraform') {
     withCredentials([string(credentialsId: 'vaultUrl', variable: 'VAULT_ADDR'),
     string(credentialsId: 'vaultToken', variable: 'VAULT_TOKEN')
     ]) {
      sh 'export VAULT_ADDR=${VAULT_ADDR} && export VAULT_TOKEN=${VAULT_TOKEN} && terraform plan'
     slackSend channel: "#practica-cloud-deployments",color: '#BADA55', message: "Plan completed! Do you approve deployment? ${env.RUN_DISPLAY_URL}"
     }
    }
   }
  }
  stage('Approval') {
   when{
    not {
     branch 'poc'
    }
   }
   steps {
    container('terraform') {
     script {
     def userInput = input(id: 'confirm', message: 'Apply Terraform?', parameters: [ [$class: 'BooleanParameterDefinition', defaultValue: false, description: 'Apply terraform', name: 'confirm'] ])
     }
    }
   }
  }
  stage('Terraform Apply') {
    steps {
    container('terraform') {
     withCredentials([string(credentialsId: 'vaultUrl', variable: 'VAULT_ADDR'),
     string(credentialsId: 'vaultToken', variable: 'VAULT_TOKEN')
     ]) {
      sh 'export VAULT_ADDR=${VAULT_ADDR} && export VAULT_TOKEN=${VAULT_TOKEN} && terraform apply -auto-approve'
     slackSend color: '#BADA55', message: "Apply completed! Build logs from jenkins server ${env.RUN_DISPLAY_URL}"
     }
    }
    }
  }
  stage('Waiting to review the infrastructure') {
    steps {
    container('terraform') {
     slackSend channel: "#practica-cloud-deployments", color: '#BADA55', message: "Waiting 5 minutes before destroy the infrastructure!"
     sh 'sleep 300'
    }
   }
    
  }
  stage('Destroy Infra') {
    steps {
    container('terraform') {
     withCredentials([string(credentialsId: 'vaultUrl', variable: 'VAULT_ADDR'),
     string(credentialsId: 'vaultToken', variable: 'VAULT_TOKEN')
     ]) {
      sh 'export VAULT_ADDR=${VAULT_ADDR} && export VAULT_TOKEN=${VAULT_TOKEN} && terraform destroy -auto-approve'
     slackSend channel: "#practica-cloud-deployments", color: '#BADA55', message: "Destroy completed! Build logs from jenkins server ${env.RUN_DISPLAY_URL}"
     }
    }
    }
  }
 }
} 

En el pipeline se han definido las siguientes stages:

  • Checkout: se hará el checkoput de nuestro proyecto.
  • Review Terraform Version: revisaremos la versión de terraform desplegada en nuestro contenedor agente.
  • Terraform init: stage de inicialización de entorno.
  • Terraform plan infrastructure: revisión de la infraestructura a desplegar en nuestro caso un resource group de Azure.
  • Approval: paso de aprobación manual para comprobar si la infraestructura a desplegar es correcta.
  • Terraform Apply: ejecución y creación de toda la infraestructura que vamos a realizar en Azure.
  • Waiting to review the infrastructure: solo como para la demo haremos una espera de 5 minutos para revisar toda nuestra infraestructura.
  • Destroy Infra: destrucción de toda la infraestructura una vez esperado estos 5 minutos.

Ejecución end to end

Una vez realizado todas las comprobaciones pertinentes vamos a ejecutar el job de prueba en Jenkins para ver como escala nuestra plataforma así como el envío de alertas y la monitorización que realizaremos en Grafana.

Accederemos a nuestro job de prueba y lo ejecutaremos, dándole al botón construir ahora:

Una vez que el job esta lanzado podemos comprobar cómo se instancia un nuevo agente de Jenkins para nuestra ejecución teniendo así una infraestructura totalmente escalable:

watch -n 1 kubectl get pods -n gitops 

Cuando el job este ejecutando podemos observar cómo nos van a ir llegando alertas automáticamente al Slack. La primera alerta llegará en el stage de approval donde nos pedirá Jenkins que revisemos la infraestructura que vamos a desplegar y si es correcta aprobaremos el cambio:

Y una vez que entremos en el link de slack nos saltará directamente la interfaz de blueocean:

Una vez aprobado el cambio se realizará el despliegue en nuestra cuenta de Azure, donde podemos revisar que esta todo correcto y el resource group, en nuestro caso example-test, se ha desplegado correctamente:

Por último podemos observar todas las alertas que hemos recibido en nuestro canal de Slack las cuales nos avisaran en el stage de approval, waiting y destroy de la infra:

Escalabilidad de la solución

Una vez que hemos comprobado que nuestra solución está totalmente automatizada y que funciona correctamente vamos a comprobar la escalabilidad de nuestra plataforma.

Para ello lo que vamos a hacer es lanzar múltiples ejecuciones de nuestro job de Jenkins para ver cómo va desplegando la solución tanto a nivel de agentes (Levantará un pod por ejecución) como la escalabilidad de nuestros nodos cuando tengamos más peticiones y consumo de memoria y CPU.

El primer paso será ejecutar múltiples jobs desde la interfaz web:

Como se puede observar nuestros pods irán escalando según el número de ejecuciones que tenemos:

Para ver como escala a nivel de máquinas usaremos el siguiente comando:

watch -n 1 kubectl get nodes 

Como se puede observar se ha agregado un nuevo nodo hace 23 segundos por lo que nuestra plataforma es capaz de ir escalando según las peticiones que se realicen desde nuestro Jenkins.

Monitorización de la plataforma

Dado que hemos realizado tanto la configuración de Grafana como la de Prometheus vamos a hacer uso de nuestros dashboards predefinidos para monitorizar la plataforma en Grafana y el envío de alertas con Prometheus.

Dashboards

Estos dashboards que hemos creado son los siguientes:

  • Jenkins: Performance and Health Overview: dashboard con información relativa a nuestras ejecuciones en Jenkins, número de jobs, memoria utilizada… .
  • Kubernetes Cluster: en este dahsboard tendremos información del cluster de kubernetes a nivel de pods corriendo en el cluster memoria consumida por pod etc.
  • Kubernetes Cluster: en el último dahsboard se mostrará gráficos de picos de red, CPUs y memoria a nivel de máquinas en el Cluster de GKE.

Alertas del sistema

Además de alertas cuando se ejecuta un job también se hemos realizado la configuración de alertas de la infraestructura vía Prometheus. Este nos enviará alertas cuando el pod de Jenkins este caído o alguno de los nodos del cluster se caiga.

Para hacerlo un poco más ilustrativo usaremos el ejemplo de autoescalado que hemos realizado antes para que Prometheus nos envíe una alerta cuando empiece a quitarse máquinas porque el cluster de GKE detecte que no se está consumiendo tanta memoria.

Cuando hicimos la prueba de autoescalado vimos como se había agregado un nuevo nodo llamado gke-go-euw2-bk-sca-g-default-node-poo-c9c04d95-sgmz. Este después del pico de ejecuciones de jobs en Jenkins se auto eliminará ya que no tendremos tanta actividad en nuestro GKE.

Prometheus al detectar que nuestro nodo ha desaparecido nos enviará una alerta de nodo caído automáticamente a Slack.

Así sabremos de una forma totalmente automatizada si nuestra infraestructura ha sufrido algún problema.

Para el caso de los pods lo que vamos a provocar es una caída del componente de Jenkins para que así Prometheus nos envíe una alerta de pod caído.

Para ello lo que vamos a realizar es la destrucción del pod de Jenkins para provocar una caída del servicio:

kubectl delete pod jenkins-0 -n gitops 

Una vez que el pod este eliminado se arrancará automáticamente:

Y pasado un minuto Prometheus nos avisará de la caída del servicio de Jenkins y de su posterior recuperación:

Eliminación de recursos

El último paso que haremos tras probar el éxito de nuestra plataforma CI/CD será realizar la destrucción de todos sus componentes para ello ejecutaremos el siguiente comando:

cd src/
terraform destroy 

Conclusión

Es de sobra conocido la importancia de automatizar el ciclo de vida del desarrollo software pero en este artículo le hemos querido dar una vuelta más a esta automatización consiguiendo industrializar toda nuestra plataforma CI/CD. Para ello hemos automatizado tanto el levantamiento de toda nuestra infraestructura con Terraform así como la configuración inicial de nuestras tres piezas centrales Jenkins, Prometheus y Grafana.

Haciendo uso de plugin como Jcasc conseguimos automatizar toda la configuración inicial de Jenkins que muchas veces es bastante tediosa de hacer de arranque. Esto nos permite que si alguna vez hay una caída de nuestro servicio siempre mantendremos una configuración base en este, evitando tiempos de configuración que al final se reduce en tiempo de espera para el usuario.

Además, añadiendo una monitorización inicial tanto con Prometheus como con Grafana somos capaces de crear un sistema de avisos que nos ayude a comprobar y chequear el correcto comportamiento de toda nuestra plataforma para así poder evitar futuras incidencias.

Por último el uso de GKE en este tipo de plataformas consiguen un ahorro de costes bastante importante en nuestra organización ya que nos da la posibilidad de tener un autoescalado dependiendo del uso que se esté realizando sobre nuestra plataforma, pudiendo así tener picos de gran carga sin tener un deterioro del servicio.

Navegación

Introducción

Objetivos

Introducción a Terraform

Introducción a GKE

Introducción a Jenkins

Introducción a Prometheus

Introducción a Grafana

Introducción a Slack

Preparación de entorno

Clonación de repositorio

Índice de ficheros

Configuración de Slack

Configuración de variables

Configuración VPC

Configuración GKE

Configuración Jenkins

Configuración Prometheus

Configuración Grafana

Despliegue de la infraestructura

Job de prueba

Ejecución end to end

Escalabilidad de la solución

Monitorización de la plataforma

Eliminación de recursos

Conclusión

Autor

¿Quieres saber más de lo que ofrecemos y ver otros casos de éxito?
DESCUBRE BLUETAB

Lucas Calvo Berlanga

Cloud Engineer

SOLUCIONES, SOMOS EXPERTOS

DATA STRATEGY
DATA FABRIC
AUGMENTED ANALYTICS

Te puede interesar

Bluetab en la ElixirConfEU 2023

mayo 3, 2023
LEER MÁS

DataOps

octubre 24, 2023
LEER MÁS

Hashicorp Boundary

diciembre 3, 2020
LEER MÁS

Algunas de las capacidades de Matillion ETL en Google Cloud

julio 11, 2022
LEER MÁS

Gobierno del Dato: Una mirada en la realidad y el futuro

mayo 18, 2022
LEER MÁS

Detección de Fraude Bancario con aprendizaje automático

septiembre 17, 2020
LEER MÁS

Publicado en: Blog, Practices, Tech

Workshop Ingeniería del caos sobre Kubernetes con Litmus

julio 7, 2021 by Bluetab

Workshop Ingeniería del caos sobre Kubernetes con Litmus

Ángel Maroco

AWS Cloud Architect

LitmusChaos nace con el objetivo de ayudar a desarrolladores y SREs (Site Reliability Engineering ) de Kubernetes a identificar puntos débiles y mejorar la resiliencia de sus aplicaciones/plataformas proporcionando un marco de trabajo completo.

Sus principales ventajas respecto a otras herramientas son:

  • Experimentos declarativos mediante K8S CRDs (Custom Resource Definition): todos los componentes (planificación, ejecución, parametrización, etc.) de un experimento se definen dentro del ámbito de kubernetes haciendo uso de YAML.
  • Múltiples experimentos predefinidos: dispone de un conjunto de experimentos suficientemente amplio para dar cobertura a los principales recursos de K8s.
  • SDK en Go/Python/Ansible para desarrollar tus propios experimentos: dispone de un metodología de desarrollo bien definida para construir experimentos que se adapten a tus necesidades particulares.
  • Creación de workflows a través de GUI: con Litmus UI Portal puedes crear workflows complejos utilizando todos los experimentos predefinidos mediante interfaz web.
  • Fácil integración en pipelines CI/CD: invocar y obtener el resultado de un experimento es extremadamente fácil.
  • Exportación de métricas: puedes exportar distintas métricas de tus experimentos directamente a Prometheus.

El producto está liberado bajo licencia Apache-2.0, dispone de una amplia comunidad de desarrolladores y desde 2020 pertenece a Cloud Native Computing Foundation.

Objetivos del workshop

  1. Conocer los principales componentes de un experimento y realizar su despliegue
  2. Analizar detalladamente la ejecución de tres experimentos (criterios de entrada, hipótesis, observaciones y resultados)
  3. Ver las múltiples opciones referentes a planificación de experimentos.
  4. Visualizar los resultados mediante Prometheus/Grafana.
  5. Analizar un caso de pruebas de resiliencia + test de rendimiento con JMeter.
  6. Principales funcionalidades de Litmus UI Portal

Preparación de consola

Recomendamos abrir una consola y crear 4 paneles:

  1. Panel principal (ejecutaremos todo el contenido del workshop)
  2. Monitorización de la aplicación de test
  3. Monitorización de pods
  4. Monitorización de eventos

Clonación de repositorio

git clone https://github.com/angelmaroco/litmus-chaos-engineering-workshop.git
cd litmus-chaos-engineering-workshop 

Creación de entorno de pruebas K8s con minikube

Para este workshop vamos a utilizar minikube pero Litmus puede ser desplegado en cualquier servicio gestionado tipo EKS/AKS/GKE.

Minikube requiere de un gestor de contenedores o máquinas virtuales (Docker, Hyperkit, Hyper-V, KVM, Parallels, Podman, VirtualBox, or VMWare).

Recomendamos hacer uso de docker. En el caso de no estar disponible en el sistema, puedes realizar la instalación con los siguientes comandos:

if ! [ -x "$(command -v docker)" ]; then
    curl -fsSL https://get.docker.com -o /tmp/get-docker.sh
    sh /tmp/get-docker.sh
fi 
# install kubectl
curl -Ls "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" --output /tmp/kubectl
sudo install /tmp/kubectl /usr/local/bin/kubectl
kubectl version --client

# install minikube
curl -Ls https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 --output /tmp/minikube-linux-amd64
sudo install /tmp/minikube-linux-amd64 /usr/local/bin/minikube
minikube version

# starting minikube
minikube start --cpus 2 --memory 4096

# enabled ingress & metrics servers
minikube addons enable ingress
minikube addons enable metrics-server

# enabled tunnel & dashboard
minikube tunnel > /dev/null &
minikube dashboard > /dev/null &
 

Creación de namespaces K8s

# create namespace testing
kubectl apply -f src/base/testing-ns.yaml

# create namespace litmus
kubectl apply -f src/base/litmus-ns.yaml

# create namespace monitoring (prometheus + grafana)
kubectl apply -f src/base/monitoring-ns.yaml

TESTING_NAMESPACE="testing"
LITMUS_NAMESPACE="litmus"
MONITORING_NAMESPACE="monitoring"
 

Despliegue de aplicación de test

Desplegamos una aplicación de test para poder ejecutar los experimentos de litmus.

  • nginx-deployment.yaml: creación de despliegue «app-sample», con recursos de cpu/memoria «limits»/»request» y configuración de «readinessProbe». Exponemos el servicio en el puerto 80 a través de un balanceador.
  • nginx-hpa.yaml: creación de Horizontal Pod Autoscaler (min 2 réplicas / max 10 réplicas)
# deployment
kubectl apply -f src/nginx/nginx-deployment.yaml --namespace="${TESTING_NAMESPACE}"

# enable hpa
kubectl apply -f src/nginx/nginx-hpa.yaml --namespace="${TESTING_NAMESPACE}"

# expose service 
kubectl expose deployment app-sample --type=LoadBalancer --port=80  -n "${TESTING_NAMESPACE}"

# wait deployment
kubectl wait --for=condition=available --timeout=60s deployment/app-sample -n "${TESTING_NAMESPACE}"

# get pods
kubectl get pods -n "${TESTING_NAMESPACE}"

#-----------------------------------------

NAME                          READY   STATUS    RESTARTS   AGE
app-sample-7ff489dbd5-82ppw   1/1     Running   0          45m
app-sample-7ff489dbd5-jg9vh   1/1     Running   0          45m

# get service
kubectl get services -n "${TESTING_NAMESPACE}"

# -----------------------------------------

NAME         TYPE           CLUSTER-IP       EXTERNAL-IP      PORT(S)        AGE
app-sample   LoadBalancer   10.109.196.239   10.109.196.239   80:30020/TCP   3m54s

 

En PANEL 2 ejecutar:

TESTING_NAMESPACE='testing'
URL_SERVICE=$(minikube service app-sample --url -n "${TESTING_NAMESPACE}")
while true; do sleep 5; curl --connect-timeout 2 -s -o /dev/null -w "Response code %{http_code}"  ${URL_SERVICE}; echo -e ' - '$(date);done

 

En PANEL 3 ejecutar:

TESTING_NAMESPACE='testing'
watch -n 1 kubectl get pods -n "${TESTING_NAMESPACE}"
 

En PANEL 4 ejecutar:

kubectl get events -A -w 

Despliegue Chaos Experiments

# litmus operator & experiments
kubectl apply -f https://litmuschaos.github.io/litmus/litmus-operator-v1.13.0.yaml -n "${LITMUS_NAMESPACE}"

kubectl apply -f https://hub.litmuschaos.io/api/chaos/1.13.0\?file\=charts/generic/experiments.yaml -n "${TESTING_NAMESPACE}"
 
kubectl get chaosexperiments -n "${TESTING_NAMESPACE}"

# ----------------------------------------------------

NAME                      AGE
container-kill            6s
disk-fill                 6s
disk-loss                 6s
docker-service-kill       6s
k8-pod-delete             6s
k8-service-kill           6s
kubelet-service-kill      6s
node-cpu-hog              6s
node-drain                6s
node-io-stress            6s
node-memory-hog           6s
node-poweroff             6s
node-restart              6s
node-taint                6s
pod-autoscaler            6s
pod-cpu-hog               6s
pod-delete                6s
pod-io-stress             6s
pod-memory-hog            6s
pod-network-corruption    6s
pod-network-duplication   6s
pod-network-latency       6s
pod-network-loss          6s
 

Despliegue servicios monitorización: Prometheus + Grafana

Litmus permite exportar las métricas de los experimentos a Prometheus a través de chaos-exporter.

kubectl -n ${MONITORING_NAMESPACE} apply -f src/litmus/monitoring/utils/prometheus/prometheus-operator/

kubectl -n ${MONITORING_NAMESPACE} apply -f src/litmus/monitoring/utils/metrics-exporters-with-service-monitors/kube-state-metrics/

kubectl -n ${MONITORING_NAMESPACE} apply -f src/litmus/monitoring/utils/alert-manager-with-service-monitor/

kubectl -n ${LITMUS_NAMESPACE} apply -f src/litmus/monitoring/utils/metrics-exporters-with-service-monitors/litmus-metrics/chaos-exporter/

kubectl -n ${MONITORING_NAMESPACE} apply -f src/litmus/monitoring/utils/prometheus/prometheus-configuration/

kubectl -n ${MONITORING_NAMESPACE} apply -f src/litmus/monitoring/utils/grafana/

kubectl -n ${MONITORING_NAMESPACE} apply -f src/litmus/monitoring/utils/metrics-exporters-with-service-monitors/node-exporter/

# wait deployment
kubectl wait --for=condition=available --timeout=60s deployment/grafana -n ${MONITORING_NAMESPACE}
kubectl wait --for=condition=available --timeout=60s deployment/prometheus-operator -n ${MONITORING_NAMESPACE}

echo "Acceso dashboard --> $(minikube service grafana -n ${MONITORING_NAMESPACE} --url)/d/nodepodmetrics/node-and-pod-chaos-metrics?orgId=1&refresh=5s"
 

Para este workshop hemos personalizado un dashboard de grafana donde visualizaremos:

  • Timelime de experimentos ejecutados
  • 4 gráficas tipo «Gauge» con número de total de experimentos, estado Pass, estado Fail y estado Awaited.
  • Consumo de CPU nivel nodo
  • Consumo de CPU a nivel POD (app-sample)
  • Consumo de memoria nivel nodo
  • Consumo de memoria a nivel POD (app-sample)
  • Tráfico red (IN/OUT) nivel nodo
  • Tráfico red (IN/OUT) nivel POD (app-sample)


Datos acceso grafana:

  • usuario: admin
  • password: admin

Creación de anotación "litmuschaos"

Para habilitar la ejecución de experimentos contra nuestro deployment, necesitamos añadir la anotación litmuschaos.io/chaos=“true“. Como veremos más adelante, todos los experimentos tienen la propiedad annotationCheck: “true”.

# add annotate (enable chaos)
kubectl annotate deploy/app-sample litmuschaos.io/chaos="true" -n "${TESTING_NAMESPACE}"
 
kubectl describe deploy/app-sample -n "${TESTING_NAMESPACE}"

# -----------------------------------------------------------

Name:                   app-sample
Namespace:              testing
CreationTimestamp:      Mon, 29 Mar 2021 09:35:53 +0200
Labels:                 app=app-sample
                        app.kubernetes.io/name=app-sample
Annotations:            deployment.kubernetes.io/revision: 1
                        litmuschaos.io/chaos: true # <-- HABILITAMOS EXPERIMENTOS
Selector:               app.kubernetes.io/name=app-sample
Replicas:               2 desired | 2 updated | 2 total | 2 available | 0 unavailable
StrategyType:           RollingUpdate
 

Detalle componentes de un experimento

Service Account, Role y RoleBinding

Cada experimento debe tener asociado un ServiceAccount, un Role para definir permisos y un RoleBinding para relacionar el ServiceAccount/Role.

Podéis encontrar todas las definiciones dentro de src/litmus/nombre-experimento/nombre-experimento-sa.yaml

apiVersion: v1
kind: ServiceAccount
metadata:
  name: container-kill-sa
  namespace: testing
  labels:
    name: container-kill-sa
    app.kubernetes.io/part-of: litmus
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: container-kill-sa
  namespace: testing
  labels:
    name: container-kill-sa
    app.kubernetes.io/part-of: litmus
rules:
  - apiGroups: [""]
    resources:
      ["pods", "pods/exec", "pods/log", "events", "replicationcontrollers"]
    verbs:
      ["create", "list", "get", "patch", "update", "delete", "deletecollection"]
  - apiGroups: ["batch"]
    resources: ["jobs"]
    verbs: ["create", "list", "get", "delete", "deletecollection"]
  - apiGroups: ["apps"]
    resources: ["deployments", "statefulsets", "daemonsets", "replicasets"]
    verbs: ["list", "get"]
  - apiGroups: ["apps.openshift.io"]
    resources: ["deploymentconfigs"]
    verbs: ["list", "get"]
  - apiGroups: ["argoproj.io"]
    resources: ["rollouts"]
    verbs: ["list", "get"]
  - apiGroups: ["litmuschaos.io"]
    resources: ["chaosengines", "chaosexperiments", "chaosresults"]
    verbs: ["create", "list", "get", "patch", "update"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: container-kill-sa
  namespace: testing
  labels:
    name: container-kill-sa
    app.kubernetes.io/part-of: litmus
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: container-kill-sa
subjects:
  - kind: ServiceAccount
    name: container-kill-sa
    namespace: testing 

Definición ChaosEngine

Para facilitar la comprensión, hemos dividido en 3 secciones el contenido de un experimento. Podéis encontrar todas las definiciones dentro de src/litmus/nombre-experimento/chaos-engine-.yaml

Especificaciones generales

En esta sección especificaremos atributos comunes a todos los experimentos. Para este workshop, debido a que estamos realizando los experimentos contra un único deployment, el único atributo que cambiará entre experimentos es «chaosServiceAccount».

apiVersion: litmuschaos.io/v1alpha1
kind: ChaosEngine
metadata:
  name: app-sample-chaos # Nombre del chaos-engine
  namespace: testing     # Namespace de testing
spec:
  annotationCheck: "true" # Hemos creado una anotación en nuestro deployment app-sample. Con la propiedad marcada a "true" indicamos que aplicarmeos el experimento a este despliegue.

  engineState: "active"   # Activación/desactivación de experimento

  appinfo:                # En esta sección proporcionamos la información de nuestro deployment.
    appns: "testing"      # Namespace donde se localiza
    applabel: "app.kubernetes.io/name=app-sample" # Etiqueta asociada a nuestro deployment
    appkind: "deployment" # Tipo de recurso (sólo admite deployment, lo que afectará a todos los pods)

  chaosServiceAccount: container-kill-sa # Nombre del service account (creado en el paso anterior)
  monitoring: true       # si queremos activar la monitorización (prometheus o similares)
  jobCleanUpPolicy: "delete" # Permite controlar la limpieza de recursos tras la ejecución. Especificar "retain" para debug.
 
Especificaciones de componentes

En esta sección definiremos las variables de entorno propias de cada experimento. Las variables «CHAOS_INTERVAL» y «TOTAL_CHAOS_DURATION» son comunes a todos los experimentos.

  experiments:
    - name: container-kill # Nombre del experimento
      spec:
        components:
          env:
            # Intervalo (segundos) por cada iteración
            - name: CHAOS_INTERVAL
              value: "10"

            # Tiempo total (segundos) que durará el experimento
            - name: TOTAL_CHAOS_DURATION
              value: "60"
 
Especificaciones de pruebas

En esta sección se informan los atributos para las pruebas de validación. El resultado del experimento dependerá del cumplimiento de la validación especificada.

En el siguiente enlace podeis consultar los tipos de pruebas disponibles.

        probe:
          - name: "check-frontend-access-url" # Nombre de prueba
            type: "httpProbe"                 # Petición de tipo HTTP(S). Alternativas: cmdProbe, k8sProbe, promProbe.
            httpProbe/inputs:                  
              url: "http://app-sample.testing.svc.cluster.local" # URL a validar
              insecureSkipVerify: false                               # Permitir HTTP sin TLS
              method:
                get:                          # Petición tipo GET
                  criteria: ==                # Criterio a evaluar
                  responseCode: "200"         # Respuesta a evaluar
            mode: "Continuous"                # La prueba se ejecuta de forma continua (alternativas: SoT, EoT, Edge, OnChaos)
            runProperties:
              probeTimeout: 5                 # Número de segundos para timeout en la petición
              interval: 5                     # Intervalo (segundos) entre re-intentos
              retry: 1                        # Número de re-intento antes de dar por fallida la validación   
              probePollingInterval: 2         # Intervalo (segundos) entre peticiones

 
Gestión de experimentos

Una de las principales ventajas de litmus es poder definir los experimentos de forma declarativa, lo que nos permite incluir fácilmente nuestros gestores de plantillas. Recomendamos el uso de kustomize.

Ejecución de experimentos

Container Kill

  • Descripción: Aborta la ejecución del servicio docker dentro de un pod. La selección del pod es aleatoria.

  • Información oficial del experimento: enlace

  • Criterio de entrada: 2 pods de app-sample en estado «Running»

      kubectl get pods -n "${TESTING_NAMESPACE}"
  kubectl get pods -n "${TESTING_NAMESPACE}"

  # -----------------------------------------

  NAME                          READY   STATUS    RESTARTS   AGE
  app-sample-7ff489dbd5-82ppw   1/1     Running   0          9h
  app-sample-7ff489dbd5-jg9vh   1/1     Running   0          9h
 
  • Parámetros de entrada experimento:
experiments:
    - name: container-kill
    spec:
        components:
        env:
            # provide the chaos interval
            - name: CHAOS_INTERVAL
            value: "10"

            # provide the total chaos duration
            - name: TOTAL_CHAOS_DURATION
            value: "20"

            - name: CONTAINER_RUNTIME
            value: "docker"

            - name: SOCKET_PATH
            value: "/var/run/docker.sock"
 
  • Hipótesis: Tenemos dos pods escuchando por el 80 tras un balanceador. Nuestro deployment tiene readinessProbe con periodSeconds=1 y failureThreshold=1. Si uno de los pods deja de responder, el balanceador deja de enviar tráfico a ese pod y debe responder el otro. Hemos establecido el healthcheck del experimento cada 5s (tiempo máximo de respuesta aceptable) atacando directamente contra el balanceador, por lo que no deberíamos de tener pérdida de servicio en ningún momento.

  • Creación de SA, Role y RoleBinding

kubectl apply -f src/litmus/kill-container/kill-container-sa.yaml -n "${TESTING_NAMESPACE}"
 
  • Ejecución de experimento

kubectl apply -f src/litmus/kill-container/chaos-engine-kill-container.yaml  -n "${TESTING_NAMESPACE}"

# Awaited -> Pass/Fail
watch -n 1 kubectl get chaosresult app-sample-chaos-container-kill -n "${TESTING_NAMESPACE}" -o jsonpath="{.status.experimentstatus.verdict}"
 
  • Observaciones: durante el experimento observamos 2 reinicios de pod con transición «Running» -> «Error» -> «Running».

  • Validación: Peticiones get al balanceador con respuesta 200.

probe:
- name: "check-frontend-access-url"
    type: "httpProbe"
    httpProbe/inputs:
    url: "http://app-sample.testing.svc.cluster.local"
    insecureSkipVerify: false
    method:
        get:
        criteria: ==
        responseCode: "200"
    mode: "Continuous"
    runProperties:
    probeTimeout: 5
    interval: 5
    retry: 1
    probePollingInterval: 2
 
  • Resultado: resultado «Pass» (dos pods en estado «Running», sin pérdida de servicio durante la duración del experimento)
$ kubectl describe chaosresult app-sample-chaos-container-kill -n "${TESTING_NAMESPACE}" 

# --------------------------------------------------------------------------------------

Spec:
    Engine:      app-sample-chaos
    Experiment:  container-kill
Status:
    Experimentstatus:
        Fail Step:                 N/A
        Phase:                     Completed
        Probe Success Percentage:  100
        Verdict:                   Pass
History:
    Failed Runs:   0
    Passed Runs:   6
    Stopped Runs:  0
Probe Status:
    Name:  check-frontend-access-url
    Status:
        Continuous:  Passed 👍
    Type:            httpProbe
Events:
    Type    Reason   Age    From                         Message
    ----    ------   ----   ----                         -------
    Normal  Awaited  4m48s  container-kill-5i56m6-4pkxg  experiment: container-kill, Result: Awaited
    Normal  Pass     4m4s   container-kill-5i56m6-4pkxg  experiment: container-kill, Result: Pass


$ kubectl get pods -n testing

NAME                          READY   STATUS    RESTARTS   AGE
app-sample-6c48f8c4cc-74lvl   1/1     Running   2          25m
app-sample-6c48f8c4cc-msdmj   1/1     Running   0          25m
 
  • Pod autoscaler

    • Descripción: permite escalar las réplicas para testear el autoescalado en el nodo.

    • Información oficial del experimento: enlace

    • Criterio de entrada: 2 pods de app-sample en estado «Running»

  $ kubectl get pods -n "${TESTING_NAMESPACE}"

  # ------------------------------------------

  NAME                          READY   STATUS    RESTARTS   AGE
  app-sample-6c48f8c4cc-74lvl   1/1     Running   2          29m
  app-sample-6c48f8c4cc-msdmj   1/1     Running   0          28m


 
  • Parámetros de entrada experimento:

experiments:
  - name: pod-autoscaler
    spec:
      components:
        env:
          # set chaos duration (in sec) as desired
          - name: TOTAL_CHAOS_DURATION
            value: "60"

          # number of replicas to scale
          - name: REPLICA_COUNT
            value: "10"
 
  • Hipótesis: Disponemos de un HPA con min = 2 y max = 10. Con la ejecución de este experimento queremos validar que nuestro nodo es capaz de escalar a 10 réplicas (el max. establecido en el HPA). Cuando ejecutemos el experimento, se crearán 10 réplicas y en ningún momento tendremos pérdida de servicio. Nuestro HPA tiene establecido el parámetro «–horizontal-pod-autoscaler-downscale-stabilization» a 300s, por lo que durante ese intervalo tendremos 10 réplicas en estado «Running» y transcurrido ese intervalo, volveremos a tener 2 réplicas.

  • Creación de SA, Role y RoleBinding

$ kubectl apply -f src/litmus/pod-autoscaler/pod-autoscaler-sa.yaml -n "${TESTING_NAMESPACE}"
 
  • Ejecución de experimento
$ kubectl apply -f src/litmus/pod-autoscaler/chaos-engine-pod-autoscaler.yaml  -n "${TESTING_NAMESPACE}"
 
  • Observaciones:

  • Validación: Peticiones get al balanceador con respuesta 200.

probe:
- name: "check-frontend-access-url"
    type: "httpProbe"
    httpProbe/inputs:
    url: "http://app-sample.testing.svc.cluster.local"
    insecureSkipVerify: false
    method:
        get:
        criteria: ==
        responseCode: "200"
    mode: "Continuous"
    runProperties:
    probeTimeout: 5
    interval: 5
    retry: 1
    probePollingInterval: 2
 
  • Resultado:
$ kubectl describe chaosresult app-sample-chaos-pod-autoscaler  -n "${TESTING_NAMESPACE}"

# ----------------------------------------------------------------------------------------

Spec:
    Engine:      app-sample-chaos
    Experiment:  pod-autoscaler
Status:
    Experimentstatus:
        Fail Step:                 N/A
        Phase:                     Completed
        Probe Success Percentage:  100
        Verdict:                   Pass
History:
    Failed Runs:   0
    Passed Runs:   6
    Stopped Runs:  0
Probe Status:
    Name:  check-frontend-access-url
    Status:
        Continuous:  Passed 👍
    Type:            httpProbe
Events:
    Type    Reason   Age    From                         Message
    ----    ------   ----   ----                         -------
    Normal  Awaited  4m46s  pod-autoscaler-95wa6x-858jv  experiment: pod-autoscaler, Result: Awaited
    Normal  Pass     3m32s  pod-autoscaler-95wa6x-858jv  experiment: pod-autoscaler, Result: Pass

$ kubectl get pods -n testing

# ---------------------------

NAME                          READY   STATUS        RESTARTS   AGE
app-sample-6c48f8c4cc-5kzpg   0/1     Completed     0          39s
app-sample-6c48f8c4cc-74lvl   0/1     Running       2          32m
app-sample-6c48f8c4cc-bflws   0/1     Completed     0          39s
app-sample-6c48f8c4cc-c5ls8   0/1     Completed     0          39s
app-sample-6c48f8c4cc-d9zj4   0/1     Completed     0          39s
app-sample-6c48f8c4cc-f2xnt   0/1     Completed     0          39s
app-sample-6c48f8c4cc-f7qdl   0/1     Completed     0          39s
app-sample-6c48f8c4cc-ff84v   0/1     Completed     0          39s
app-sample-6c48f8c4cc-k29rr   0/1     Completed     0          39s
app-sample-6c48f8c4cc-l5fqp   0/1     Completed     0          39s
app-sample-6c48f8c4cc-m587t   0/1     Completed     0          39s
app-sample-6c48f8c4cc-msdmj   1/1     Running       0          32m
app-sample-6c48f8c4cc-n5h6l   0/1     Completed     0          39s
app-sample-6c48f8c4cc-qr5nd   0/1     Completed     0          39s
app-sample-chaos-runner       0/1     Completed     0          47s
pod-autoscaler-95wa6x-858jv   0/1     Completed     0          45s
 

Pod CPU Hog

  • Descripción: permite consumir recursos de CPU dentro de POD

  • Información oficial del experimento: enlace

  • Criterio de entrada: 2 pods de app-sample en estado «Running»

  kubectl get pods -n "${TESTING_NAMESPACE}"

  # ---------------------------------------

  NAME                          READY   STATUS    RESTARTS   AGE
  app-sample-6c48f8c4cc-74lvl   1/1     Running   2          52m
  app-sample-6c48f8c4cc-msdmj   1/1     Running   0          52m

 
  • Parámetros de entrada experimento:
experiments:
  - name: pod-cpu-hog
    spec:
      components:
        env:
          #number of cpu cores to be consumed
          #verify the resources the app has been launched with
          - name: CPU_CORES
            value: "1"

          - name: TOTAL_CHAOS_DURATION
            value: "60" # in seconds

          - name: PODS_AFFECTED_PERC
            value: "0"
 
  • Hipótesis: Disponemos de un HPA con min = 2 y max = 10. Con la ejecución de este experimento queremos validar que nuestro HPA funciona correctamente. Tenemos establecido un targetCPUUtilizationPercentage=50%, lo que quiere decir que si inyectamos consumo de CPU en un pod, el HPA debe establecer el número de réplicas a 3 (2 min + 1 autoscaler). En ningún momento debemos tener pérdida de servicio. Nuestro HPA tiene establecido el parámetro «–horizontal-pod-autoscaler-downscale-stabilization» a 300s, por lo que durante ese intervalo tendremos 10 réplicas en estado «Running» y transcurrido ese intervalo, volveremos a tener 2 réplicas.

  • Creación de SA, Role y RoleBinding

kubectl apply -f src/litmus/pod-cpu-hog/pod-cpu-hog-sa.yaml -n "${TESTING_NAMESPACE}"
 
  • Ejecución de experimento

kubectl apply -f src/litmus/pod-cpu-hog/chaos-engine-pod-cpu-hog.yaml -n "${TESTING_NAMESPACE}"
 
  • Observaciones: durante el experimento vemos 2 pods en estado «Runnning». Se comienza a inyectar consumo en uno de los POD y se autoescala a 3 réplicas. A los 300s se vuelve a tener 2 réplicas.

  • Validación: Peticiones get al balanceador con respuesta 200.

probe:
- name: "check-frontend-access-url"
    type: "httpProbe"
    httpProbe/inputs:
    url: "http://app-sample.testing.svc.cluster.local"
    insecureSkipVerify: false
    method:
        get:
        criteria: ==
        responseCode: "200"
    mode: "Continuous"
    runProperties:
    probeTimeout: 5
    interval: 5
    retry: 1
    probePollingInterval: 2
 
  • Resultado: resultado «Pass» (tres pods en estado «Running», sin pérdida de servicio durante la duración del experimento)

$ kubectl describe chaosresult app-sample-chaos-pod-cpu-hog -n "${TESTING_NAMESPACE}" 

# -----------------------------------------------------------------------------------

Spec:
    Engine:      app-sample-chaos
    Experiment:  pod-cpu-hog
Status:
    Experimentstatus:
        Fail Step:                 N/A
        Phase:                     Completed
        Probe Success Percentage:  100
        Verdict:                   Pass
History:
    Failed Runs:   0
    Passed Runs:   6
    Stopped Runs:  0
Probe Status:
    Name:  check-frontend-access-url
    Status:
        Continuous:  Passed 👍
    Type:            httpProbe
Events:
    Type    Reason   Age    From                         Message
    ----    ------   ----   ----                         -------
    Normal  Awaited  2m23s  pod-cpu-hog-mpen59-zcpr6  experiment: pod-cpu-hog, Result: Awaited
    Normal  Pass     74s    pod-cpu-hog-mpen59-zcpr6  experiment: pod-cpu-hog, Result: Pass

$ kubectl get pods -n testing

  NAME                          READY   STATUS      RESTARTS   AGE
  app-sample-6c48f8c4cc-74lvl   1/1     Running     6          46m
  app-sample-6c48f8c4cc-msdmj   1/1     Running     0          46m
  app-sample-5c5575cdb7-hq5gs   1/1     Running     0          49s
  app-sample-chaos-runner       0/1     Completed   0          104s
  pod-cpu-hog-mpen59-zcpr6      0/1     Completed   0          103s
 

Extra – Otros experimentos

  • pod-network-loss

kubectl apply -f src/litmus/pod-network-loss/pod-network-loss-sa.yaml -n "${TESTING_NAMESPACE}"

kubectl apply -f src/litmus/pod-network-loss/chaos-engine-pod-network-loss.yaml  -n "${TESTING_NAMESPACE}"

kubectl describe chaosresult app-sample-chaos-pod-network-loss -n "${TESTING_NAMESPACE}"
 
  • pod-memory-hog
kubectl apply -f src/litmus/pod-memory/pod-memory-hog-sa.yaml -n "${TESTING_NAMESPACE}"

kubectl apply -f src/litmus/pod-memory/chaos-engine-pod-memory-hog.yaml  -n "${TESTING_NAMESPACE}"

kubectl describe chaosresult app-sample-chaos-pod-memory-hog -n "${TESTING_NAMESPACE}" 
 
  • pod-delete
kubectl apply -f src/litmus/pod-delete/pod-delete-sa.yaml -n "${TESTING_NAMESPACE}"

kubectl apply -f src/litmus/pod-delete/chaos-engine-pod-delete.yaml -n "${TESTING_NAMESPACE}"

kubectl describe chaosresult app-sample-chaos-pod-delete -n "${TESTING_NAMESPACE}" 
 

Planificación de experimentos

Litmus soporta el uso de planificaciones de experimentos. Dispone de las siguientes opciones:

  • Inmediato
apiVersion: litmuschaos.io/v1alpha1
kind: ChaosSchedule
metadata:
  name: schedule-nginx
spec:
  schedule:
    now: true
  engineTemplateSpec:
    appinfo:
      appns: testing
      applabel: app.kubernetes.io/name=app-sample
      appkind: deployment
    annotationCheck: 'true'
 
  • Timestamp específico
apiVersion: litmuschaos.io/v1alpha1
kind: ChaosSchedule
metadata:
  name: schedule-nginx
spec:
  schedule:
    once:
      #should be modified according to current UTC Time
      executionTime: "2020-05-12T05:47:00Z" 
  engineTemplateSpec:
    appinfo:
      appns: testing
      applabel: app.kubernetes.io/name=app-sample
      appkind: deployment
    annotationCheck: 'true'
 
  • Repeticiones
apiVersion: litmuschaos.io/v1alpha1
kind: ChaosSchedule
metadata:
  name: schedule-nginx
spec:
  schedule:
    repeat:
      properties:
         #format should be like "10m" or "2h" accordingly for minutes or hours
        minChaosInterval: "2m"  
  engineTemplateSpec:
    appinfo:
      appns: testing
      applabel: app.kubernetes.io/name=app-sample
      appkind: deployment
    annotationCheck: 'true'
 
  • Repeticiones entre un rango de fechas
apiVersion: litmuschaos.io/v1alpha1
kind: ChaosSchedule
metadata:
  name: schedule-nginx
spec:
  schedule:
    repeat:
      timeRange:
        #should be modified according to current UTC Time
        startTime: "2020-05-12T05:47:00Z"   
        endTime: "2020-09-13T02:58:00Z"   
      properties:
        #format should be like "10m" or "2h" accordingly for minutes and hours
        minChaosInterval: "2m"  
  engineTemplateSpec:
    appinfo:
      appns: testing
      applabel: app.kubernetes.io/name=app-sample
      appkind: deployment
    annotationCheck: 'true'
 
  • Repeticiones con una fecha de finalización
apiVersion: litmuschaos.io/v1alpha1
kind: ChaosSchedule
metadata:
  name: schedule-nginx
spec:
  schedule:
    repeat:
      timeRange:
        #should be modified according to current UTC Time
        endTime: "2020-09-13T02:58:00Z"   
      properties:
        #format should be like "10m" or "2h" accordingly for minutes and hours
        minChaosInterval: "2m"   
  engineTemplateSpec:
    appinfo:
      appns: testing
      applabel: app.kubernetes.io/name=app-sample
      appkind: deployment
    annotationCheck: 'true'
 
  • Repeticiones desde una fecha de inicio (ejecuciones indefinidas)
apiVersion: litmuschaos.io/v1alpha1
kind: ChaosSchedule
metadata:
  name: schedule-nginx
spec:
  schedule:
    repeat:
      timeRange:
        #should be modified according to current UTC Time
        startTime: "2020-05-12T05:47:00Z"   
      properties:
         #format should be like "10m" or "2h" accordingly for minutes and hours
        minChaosInterval: "2m" 
  engineTemplateSpec:
    appinfo:
      appns: testing
      applabel: app.kubernetes.io/name=app-sample
      appkind: deployment
    annotationCheck: 'true'
 
  • Ejecución entre horas con frecuencia
apiVersion: litmuschaos.io/v1alpha1
kind: ChaosSchedule
metadata:
  name: schedule-nginx
spec:
  schedule:
    repeat:
      properties:
        #format should be like "10m" or "2h" accordingly for minutes and hours
        minChaosInterval: "2m"   
      workHours:
        # format should be <starting-hour-number>-<ending-hour-number>(inclusive)
        includedHours: 0-12
  engineTemplateSpec:
    appinfo:
      appns: testing
      applabel: app.kubernetes.io/name=app-sample
      appkind: deployment
    annotationCheck: 'true'
 
  • Ejecuciones periódicas en días específicos
apiVersion: litmuschaos.io/v1alpha1
kind: ChaosSchedule
metadata:
  name: schedule-nginx
spec:
  schedule:
    repeat:
      properties:
        #format should be like "10m" or "2h" accordingly for minutes and hours
        minChaosInterval: "2m"   
      workDays:
        includedDays: "Mon,Tue,Wed,Sat,Sun"
  engineTemplateSpec:
    appinfo:
      appns: testing
      applabel: app.kubernetes.io/name=app-sample
      appkind: deployment
    annotationCheck: 'true'
 

LitmusChaos + Load Test Performance con Apache Jmeter

Hasta el momento hemos realizado pruebas para validar cómo se comporta nuestro nodo de k8s bajo escenarios ideales, sin carga en el sistema por parte de los usuarios finales de la aplicación.

Por lo general, tendremos definidos SLIs/SLOs/SLAs los cuales hay que garantizar que cumplimos bajo cualquier eventualidad y para ello debemos de disponer de las herramientas adecuadas. En este caso, Litmus + Apache Jmeter nos facilitarán la tarea de simular múltiples escenarios de concurrencia con inyección de anomalías en el sistema. Durante esta fase de pruebas es posible que tengamos que realizar ajustes de dimensionamiento, modificar alguna política de escalado o incluso que identifiquemos cuellos de botella y los equipos de desarrollo tengan que ajustar algún componente.

Para no desvirtuar el objetivo del workshop con la definición de SLIs/SLOs/SLAs (más info aquí), únicamente vamos a utilizar la métrica «Ratio de error», la cual vamos a establecer en < 2,00%.

Planteamos un escenario ficticio donde nuestra aplicación tiene 200 usuarios concurrentes durante la mayor parte del tiempo de servicio.

Procedemos a descargar el binario de JMeter y unos complementos para la visualización de gráficas:

JMeter requiere Java JRE. En el caso de no estar disponible en el sistema, puedes realizar la instalación de [OpenJDK](https://adoptopenjdk.net/index.html). En caso contrario, omite este paso.

curl -L https://ftp.cixug.es/apache//jmeter/binaries/apache-jmeter-5.4.1.tgz --output /tmp/apache-jmeter.tgz
tar zxvf /tmp/apache-jmeter.tgz && mv apache-jmeter-5.4.1 apache-jmeter

# install plugins-manager
curl -L https://jmeter-plugins.org/get/ --output apache-jmeter/lib/ext/jmeter-plugins-manager-1.6.jar

# install bzm - Concurrency Thread Group
curl -L https://repo1.maven.org/maven2/kg/apc/jmeter-plugins-casutg/2.9/jmeter-plugins-casutg-2.9.jar --output apache-jmeter/lib/ext/jmeter-plugins-casutg-2.9.jar
curl -L https://repo1.maven.org/maven2/kg/apc/jmeter-plugins-cmn-jmeter/0.6/jmeter-plugins-cmn-jmeter-0.6.jar --output apache-jmeter/lib/jmeter-plugins-cmn-jmeter-0.6.jar
curl -L https://repo1.maven.org/maven2/kg/apc/cmdrunner/2.2/cmdrunner-2.2.jar --output apache-jmeter/lib/cmdrunner-2.2.jar
curl -L https://repo1.maven.org/maven2/net/sf/json-lib/json-lib/2.4/json-lib-2.4.jar --output apache-jmeter/lib/json-lib-2.4-jdk15.jar


curl -L https://repo1.maven.org/maven2/kg/apc/jmeter-plugins-graphs-basic/2.0/jmeter-plugins-graphs-basic-2.0.jar --output apache-jmeter/lib/ext/jmeter-plugins-graphs-basic-2.0.jar
curl -L https://repo1.maven.org/maven2/kg/apc/jmeter-plugins-graphs-additional/2.0/jmeter-plugins-graphs-additional-2.0.jar --output apache-jmeter/lib/ext/jmeter-plugins-graphs-additional-2.0.jar

# Get url service
url=$(minikube service app-sample --url -n "${TESTING_NAMESPACE}")

HOST_APP_SAMPLE=$(echo ${url} | cut -d/ -f3 | cut -d: -f1)
PORT_APP_SAMPLE=$(echo ${url} | cut -d: -f3)
 

Vamos a validar que con el dimensionamiento actual cumplimos con los requisitos. Durante 60 segundos, ejecutamos 200 peticiones concurrentes, lo que se traduce en 12.000 peticiones. La petición será de tipo «GET» por el puerto 80 del balanceador.

Este es el aspecto que tiene la GUI de JMeter con el plan de pruebas.

TARGET_RATE=200
RAMP_UP_TIME=60
RAMP_UP_STEPS=1

# GUI mode
bash apache-jmeter/bin/jmeter.sh -t src/jmeter/litmus-k8s-workshop.jmx -f -l apache-jmeter/logs/result.jtl -j apache-jmeter/logs/jmeter.log -Jhost=${HOST_APP_SAMPLE} -Jport=${PORT_APP_SAMPLE} -Jtarget_rate=${TARGET_RATE} -Jramp_up_time=${RAMP_UP_TIME} -Jramp_up_steps=${RAMP_UP_STEPS}
 

Nuestro dimensionamiento base son dos réplicas de nuestro servicio app-sample:

kubectl get pods -n "${TESTING_NAMESPACE}"

# ----------------------------------------

NAME                         READY   STATUS    RESTARTS   AGE
app-sample-d9d578cfb-55flr   1/1     Running   8          3h1m
app-sample-d9d578cfb-klmxn   1/1     Running   0          3h2m
 

Ejecutamos el plan de pruebas sin GUI:

TARGET_RATE=200
RAMP_UP_TIME=60
RAMP_UP_STEPS=1

bash apache-jmeter/bin/jmeter.sh -n -t src/jmeter/litmus-k8s-workshop.jmx -f -l apache-jmeter/logs/result.jtl -j apache-jmeter/logs/jmeter.log -Jhost=${HOST_APP_SAMPLE} -Jport=${PORT_APP_SAMPLE} -Jtarget_rate=${TARGET_RATE} -Jramp_up_time=${RAMP_UP_TIME} -Jramp_up_steps=${RAMP_UP_STEPS}

rm -rf apache-jmeter/logs/report && bash apache-jmeter/bin/jmeter.sh -g apache-jmeter/logs/result.jtl -o apache-jmeter/logs/report
 

En la ruta «./apache-jmeter/logs/report/index.html» podéis ver un dashboard con los resultados.

Hemos realizado 12000 peticiones con 200 usuarios concurrentes durante 60s. Estos son los resultados:

  • Ratio de error: 0.00%

Vamos a realizar la misma prueba pero inyectando disrupcción de red en uno de los pods, lo que provocará que deje de responder (estado CrashLoopBackOff) y sólo tengamos disponible una réplica.

kubectl apply -f src/litmus/pod-network-loss/pod-network-loss-sa.yaml -n "${TESTING_NAMESPACE}"
kubectl apply -f src/litmus/pod-network-loss/chaos-engine-pod-network-loss.yaml  -n "${TESTING_NAMESPACE}"

TARGET_RATE=200
RAMP_UP_TIME=60
RAMP_UP_STEPS=1

bash apache-jmeter/bin/jmeter.sh -n -t src/jmeter/litmus-k8s-workshop.jmx -f -l apache-jmeter/logs/result.jtl -j apache-jmeter/logs/jmeter.log -Jhost=${HOST_APP_SAMPLE} -Jport=${PORT_APP_SAMPLE} -Jtarget_rate=${TARGET_RATE} -Jramp_up_time=${RAMP_UP_TIME} -Jramp_up_steps=${RAMP_UP_STEPS}

rm -rf apache-jmeter/logs/report && bash apache-jmeter/bin/jmeter.sh -g apache-jmeter/logs/result.jtl -o apache-jmeter/logs/report
 

¿Qué ha sucedido?

Al inyectar el experimento, uno de los pods ha dejado de responder. Si nos fijamos en la definición del deployment app-sample, tenemos un livenessProbe cuya propiedad periodSeconds está establecida a 5 segundos y failureThreshold a 1 intento. Según nuestra configuración, el balanceador envía el 50% aprox. del tráfico a cada uno de los pods. Durante 5 segundos tenemos que el pod al que hemos inyectado una disrupción de red mediante el experimento no responde, lo que se traduce en error en la petición. Transcurridos los 5 segundos, el balanceador deja de enviar tráfico a ese pod y sólo tendremos un pod recibiendo peticiones.

Teníamos establecido un requisito que nuestro servicio no puede superar el 2% de errores bajo ningún escenario y hemos obtenido un 5,03% (603 peticiones erróneas), por lo que debemos realizar algún ajuste para cumplir el objetivo.

¿Cuál es el resultado del experimento?

kubectl describe chaosresult app-sample-chaos-pod-network-loss  -n "${TESTING_NAMESPACE}"

#-------------------------

Events:
Type    Reason   Age    From                           Message
----    ------   ----   ----                           -------
Normal  Awaited  4m16s  pod-network-loss-uf6hms-sk47z  experiment: pod-network-loss, Result: Awaited
Normal  Pass     2m23s  pod-network-loss-uf6hms-sk47z  experiment: pod-network-loss, Result: Pass
 

Aunque nuestro requisito de ratio de error < 2,00% no se cumple, el experimento termina con resultado «Pass». Esto es debido a que Litmus tiene como criterio de salida «Pass» si el pod vuelve a estar disponible, lo cual se cumple. Aquí estamos haciendo uso de litmus para inyectar errores en el sistema.

¿Cómo podemos conseguir reducir el ratio de error?

Únicamente con fines ilustrativos, para resolver el problema que nos ocupa, vamos a incrementar el número de réplicas a 4 en el HorizontalPodAutoscaler y en el deployment disminuir el valor de la propiedad periodSeconds de 5s a 2s. Con esto pasamos a distribuir el 25% del tráfico a cada pod y además, el tiempo que el pod afectado por la disrupción de tráfico pasa de 5s a 2s, lo que debe traducirse en una reducción del ratio de error.

ℹ️ Nuestro sistema debe estar diseñado para adaptarse a la demanda en base a métricas (CPU, memoria, peticiones por segundo, latencia, I/O, etc.) siempre manteniendo los mínimos recursos activos. Con la expansión de servicios gestionados de kubernetes en los principales proveedores cloud (EKS/GKE/AKS), disponemos de múltiples estrategias para conseguir dicho objetivo.

kubectl edit deployment app-sample -n "${TESTING_NAMESPACE}" 

kubectl edit HorizontalPodAutoscaler app-sample-ha -n "${TESTING_NAMESPACE}" 
 

Volvemos a ejecutar nuestro test:

kubectl apply -f src/litmus/pod-network-loss/pod-network-loss-sa.yaml -n "${TESTING_NAMESPACE}"
kubectl apply -f src/litmus/pod-network-loss/chaos-engine-pod-network-loss.yaml  -n "${TESTING_NAMESPACE}"

TARGET_RATE=200
RAMP_UP_TIME=60
RAMP_UP_STEPS=1

bash apache-jmeter/bin/jmeter.sh -n -t src/jmeter/litmus-k8s-workshop.jmx -f -l apache-jmeter/logs/result.jtl -j apache-jmeter/logs/jmeter.log -Jhost=${HOST_APP_SAMPLE} -Jport=${PORT_APP_SAMPLE} -Jtarget_rate=${TARGET_RATE} -Jramp_up_time=${RAMP_UP_TIME} -Jramp_up_steps=${RAMP_UP_STEPS}

rm -rf apache-jmeter/logs/report && bash apache-jmeter/bin/jmeter.sh -g apache-jmeter/logs/result.jtl -o apache-jmeter/logs/report
 

Como podemos observar, nuestros cambios han provocado disminuir nuestro ratio de error a 1,60%, por lo que conseguimos cumplir nuestro objetivo de < 2,00%.

Litmus UI Portal

Litmus dispone de un portal para poder realizar experimentos sin necesidad de utilizar la consola. Dispone de las siguientes funcionalidades:

  • Gestión de workflows: dispone de todos los experimentos pre-cargados listos para ejecutar en tu k8s.
  • MyHubs: permite conectar a repositorios públicos/privados para hacer uso de tus propios experimentos.
  • Analytics: permite visualizar las ejecuciones de tus experimentos, así como estadísticas sobre los mismos. Además, permite conectar a otros DataSources como Prometheus.
  • Gestión de equipos y usuarios.
# install litmus portal
kubectl apply -f src/litmus/portal/portal.yaml

minikube service litmusportal-frontend-service -n  ${LITMUS_NAMESPACE} > /dev/null &
 

Guía Litmus para desarrolladores

En la actualidad, litmus dispone de 53 experimentos a través de Litmus ChaosHub. Están desarrollados principalmente en Go, aunque disponen de una SDK para python y ansible.

Los experimentos tienen una estructura bien definida (pre-checks, chaos-injection, litmus-probes, post-checks y result-updates) y es viable desarrollar experimentos que se ajusten a tus necesidades.

En este enlace encontraréis toda la información para desarrolladores.

Consideraciones finales

Debemos asumir que nuestro sistema no va a ser 100% tolerante a fallos pero ello no implica que pongamos todos los medios para minimizar los riesgos y en caso de producirse el desastre, lo hagamos de una forma relativamente controlada. La clave del éxito pasa por aplicar las prácticas de ingeniería del caos en fases tempranas del desarrollo, conocer las particularidades de la infraestructura donde ejecuta y disponer de herramientas adecuadas para automatizar las pruebas.

Un factor importante es dimensionar los esfuerzos en base a la criticidad del servicio que presta nuestro sistema: el esfuerzo en validar la resiliencia de un portal con información para empleados con 100 usuarios potenciales cuyo SLA es del 98% difiere mucho de una aplicación bancaria que realiza operaciones financieras a miles de usuarios concurrentes cuyo SLA es del 99.9XX%. En ambos casos el único método para verificar el cumplimiento del SLA es mediante test de resiliencia pero existe una notable diferencia respecto al esfuerzo que deberíamos dedicar.

En este workshop nos hemos centrado en Litmus y Kubernetes pero cabe recordar que dependiendo del sistema que estemos desarrollando, tengamos que complementar nuestras pruebas con otras herramientas, principalmente las enfocadas a la inyección de fallos sobre infraestructura (+ info).

Referencias

  • Litmus official web
  • Litmus GitHub
  • Principles of Chaos Engineering
  • Chaos Engineering: the history, principles and practice
  • Awesome Chaos Engineering
  • SRE Fundamentals – Google

Licencia

Este workshop está licenciado bajo MIT (ver LICENSE para más detalle).

Navegación

Introducción

Objetivos del workshop

Preparación de consola

Clonación de repositorio

Creación de entorno de pruebas K8s con minikube

Creación de namespaces K8s

Despliegue de aplicación de test

Despliegue Chaos Experiments

Despliegue servicios monitorización: Prometheus + Grafana

Creación de anotación «litmuschaos»

Detalle componentes de un experimento

Ejecución de experimentos

Planificación de experimentos

LitmusChaos + Load Test Performance con Apache Jmeter

Litmus UI Portal

Guía Litmus para desarrolladores

Consideraciones finales

Referencias

Licencia

Autor

¿Quieres saber más de lo que ofrecemos y ver otros casos de éxito?
DESCUBRE BLUETAB
Ángel Maroco
AWS Cloud Architect

Ángel Maroco llevo en el sector IT más de una década, iniciando mi carrera profesional con el desarrollo web, pasando una buena etapa en distintas plataformas informacionales en entornos bancarios y los últimos 5 años dedicado al diseño de soluciones en entornos AWS.

En la actualidad, compagino mi papel de arquitecto junto al de responsable de la Pŕactica Cloud /bluetab, cuya misión es impulsar la cultura Cloud dentro de la compañía.

SOLUCIONES, SOMOS EXPERTOS

DATA STRATEGY
DATA FABRIC
AUGMENTED ANALYTICS

Te puede interesar

¿Cómo pueden las empresas asegurarse de que sus datos estén estructurados, escalables y disponibles cuando se necesiten?

septiembre 13, 2024
LEER MÁS

Usando los Grandes Modelos de Lenguaje en información privada

marzo 11, 2024
LEER MÁS

Potencia Tu Negocio con GenAI y GCP: Simple y para Todos

marzo 27, 2024
LEER MÁS

Cómo preparar la certificación AWS Data Analytics – Specialty

noviembre 17, 2021
LEER MÁS

¿Cuánto vale tu cliente?

octubre 1, 2020
LEER MÁS

Databricks sobre Azure – Una perspectiva de Arquitectura (parte 1)

febrero 15, 2022
LEER MÁS

Publicado en: Blog, Practices, Tech

5 errores comunes en Redshift

diciembre 15, 2020 by Bluetab

5 errores comunes en Redshift

Alvaro Santos

Senior Cloud Solution Architect​

Amazon Redshift se puede considerar como unos de los data warehouse más importantes de la actualidad y que ofrece AWS en su nube. Trabajando en Bluetab, hemos tenido el placer de usarlo en muchas ocasiones con nuestros momentos buenos / malos al igual que este año 2020. Por ello, hemos creado una lista con los errores más comunes que debéis evitar y que esperemos os sirvan de gran ayuda.

En Bluetab llevamos desde hace más de 10 años trabajando alrededor del dato. En muchos de los cuales, hemos ayudado en la evolución tecnológica de muchas empresas migrando sus entornos tradicionales analíticos y BI de Data Warehouse a entornos de Big Data.

Además desde la Práctica Cloud hemos participado en migraciones a la nube y nuevos desarrollos de proyectos de Big Data la nube de Amazon Web Services y Google Cloud. Toda esta experiencia nos ha permitido crear un grupo de personas muy cualificadas que piensan/trabajan por/para la nube.

Para ayudaros con vuestros trabajos en la nube, os queremos presentar los errores más comunes que hemos encontrado a la hora de trabajar con Redhisft, la herramienta de DW más importante que ofrece AWS.

Aquí tenéis la lista de ellos:

  1. Trabajar como si fuera un PostgreSQL.
  2. Cargar datos de aquella manera.
  3. Dimensionar mal el cluster.
  4. No hacer uso de workload management (WLM).
  5. Desentenderse del mantenimiento

Qué es Redshift

Amazon Redshift es un base de datos analítica (OLAP) en la nube muy rápida y totalmente administrada por AWS. Con ella se simplifica y mejora el análisis de datos utilizando SQL estándar compatible con la mayoría de las herramientas de BI existentes.

Las características más importantes de Amazon Redshift son:

  • Almacenamiento de datos en columnas: en lugar de almacenar datos como una serie de filas, Amazon Redshift organiza los datos por columna. Dado que solo se procesan las columnas involucradas en las consultas y los datos en columnas se almacenan secuencialmente en los medios de almacenamiento, los sistemas basados ​​en columnas requieren muchas menos I/O, lo que mejora enormemente el rendimiento de las consultas.
  • Compresión avanzada: las bases de datos columnares se pueden comprimir mucho más que las basados ​​en filas porque los datos similares se almacenan secuencialmente en el disco.
  • Procesamiento masivo paralelo (MPP): Amazon Redshift distribuye automáticamente la carga de datos y consultas en todos los nodos.
  • Redshift Spectrum: Redshift Spectrum le permite ejecutar consultas en exabytes de datos almacenados en Amazon S3.
  • Vistas materializadas: las consultas posteriores que hacen referencia a las vistas materializadas utilizan los resultados pre.calculados para ejecutarse mucho más rápido. Las vistas materializadas se pueden crear en base a una o más tablas de origen utilizando filtros, proyecciones, combinaciones internas, agregaciones, agrupaciones, funciones y otras construcciones SQL.
  • Escalabilidad: Redshift tiene la capacidad de escalar su procesamiento y almacenamiento aumentado el tamaño de cluster a cientos de nodos.

Amazon Redshift no es igual que otros sistemas SQL de base de datos. Para aprovechar adecuadamente todos sus beneficios es necesario que se sigan una buenas practicas, de esa manera el cluster funcionará de manera óptima.

1. Trabajar como si fuera un PostgreSQL

Un error muy común que cometemos al comenzar a usar Redshift, es suponer que Redshift es simplemente un PostgreSQL vitaminado y que partiendo de un schema compatible con él puedes empezar a trabajar con Redshift. Sin embargo, no podrías estar más equivocado.

Aunque es cierto que Redshift se basó en una versión antigua de PostgreSQL 8.0.2, su arquitectura ha cambiado radicalmente y ha sido optimizada durante años para mejorar el redimiendo para su estrictamente analítico. Por ellos es necesario:
  • Diseñar las tablas de manera adecuada.
  • Lanzar consultas optimizadas para entornos MPP.

Diseñar las tablas de manera adecuada

Cuando se diseña la base de datos ten en cuenta que algunas decisiones clave sobre el diseño de las tablas influyen considerablemente en el rendimiento general de la consulta. Unas buenas practicas son:
  • Seleccionar el tipo de distribución de datos óptima:
    • Para las tablas de hechos (facts) elige el tipo DISTKEY. De esta manera los datos se distribuirán en los diferentes nodos agrupados por los valores de la clave elegida. Esto te permitirá realizar consultas de tipo JOIN sobre esa columna de manera muy eficiente.
    • Para las tablas de dimensiones (dimensions) con un pocos de millones de entradas elige el tipo ALL. Aquellas tablas que son comúnmente usadas en joins de tipo diccionario es recomendable que se copien a todos los nodos. De esta manera la sentencia JOIN realizada con tablas de hechos mucho más grandes se ejecutará mucho más rápido.
    • Cuando no tengas claro como vas a realizar la consulta sobre una tabla muy grande o simplemente no tenga ninguna relación con el resto, elige el tipo EVEN. De esta forma los datos se distribuirán de manera aleatoria.
  • Usa la compresión automática permitiendo a Redshift que seleccione el tipo más optimo para cada columna. Esto lo consigue realizando un escaneo sobre un número limitado de elementos.

Usar consultas optimizadas para entornos MPP

Puesto que Redshift es un entorno MPP distribuido, es necesario maximizar el rendimiento de las consultas siguiendo unas recomendaciones básicas. Unas buenas practicas son:
  • Las tablas tiene que diseñarse pensando en las consultas que se van a realizar. Por lo tanto, si una consulta no encaja es necesario que revises el diseño de las tablas que participan.
  • Evite usar SELECT *. e incluye solo las columnas que necesites.
  • No uses cross-joins a no ser que sea necesario.
  • Siempre que puedas, usa la sentencia WHERE para restringir la cantidad de datos a leer.
  • Use claves de ordenación en las cláusulas GROUP BY y SORT BY para que el planificador de consultas pueda usar una agregación más eficiente.

2. Cargar datos de aquella manera

Cargar conjuntos de datos muy grandes puede tomar mucho tiempo y consumir gran cantidad de recursos del cluster. Además si esta carga se realiza de manera inadecuada también puede afectar el rendimiento de las consultas.

Por ello, es recomendable seguir estas pautas:

  • Usa siempre el comando COPY para cargar los datos en paralelo desde Amazon S3, Amazon EMR, Amazon DynamoDB o desde distintos orígenes de datos en hosts remotos.

 copy customer from 's3://mybucket/mydata' iam_role 'arn:aws:iam::12345678901:role/MyRedshiftRole'; 
  • Si es posible, lanza un solo comando en vez de varios. Puedes usar un fichero manifest o patrones para cargar varios ficheros de una sola vez.

  • Divide los archivos de datos de carga de tal modo que sean:

    • De igual tamaño, entre 1 MB y 1 GB, después de la compresión.
    • Un múltiplo del número de slices de tu cluster.
  • Para actualizar los datos e insertar datos nuevos de manera eficiente al cargarlos usa una tabla provisional.

  -- Crea una tabla provisional y, luego, complétala con los datos que se fusionarán.
  create temp table stage (like target); 

  insert into stage 
  select * from source 
  where source.filter = 'filter_expression';

  -- Usa una combinación interna con la tabla provisional para eliminar las filas de la tabla destino que se están actualizando.
  begin transaction;

  delete from target 
  using stage 
  where target.primarykey = stage.primarykey; 

  -- Inserta todas las filas de la tabla provisional.
  drop table stage;
  insert into target 
  select * from stage;

  end transaction;

  -- Elimina la tabla provisional.
  drop table stage; 

3. Dimensionar mal el cluster

A lo largo de los años hemos visto muchos clientes que tenían graves problemas de rendimiento con Redshift debido a fallos de diseño de sus BBDD. Muchos de ellos habían intentado resolverlos añadiendo más recursos al cluster en vez de intentar solucionar el problema de raíz.

Por ellos te propongo que sigas el siguiente flujo para dimensionar tu cluster:

  • Recolecta información sobre el tipo de consultas a realizar, tamaño de los datos, concurrencia esperada, etc.

  • Diseña tus tablas en base a las consultas que se vayan a realizar.

  • Dependiendo del tipo de consultas (sencillas, largas, complejas…), selecciona el tipo de instancia de Redshift (DC2, DS2 o RA3).

  • Teniendo en cuenta el tamaño del dataset, calcula el número nodos de tu cluster.

# of  Redshift nodes = (uncompressed data size) * 1.25 / (storage capacity of selected Redshift node type)  

« Para el cálculo del tamaño de almacenamiento, se recomienda tener además un margen mayor para realizar tareas de mantenimiento. »

  • Realizar pruebas de carga para comprobar el rendimiento.

  • En el caso de no funcionar adecuadamente, optimiza las queries modificando el diseño de las tablas incluso si fuera necesario.

  • Finalmente, si no fuera suficiente, itera hasta encontrar el dimensionamiento adecuado de nodos y tamaños.

4. No hacer uso de workload management (WLM)

Es bastante probable que vuestro caso de uso necesite que existan varias sesiones o usuarios que estén ejecutando consultas al mismo tiempo. En estos casos, algunas consultas pueden consumir recursos del clúster durante periodos de tiempo prolongados y afectar al rendimiento de las otras consultas. En esta situación, es posible que las consultas sencillas tendrán que esperar hasta que se complete las consultas más largas.

Mediante el uso de WLM, vamos a poder administrar la prioridad y capacidad de los diferentes tipos de ejecuciones creando diferente colas de ejecución.

Es posible configurar la WLM de Amazon Redshift para su ejecución de dos maneras diferentes:

  • Automatic WLM: la manera más recomendada es habilitar Amazon Redshift para que administre cómo se dividen los recursos para ejecutar consultas simultáneas con WLM automático. El usuario gestiona la prioridad de las colas y Amazon Redshift determina cuántas consultas se ejecutan simultáneamente y cuánta memoria se asigna a cada consulta enviada.
  • Manual WLM: alternativamente, se puede configurar de manera manual el uso de recursos de diferente colas. En tiempo de ejecución, se pueden enviar consultas a diferentes colas con diferentes parámetros de concurrencia y memoria gestionados por el usuario.


Cómo funciona WLM

Cuando un usuario ejecuta una consulta, WLM asigna la consulta a la primera cola coincidente, en función de las reglas de asignación de cola de WLM.

  • Si un usuario inició sesión como superusuario y ejecuta una consulta en el grupo de consultas con la etiqueta super usuario, la consulta se asigna a la cola superusuario.
  • Si un usuario pertenece a un grupo de usuarios de la lista o ejecuta una consulta dentro del grupo de consultas de la lista, la consulta se asigna a la primera cola coincidente.
  • Si una consulta no cumple con ningún criterio, la consulta se asigna a la cola predeterminada, que es la última cola definida en la configuración de WLM.

5. Desentenderse del mantenimiento

El mantenimiento de la base de datos es un término que usamos para describir un conjunto de tareas que se ejecutan con la intención de mejorar la base de datos. Existen rutinas destinadas a ayudar al rendimiento, liberar espacio en disco, verificar errores de datos, verificar fallos de hardware, actualizar estadísticas internas y muchas otras cosas oscuras (pero importantes).

En el caso de Redshift, se tiene la falsa sensación de que al ser un servicio totalmente administrado por Amazon no es necesario realizar ninguna. De esta manera creas el cluster y te olvidas de él. Aunque es cierto que AWS te facilita muchas tareas de administración (crear, parar, arrancar, destruir o realizar backups), esto no podría ser más erróneo.

Las tareas de mantenimiento más importantes que debes de llevar a cabo en Redshift son:

  • Motorización del sistema: es necesario que se monitorize el cluster 24/7 y realices revisiones periódicas para comprobar que el sistema funciona correctamente (sin consultas erróneas o bloqueos, espacio libre, tiempos de respuesta adecuados, etc). Además es necesario crear alarmas para poder anticiparse ante cualquier futura caída del servicio.
  • Compactación de las BBDD: Amazon Redshift no realiza todas las tareas de compactación en todas las situaciones automáticamente y otras veces vas a necesitar ejecutarlas de manera manual. Este proceso es denominado VACUUM y es necesario ejecutarlo manualmente para poder hacer uso de SORT KEYS de tipo INTERLEAVED. Este es un proceso bastante largo y costoso que va a tener que hacerlo a poder ser, en las ventanas de mantenimiento.
  • Integridad de los datos: como en toda carga de datos es necesario revisar que los procesos de ETL han funcionado adecuadamente. Redshift dispone de tablas de sistema como STV_LOAD_STATE en las que es posible encontrar información acerca del estado actual de las instrucciones COPY en curso. Debes de revisarlas a menudo para comprobar que no hay errores en la integridad de los datos.
  • Detección de consultas pesadas: Redshift monitoriza continuamente todas aquellas consultas que están tardando más de lo previsto y que podrían estar afectando negativamente el rendimiento del servicio. Para que puedas analizar e investigar esas consultas es posible encontrarlas en tablas de sistema como STL_ALERT_EVENT_LOG o a través de la misma consola web de AWS.
¿Quieres saber más de lo que ofrecemos y ver otros casos de éxito?
DESCUBRE BLUETAB
Álvaro Santos
Senior Cloud Solution Architect​

Mi nombre es Álvaro Santos y ejerzo como Solution Architect desde hace más de 5 años. Estoy certificado en AWS, GCP, Apache Spark y alguna que otras más. Entré a formar parte en Bluetab en octubre de 2018 y desde entonces estoy involucrado en proyectos cloud de Banca y Energía y además participo como Cloud Master Partitioner. Soy un apasionado de las nuevas patrones distribuidos, Big Data, Open-source software y cualquier otra cosa de mundo IT que mole.

SOLUCIONES, SOMOS EXPERTOS

DATA STRATEGY
DATA FABRIC
AUGMENTED ANALYTICS

Te puede interesar

Empoderando a las decisiones en diversos sectores con árboles de decisión en AWS

junio 4, 2024
LEER MÁS

Guía avanzada sobre almacenamiento en Snowflake

octubre 3, 2022
LEER MÁS

La gestión del cambio: El puente entre las ideas y el éxito

febrero 5, 2025
LEER MÁS

Detección de Fraude Bancario con aprendizaje automático II

septiembre 17, 2020
LEER MÁS

Una estrategia analítica eficiente

diciembre 13, 2022
LEER MÁS

Snowflake: Zero-Copy clone, o cómo librarte del duplicado de datos al clonar.

marzo 22, 2023
LEER MÁS

Publicado en: Blog, Practices, Tech

  • « Ir a la página anterior
  • Página 1
  • Página 2
  • Página 3
  • Página 4
  • Ir a la página siguiente »

Footer

LegalPrivacidadPolítica de cookies

Patrono

Patrocinador

© 2025 Bluetab Solutions Group, SL. All rights reserved.