Documentos de Académico
Documentos de Profesional
Documentos de Cultura
Stratospheric
Introducción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
¿Por qué Spring Boot & AWS? . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
¿Quién debería leer este libro? . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Prerrequisitos para los ejemplos prácticos . . . . . . . . . . . . . . . . . . . 5
¿Qué esperar de este libro? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
Poniéndonos en contacto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
Recursos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
Acerca de los Autores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
Amazon CloudWatch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
Amazon Cognito . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
Amazon DynamoDB . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
Amazon Elastic Compute Cloud (EC2) . . . . . . . . . . . . . . . . . . . . . . 37
Amazon Elastic Container Registry (ECR) . . . . . . . . . . . . . . . . . . . 37
Amazon Elastic Container Service (ECS) . . . . . . . . . . . . . . . . . . . . 38
Amazon MQ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
Amazon Relational Database Service (RDS) . . . . . . . . . . . . . . . . . . 39
Amazon Route 53 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
Amazon Simple Email Service (SES) . . . . . . . . . . . . . . . . . . . . . . . 39
Amazon Simple Queue Service (SQS) . . . . . . . . . . . . . . . . . . . . . . 40
Amazon Simple Storage Service (S3) . . . . . . . . . . . . . . . . . . . . . . . 40
Amazon Virtual Private Cloud (VPC) . . . . . . . . . . . . . . . . . . . . . . . 40
AWS Certificate Manager . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
AWS Identity and Access Management (IAM) . . . . . . . . . . . . . . . . . 41
AWS Lambda . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
AWS Secrets Manager . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
AWS Systems Manager (SSM) . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
Elastic Load Balancing (ELB) . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
14. Rastreando las Acciones del Usuario con Amazon DynamoDB . . . . . 338
Caso de Uso: Rastreo de Acciones del Usuario . . . . . . . . . . . . . . . . . 340
Amazon RDS vs. Amazon DynamoDB . . . . . . . . . . . . . . . . . . . . . . 341
Implementación del Rastreo de Usuarios en la Aplicación Todo . . . . . 350
Habilitando el Desarrollo Local . . . . . . . . . . . . . . . . . . . . . . . . . . 364
Apéndice . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .465
Usuario Técnico de GitHub Actions IAM . . . . . . . . . . . . . . . . . . . . 465
ÍNDICE GENERAL
El término “cloud” ha sido confuso desde su inicio hace una década. Algunas
personas suben sus fotos al cloud. Algunas organizaciones usan “servicios
cloud” para administrar a sus empleados, clientes y proveedores. ¿Eso significa
que cuando estoy pidiendo pizza en línea también estoy usando “el cloud”?
Por lo tanto, si nos preguntas: sí, pedir pizza en línea es pedir pizza “en el
cloud”.
Los equipos de software ahora tienen un gran poder sobre su proceso de desa-
rrollo de software, y la igualmente gran responsabilidad de operar software en
producción. La colección de prácticas y filosofías que surgieron de este nuevo
paradigma se conoce comúnmente como “DevOps”.
En la Parte II, aprenderemos sobre varios servicios de AWS que podemos usar
para tareas comunes. Construiremos un registro de usuarios y un inicio de
sesión sin implementarlo nosotros mismos aprovechando el servicio Amazon
Cognito. Luego, conectaremos nuestra aplicación Spring Boot con una base
de datos relacional y una base de datos NoSQL. Además, enviaremos correos
electrónicos y nos suscribiremos a sistemas de mensajería, todo “autoservicio”
y completamente administrado por AWS.
Con esto, esperamos que te diviertas tanto leyendo este libro como nosotros
escribiéndolo.
Después de leer este libro, sabrás cómo desplegar una aplicación Spring Boot en
AWS y cómo utilizar muchos servicios de AWS para facilitar tu vida.
En cuanto a AWS, por otro lado, este libro no requiere conocimientos previos.
Comenzaremos desde cero. Si nunca has oído hablar de los servicios de AWS que
estamos utilizando en este libro, no te preocupes. Profundizaremos lo suficiente
Introducción 5
Cuando desarrollamos una aplicación “en la nube”, como haremos en este libro,
estamos pasando automáticamente de un simple “desarrollo” hacia DevOps.
Seguimos desarrollando software (el “dev” en DevOps), pero también nos
importa su operación en la nube (el “ops” en DevOps).
A nosotros tampoco nos gustaba tratar con cuestiones operativas. Sin embargo,
la sensación de “estar en control” que viene con el desarrollo en la nube,
despertó nuestra curiosidad por los asuntos operativos.
Este es un libro práctico con muchos ejemplos de código y cosas para probar
por ti mismo. Puedes leer el libro de principio a fin sin ejecutar los ejemplos
de código por ti mismo, pero obtendrás el mayor beneficio de aprendizaje si
experimentas con el código y lo despliegas en AWS por ti mismo. Para esto,
Introducción 6
Este libro no es una guía para ninguna de las certificaciones de AWS. Si quieres
prepararte para una certificación de AWS, seguramente sería mejor buscar un
recurso específico para esa certificación.
Este libro ofrece conocimiento práctico sobre cómo llevar una aplicación Spring
Boot a la nube y operarla allí. Construiremos un pipeline de despliegue continuo,
accederemos a los servicios de AWS más comunes desde una aplicación Spring
Boot, y aprenderemos cómo monitorear y mantener la aplicación una vez que
Introducción 7
esté en vivo.
Poniéndonos en contacto
Recursos
Tom Hombergs
Björn Wilmsmann
Philip Riecks
Por ahora, no nos importa mucho qué aplicación estamos desplegando. Cual-
quier aplicación web en una imagen Docker valdría. Para propósitos de de-
mostración, utilizaremos una versión “Hola Mundo” de la aplicación Todo que
vamos a construir en la Parte II.
1. Familiarizándonos con AWS
Antes de empezar a construir cualquier característica para nuestra aplicación
Todo, queremos sentirnos cómodos con AWS. Comenzaremos a desarrollar las
funciones en la Parte II del libro.
Lo primero que haremos será desplegar una versión “Hola Mundo” de la apli-
cación Todo para obtener una rápida dosis de dopamina que nos mantendrá en
marcha.
Si ya sabes cómo desplegar una imagen Docker en AWS Fargate con CloudFor-
mation, es posible que quieras saltarte este capítulo.
Preparándonos
Si nunca has desplegado una aplicación en la nube antes, te espera una sorpresa.
Vamos a desplegar una primera versión de nuestra aplicación Todo en AWS con
solo un par de comandos CLI (aunque requiere algo de preparación para que
estos comandos CLI funcionen).
Para hacer cualquier cosa con AWS, necesitas una cuenta con ellos. Si aún no
tienes una cuenta, adelante y créala ahora.
Para hacer magia con AWS desde nuestra línea de comandos, necesitamos
instalar AWS CLI.
AWS CLI es una potencia de una interfaz de línea de comandos que proporciona
comandos para muchos y muy diferentes servicios de AWS (224 en el momento
de escribir esto). En este capítulo, la vamos a usar para desplegar la aplicación
y luego para obtener información sobre la aplicación desplegada.
La instalación de AWS CLI varía entre los sistemas operativos, así que por
favor sigue las instrucciones oficiales para tu sistema operativo para instalar
la versión 2 de AWS CLI en tu máquina.
1. Familiarizándonos con AWS 13
Una vez que esté instalado, ejecuta aws configure. Se te pedirá que proporcio-
nes 4 parámetros:
~ aws configure
AWS Access Key ID [****************Kweu]:
AWS Secret Access Key [****************CmqH]:
Default region name [ap-southeast-2]:
Default output format [yaml]:
Puede conseguir el “AWS Access Key ID” y “AWS Secret Access Key” después
de haber iniciado sesión en su cuenta de AWS cuando haga clic en el nombre de
su cuenta y luego en “My Security Credentials”. Allí, abra la pestaña “Access
keys” y haga clic en “Create New Access Key”. Copie los valores en el prompt
de la AWS CLI.
Ahora, el AWS CLI está autorizado para hacer llamadas a las APIs de AWS en su
nombre.
Cada vez que se interactúa con un servicio de AWS, será con la instancia del
servicio en una región específica. Por lo tanto, elija la región más cercana a su
ubicación de la lista de puntos finales de servicio proporcionados por AWS e
introduzca el código de la región en el prompt aws configure (por ejemplo, “us-
east-1”).
Puede elegir entre dos males: JSON o YAML. No se le juzgará por su elección.
Este comando lista todas las regiones de AWS en las que podemos utilizar
instancias de EC2. EC2 significa “Elastic Cloud Compute”, que es el servicio
de AWS que proporciona máquinas virtuales en las que podemos desplegar
nuestras aplicaciones. Si el comando imprime una lista de regiones, todo está
en orden.
Vamos a darle un ojo rápido a la aplicación Todo que vamos a desplegar en AWS.
./gradlew bootrun
Si sabes cómo empaquetar una aplicación Spring Boot en una imagen Docker,
puedes saltarte esta sección. Ya hemos publicado la aplicación en Docker Hub,
así que puedes usar esa imagen Docker en los próximos pasos.
Si estás interesado en los pasos para crear y publicar una imagen Docker básica,
sigue leyendo.
FROM eclipse-temurin:17-jre
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
Este archivo instruye a Docker para crear una imagen basada en una imagen
básica eclipse-temurin, que agrupa OpenJDK 17 con una distribución de Linux.
A partir de la versión 2.3.0, Spring Boot admite formas más sofisticadas de crear
imágenes Docker, incluyendo Buildpacks nativos de la nube. No entraremos en
detalles sobre esto, pero si estás interesado, esta entrada de blog ofrece una
introducción sobre lo que puedes hacer al respecto.
1. Familiarizándonos con AWS 16
./gradlew build
Para crear una imagen Docker ahora podemos ejecutar este comando:
~ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
stratospheric/todo-app-v1 latest 5d3ef7cda994 3 days ago 647MB
docker login
docker push stratospheric/todo-app-v1:latest
El comando de login te pedirá tus credenciales, por lo que necesitas tener una
cuenta en hub.docker.com. El comando push sube la imagen a Docker Hub para
que cualquiera pueda hacer un pull de ella desde allí con este comando:
Estamos desplegando un cluster ECS dentro de una subred pública en una nube privada virtual.
Para desplegar nuestra aplicación Todo, estamos comenzando con solo algunos
recursos para no sentirnos abrumados. Estamos desplegando los siguientes
recursos:
Una Nube Privada Virtual (VPC) es la base para muchos otros recursos que
desplegamos. Crea una red virtual que es accesible solo para nosotros y nuestros
recursos.
Una VPC contiene subredes públicas y privadas. Una subred pública es accesible
desde internet, una subred privada no lo es. En nuestro caso, desplegamos solo
una subred pública. Para despliegues de producción, normalmente desplega-
ríamos al menos dos subredes, cada una en una zona de disponibilidad (AZ)
diferente para una mayor disponibilidad.
No estar conectado a un Internet Gateway hace que una subred sea privada.
Dentro de un cluster ECS, podemos definir uno o más servicios diferentes que
queremos ejecutar. Para cada servicio, podemos definir un task. Un task está
respaldado por una imagen Docker. Podemos decidir cuántas instancias de
cada task queremos ejecutar y ECS se encarga de mantener vivas siempre esa
cantidad de instancias.
¡Manos a la obra y echemos un vistazo a los archivos que describen esta infraes-
tructura!
Los archivos YAML son las plantillas de CloudFormation que describen los recur-
sos que queremos desplegar. Los scripts de shell envuelven algunas llamadas al
CLI de AWS para crear (es decir, desplegar) y eliminar (es decir, destruir) los
recursos descritos en esos archivos. network.yml describe la infraestructura de
red básica que necesitamos, y service.yml describe la aplicación que queremos
ejecutar en esa red.
La Pila de Red
AWSTemplateFormatVersion: '2010-09-09'
Description: A basic network stack that creates a VPC with a single public subnet
and some ECS resources that we need to start a Docker container
within this subnet.
Resources:
...
En la pila de red, queremos desplegar los recursos básicos que necesitamos para
desplegar nuestra aplicación Todo. Eso significa que queremos desplegar un VPC
con una subred pública y una puerta de enlace a internet para hacer esa subred
accesible desde el exterior. Además, queremos un clúster de ECS donde luego
podremos cargar nuestra imagen Docker.
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: '10.0.0.0/16'
Podemos elegir la clave VPC como mejor nos parezca. Podemos referenciar el
recurso por este nombre más tarde en la plantilla.
Podríamos desplegar la pila de CloudFormation con solo este recurso, pero ne-
cesitamos más infraestructura para desplegar nuestra aplicación. Aquí hay una
lista de todos los recursos que desplegamos con una breve descripción para cada
uno. Puedes buscarlos en el archivo network.yml para ver su configuración:
pública.
• PublicRouteTable: Una RouteTable para definir rutas entre la puerta de
enlace a Internet y la subred pública.
• PublicSubnetRouteTableAssociation: Algunos códigos estándar para
vincular la tabla de rutas con nuestra subred pública.
• PublicRoute: La ruta real que le dice a AWS que queremos permitir el
tráfico desde nuestra puerta de enlace a Internet a cualquier dirección IP
dentro de nuestra subred pública.
• ECSCluster: Un contenedor para ejecutar tareas de ECS. Desplegaremos
una tarea de ECS con nuestra imagen Docker más adelante en la pila de
servicios (service.yml).
• ECSSecurityGroup: Un grupo de seguridad que podemos usar más tarde
para permitir el tráfico a las tareas de ECS (es decir, a nuestro contenedor
Docker). Nos referiremos a este grupo de seguridad más tarde en la pila de
servicios (service.yml)
• ECSSecurityGroupIngressFromAnywhere: Una regla de grupo de seguridad
que permite el tráfico desde cualquier lugar a cualquier recurso adjunto a
nuestro ECSSecurityGroup.
• ECSRole: Un rol que otorga algunos permisos al principal ecs-service.
Estamos otorgando al servicio ECS algunos permisos para modificar ele-
mentos de red por nosotros.
• ECSTaskExecutionRole: Un rol que otorga algunos permisos al principal
ecs-tasks. Este rol dará a nuestras tareas de ECS permisos para escribir
eventos de registro, por ejemplo.
Son bastantes recursos los que necesitamos conocer y configurar. Crear planti-
llas de CloudFormation rápidamente se convierte en una maratón de ensayo y
error hasta que lo configuras justo para tu caso de uso. Más adelante en el libro,
echaremos un vistazo al AWS Cloud Development Kit (CDK) que nos quita algo
1. Familiarizándonos con AWS 24
Todas las funciones tienen una forma larga (Fn::...) y una forma corta (!...)
que se comportan igual pero se ven un poco diferentes en YAML. En resumen,
podemos usar la forma corta para expresiones de una sola línea y la forma larga
para expresiones más largas que podríamos querer dividir en varias líneas.
Outputs:
ClusterName:
Description: The name of the ECS cluster
Value: !Ref 'ECSCluster'
Export:
Name: !Join [ ':', [ !Ref 'AWS::StackName', 'ClusterName' ] ]
... (more outputs)
Cada resultado describe un parámetro que queremos exportar de la pila para ser
utilizado en otras pilas.
Por ejemplo, exportamos el nombre del ECS Cluster bajo el nombre <NETWORK_-
STACK_NAME>:ClusterName. En otras pilas, como nuestra pila de servicio, ahora
solo necesitamos conocer el nombre de la pila de red para acceder a todos sus
parámetros de salida.
La Pila de Servicio
AWSTemplateFormatVersion: '2010-09-09'
Description: Deploys a Docker container within a previously created VPC.
Requires a running network stack.
Parameters:
NetworkStackName:
Type: String
Description: The name of the networking stack that
these resources are put into.
ServiceName:
Type: String
Description: A human-readable name for the service.
ImageUrl:
Type: String
Description: The url of a docker image that will handle incoming traffic.
ContainerPort:
Type: Number
Default: 80
Description: The port number the application inside the docker container
is binding to.
ContainerCpu:
Type: Number
Default: 256
Description: How much CPU to give the container. 1024 is 1 CPU.
ContainerMemory:
Type: Number
Default: 512
Description: How much memory in megabytes to give the container.
DesiredCount:
Type: Number
Default: 1
Description: How many copies of the service task to run.
...
En varias instancias, observarás referencias a las salidas del stack de red como
esta:
Fn::ImportValue:
!Join [':', [!Ref 'NetworkStackName', 'ClusterName']]
Así que, hemos visto más de 200 líneas de configuración YAML que describen
la infraestructura que queremos desplegar. Más tarde, veremos cómo usar CDK
para lograr esto en Java en lugar de en YAML, lo cual lo hace más reutilizable y
más fácil de manejar en general.
sí.
Estamos pasando el nombre para el stack, la ruta hacia nuestra plantilla de stack
network.yml, y la capacidad CAPABILITY_IAM para autorizar al stack a realizar
cambios en los roles de IAM (Identidad y Gestión de Acceso).
CLUSTER_NAME=$(
aws cloudformation describe-stacks \
--stack-name stratospheric-ecs-basic-network \
--output text \
--query 'Stacks[0].Outputs[?OutputKey==`ClusterName`].OutputValue | [0]'
)
echo "ECS Cluster: " $CLUSTER_NAME
TASK_ARN=$(
aws ecs list-tasks \
--cluster $CLUSTER_NAME \
--output text --query 'taskArns[0]'
)
echo "ECS Task: " $TASK_ARN
ENI_ID=$(
aws ecs describe-tasks \
--cluster $CLUSTER_NAME \
--tasks $TASK_ARN \
--output text \
--query 'tasks[0].attachments[0].details[?name==`networkInterfaceId`].value'
)
echo "Network Interface: " $ENI_ID
PUBLIC_IP=$(
aws ec2 describe-network-interfaces \
--network-interface-ids $ENI_ID \
--output text \
--query 'NetworkInterfaces[0].Association.PublicIp'
)
echo "Public IP: " $PUBLIC_IP
Todos los comandos utilizan la CLI de AWS para mostrar los resultados como
text y extraemos cierta información de ese texto con el parámetro --query.
StackId: arn:aws:cloudformation:.../stratospheric-ecs-basic-network/...
StackId: arn:aws:cloudformation:.../stratospheric-ecs-basic-service/...
ECS Cluster: stratospheric-ecs-basic-network-ECSCluster-qqX6Swdw54PP
ECS Task: arn:aws:ecs:.../stratospheric-ecs-basic-network-...
Network Interface: eni-02c096ce1faa5ecb9
Public IP: 13.55.30.162
You can access your service at http://13.55.30.162:8080
La consola AWS es el panel de control para todas las cosas AWS. Con nuestro
navegador, podemos ver el estado de todos los recursos que estamos utilizando,
1. Familiarizándonos con AWS 31
Sin embargo, la consola AWS es un buen lugar para ver los recursos que hemos
desplegado, para comprobar su estado, y para iniciar la depuración si lo necesi-
tamos.
Debería ver una lista de sus pilas de CloudFormation con un estado para cada
una. La lista debería contener al menos las pilas stratospheric-ecs-basic-
service y stratospheric-ecs-basic-network en estado CREATE_COMPLETE.
Haga clic en la pila de red.
Aquí, vemos una lista de eventos para esta pila. Cada evento es un cambio de
estado de uno de los recursos de la pila. Podemos ver el historial de eventos:
Al principio, una serie de recursos estaban en estado CREATE_IN_PROGRESS y
pasaron al estado CREATE_COMPLETE unos segundos después. Luego, cuando
los recursos de los que dependen están listos, otros recursos comenzaron su
vida de la misma manera. Y así continua. CloudFormation se encarga de las
dependencias entre recursos y los crea y elimina en la secuencia correcta.
La pestaña “Eventos” es el lugar al que ir cuando la creación de una pila falla por
alguna razón. Mostrará qué recurso falló y (usualmente) mostrará un mensaje
1. Familiarizándonos con AWS 32
El enlace nos ha llevado a la consola del servicio ECS. También podemos llegar
aquí abriendo el menú desplegable “Servicios” en la parte superior de la página
y escribiendo “ECS” en el cuadro de búsqueda.
La vista detallada de nuestro cluster ECS muestra que tenemos 1 servicio y 1 tarea
ejecutándose en este cluster. Si hacemos clic en la pestaña “Tareas”, veremos
una lista de tareas en ejecución, que debería contener una sola entrada. Haga
clic en el enlace en la columna “Tarea” para obtener una vista detallada de la
tarea.
La vista detallada muestra mucha información que no nos interesa, pero tam-
bién muestra la dirección IP pública de la tarea. Esta es la dirección IP que
extrajimos mediante comandos de AWS CLI anteriormente. Puede copiarla en
su navegador, añadir el puerto 8080, y debería ver el mensaje de bienvenida de
nuevo.
Esto concluye nuestro primer experimento con AWS. Siéntase libre de explorar
un poco más la consola AWS para familiarizarse con cómo funciona todo. Entra-
remos en más detalle sobre diferentes servicios en el resto de este libro.
Los servicios que elegimos incluir en este libro y la aplicación de muestra que
vamos a desarrollar solo pueden representar una pequeña sección de lo que AWS
tiene para ofrecer. Para algunas áreas, a veces existen servicios competidores o
aparentemente superpuestos. A veces, estos solo podrían diferir en términos de
requisitos específicos o incluso simplemente matices.
Por lo tanto, nuestra selección de servicios tiene que ser una elección opinada.
Elegimos aquellos servicios de AWS que cumplen con los requisitos de nuestra
aplicación de muestra. En caso de que hubiera una elección entre dos servicios
competidores, elegimos el más común para proporcionar más valor para el
lector.
Alguien más podría llegar a una selección diferente de servicios, y eso es perfec-
tamente aceptable. Cuando sea apropiado, estaremos discutiendo alternativas
a los servicios presentados.
Creemos que los servicios de AWS que vamos a cubrir en este libro son adecua-
dos para abordar una amplia gama de casos de uso comunes de aplicaciones web.
2. Una visión general de los servicios de AWS 35
Esto, con suerte, le permitirá poner el contenido de este libro a buen uso en el
contexto más amplio del desarrollo de aplicaciones web.
Echemos un vistazo muy rápido a cada uno de los servicios de AWS de los
que vamos a hablar en este libro para dar un poco de contexto a los próximos
capítulos.
AWS CloudFormation
Amazon CloudWatch
Amazon Cognito
Amazon DynamoDB
No vamos a utilizar EC2 directamente en este libro, pero dado que EC2 es el
servicio subyacente para muchos otros servicios de AWS, será mencionado de
vez en cuando. En su lugar, vamos a usar el servicio ECS de un nivel superior
para manejar las instancias de EC2 por nosotros (ver la siguiente sección).
con ECS.
Amazon MQ
Vamos a introducir RDS con una base de datos PostgreSQL para almacenar y
recuperar datos de nuestra aplicación de muestra en el capítulo Conectando a
una Base de Datos con RDS.
Amazon Route 53
Vamos a utilizar Route 53 para redirigir a los usuarios finales a nuestra aplicación
a través de un nombre de dominio fácil de recordar en el capítulo Configurando
HTTPS y un Dominio Personalizado con Route 53 y ELB.
Vamos a introducir SES en conjunto con SQS (ver abajo) en el capítulo Compar-
tiendo Tareas con SQS y SES para enviar correos electrónicos cuando un usuario
quiera compartir una tarea con otro usuario.
2. Una visión general de los servicios de AWS 40
SQS es el servicio de colas básico de Amazon. Proporciona una API para enviar y
recibir mensajes a gran escala y ofrece muchas opciones para manejar mensajes
de manera robusta. Es la primera opción si queremos desacoplar aspectos de
nuestra aplicación de manera asíncrona.
Un VPC es una red virtual que puede contener otros recursos de AWS. Con un
VPC, podemos establecer los límites de la red entre el internet público y nuestra
propia infraestructura. Incluso podemos establecer límites dentro de nuestra
infraestructura.
AWS Lambda
Usaremos AWS Lambda (y Amazon SQS) para organizar en cola las implemen-
taciones en el capítulo Construyendo un Pipeline de Despliegue Continuo.
Con AWS Identity and Access Management (IAM), podemos abordar estas preo-
cupaciones para nuestras aplicaciones en la nube y en el contexto más amplio
de la seguridad y la gestión de accesos dentro de una organización.
Utilizar los servicios de AWS requiere tanto autenticación como los permisos
apropiados (o: privilegios) para acceder a un recurso en particular.
Las listas de control de acceso (ACL) son un enfoque probado y verdadero para
otorgar y gestionar el acceso a los recursos. Las ACL esencialmente responden la
pregunta “¿Quién tiene permiso para acceder a un recurso específico - y en qué
medida - en un momento específico?”. Esta respuesta puede darse de muchas
maneras, siendo la más simple un solo usuario con permisos específicos para
acceder a un recurso específico.
Para aliviar este problema, podemos conceder privilegios en función del grupo
al que pertenece un usuario o del rol que asume en un momento dado. Mientras
que los grupos representan un conjunto de usuarios, los roles combinan un con-
junto de permisos que son necesarios para cumplir una determinada función.
Este diagrama ofrece una visión general de cómo las principales entidades
dentro de IAM se relacionan entre sí:
3. Gestión de Permisos con IAM 46
Los permisos son el concepto central de IAM. Se pueden otorgar a usuarios y aplicaciones de
varias formas.
Como nuevo usuario de AWS, el primer encuentro con IAM ocurre inmediata-
mente después del registro.
La Consola IAM nos permite administrar usuarios, grupos y políticas de IAM (vea
la sección Definición de Políticas) a través de una aplicación web. Más adelante,
también veremos cómo administrar recursos de manera programática.
https://account-ID-or-alias.signin.aws.amazon.com/console
Ahora que hemos configurado tanto los usuarios administrativos como los
normales y sus respectivos grupos, podemos continuar otorgándoles privilegios
para acceder a los recursos de AWS.
Definición de Políticas
Una vez que hemos creado nuestro(s) usuario(s) IAM para nuestro trabajo
diario con AWS, podemos proceder a otorgarles privilegios para los recursos de
AWS. Siguiendo el principio de mínimo privilegio, deberíamos preferir agregar
permisos según sea necesario en lugar de otorgar permisos generales.
3. Gestión de Permisos con IAM 49
Si, por ejemplo, queremos que nuestros desarrolladores tengan acceso sin
restricciones a EC2, podemos otorgarles la política AmazonEC2FullAccess. Sin
embargo, vale la pena mencionar que IAM tiene un límite de 10 políticas por
usuario, rol o grupo.
Dependiendo de nuestros casos de uso, podría ser una buena idea crear una
jerarquía de grupos más detallada que tenga en cuenta diferentes roles y espe-
cializaciones. Por ejemplo, algunos de nuestros desarrolladores podrían trabajar
predominantemente en la infraestructura de la base de datos. Esto a su vez
podría justificar la creación de otro grupo “Desarrolladores de Bases de Datos”,
al que se podrían otorgar privilegios AmazonRDSFullAccess (RDS = Servicio de
Base de Datos Relacional). Los desarrolladores que trabajan principalmente en
los interfaces de usuario web, por otro lado, podrían ser asignados a un grupo
de “Desarrolladores de interfaz de usuario” que tiene permisos AmazonCognito-
PowerUser y AmazonS3FullAccess.
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "ec2:*",
"Effect": "Allow",
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "elasticloadbalancing:*",
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "cloudwatch:*",
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "autoscaling:*",
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "iam:CreateServiceLinkedRole",
"Resource": "*",
"Condition": {
"StringEquals": {
"iam:AWSServiceName": [
"autoscaling.amazonaws.com",
"ec2scheduled.amazonaws.com",
"elasticloadbalancing.amazonaws.com",
3. Gestión de Permisos con IAM 51
"spot.amazonaws.com",
"spotfleet.amazonaws.com",
"transitgateway.amazonaws.com"
]
}
}
}
]
}
La propiedad Effect puede tener un valor de Allow o Deny. Por defecto, se niega
el acceso a los recursos. Al establecer Effect a Allow, estamos otorgando acceso
a los recurso(s) especificados por la propiedad Resource de la declaración.
Asignar a Effect un valor de Deny anula cualquier Allow previo.
La propiedad Resource puede asumir un ARN completo o un ARN parcial con ca-
racteres comodín para coincidir con varios recursos (“*” para cualquier número
de caracteres o “?” para cualquier carácter individual).
otorga permiso a las acciones ec2:*, es decir, todas las acciones en el namespace
“ec2”, mientras que la última declaración otorga permiso solo a la acción
específica iam:CreateServiceLinkedRole.
"Action": [
"sqs:SendMessage",
"sqs:ReceiveMessage",
"ec2:StartInstances",
"iam:ChangePassword",
"s3:GetObject"
]
Una vez que hemos creado una política, podemos adjuntarla a usuarios, grupos
y roles al igual que con las políticas predefinidas.
Vale la pena destacar que no solo podemos acceder a IAM a través de la consola
web, sino también de forma programática.
Aquí es donde la CLI y la API de AWS entran en juego en IAM. Cada acción que
puede ser desencadenada manualmente por un usuario de IAM también puede
ser desencadenada a través de la CLI o la API.
Un caso de uso interesante para esto es crear roles con todos los permisos que
nuestra aplicación necesita para funcionar, y nuestra aplicación asume estos
roles durante la ejecución. A diferencia de los usuarios, los roles vienen con sus
propias credenciales temporales, que son gestionadas completamente por IAM.
Dado que las credenciales nunca abandonan los servidores de AWS, no podemos
accidentalmente subir las credenciales al control de versiones, lo que constitui-
ría una importante brecha de seguridad.
3. Gestión de Permisos con IAM 54
Además, somos capaces de revocar fácilmente los permisos para una aplicación
en particular, o incluso solo un subconjunto de esos permisos.
Permíteme (Tom) contarte una historia de mis primeros días como ingeniero
de software. De los tiempos oscuros de la implementación de software. Depen-
diendo de cuándo te uniste a la industria del software, es posible que hayas
4. La Evolución de las Implementaciones Automatizadas 56
En aquel entonces, cuando todavía pensaba que solo puedes llegar a ser un buen
ingeniero de software a través del sufrimiento, heredamos una base de código
de 10 años de otra empresa de software. El cliente ya no estaba contento con
esa empresa de software y nos otorgó a nosotros el contrato para mantener y
extender el producto. Ahora teníamos 350,000 líneas de código sobre las que no
sabíamos nada. Y no teníamos pruebas unitarias ni scripts de construcción. La
empresa anterior sí tenía algunas pruebas unitarias y scripts de construcción,
pero no nos los dieron porque alguien olvidó mencionarlos en el contrato de
transición. Nos sentíamos como aventureros en el peor sentido de la palabra.
(la siguiente cuenta puede estar teñida con la experiencia de algunos otros
proyectos de software en los que estuve involucrado):
versión más reciente de JBoss. Nadie realmente cree que funcionará pero no
hay otra idea, así que lo intentan.
• Domingo, 09:00: Los desarrolladores informan que en sus máquinas loca-
les, todo funciona bien con la última versión de JBoss. Actualizan las notas
de lanzamiento y las envían a los admins de sistemas.
• Domingo, 10:00: Los admins de sistemas informan que el software se inicia
sin errores (o más bien, sin ningún error al que no estén acostumbrados).
Los probadores inician su trabajo y revisan su propia hoja de cálculo con
casos de prueba para ver si la nueva versión en producción funciona.
• Domingo, 12:00: Teleconferencia programada para reportar avances. Los
probadores han encontrado un bug que bloquea la implementación en
producción. Los desarrolladores comienzan a trabajar en una solución.
• Domingo, 14:00: Los desarrolladores han corregido el bug y subido una
nueva versión. Los admins de sistemas pueden descifrarlo al primer intento
y actualizar la instancia de producción.
• Domingo, 15:00: Los probadores informan que el bug ha sido solucionado
pero se ha introducido otro bug. Aún así, el nuevo bug no es tan grave como
el anterior, por lo que están de acuerdo en lanzar la versión actual (además,
no quieren perderse la fiesta de cumpleaños a la que están invitados, pero
no mencionan eso).
• Domingo, 16:00: Los admins de sistemas han trabajado a través de los
pasos finales en la hoja de cálculo. Nadie excepto ellos entiende de qué se
tratan esos pasos, pero a nadie excepto a ellos realmente les importa. Final-
mente, el equilibrador de carga se reconfigura para apuntar a la aplicación
en lugar de a la página de mantenimiento y todos se van a casa contentos.
• Lunes, 08:00: Los primeros usuarios informan que no pueden trabajar con
el software …
4. La Evolución de las Implementaciones Automatizadas 61
En general, los lanzamientos de software eran así en los primeros días de mi ca-
rrera. ¡Y ten en cuenta que lo anterior se consideraba un lanzamiento exitoso! Un
lanzamiento siempre era algo de lo que tener miedo. Incluso cuando no estaba
planificado que ayudara durante un fin de semana de lanzamiento, me sentía
nervioso al irme a casa el viernes por la noche, sabiendo que probablemente
algo saldrá mal y podrían llamarme para ayudar.
Además, el largo tiempo entre lanzamientos los hacía más grandes, aumen-
tando la posibilidad de fallo. Las funcionalidades se amontonaban porque no
podían esperar a la próxima versión. La presión para tener éxito era mayor, ya
sea la presión de la gerencia o la presión que nos imponíamos. Y los humanos
no toman buenas decisiones bajo presión.
Además, podríamos usar la interfaz de usuario web del servicio Elastic Beanstalk
para simplemente cargar un archivo WAR y dejar que AWS se encargue de
provisionar la infraestructura que necesita. Hay más opciones para desplegar
una aplicación a través de la Consola AWS y parece que se agregan más cada
semana.
La Consola AWS es una puerta de entrada a nuestra nube. Podemos usarla para
desplegar recursos y monitorizar los recursos que ya hemos desplegado.
Aquí es donde entra en juego AWS CLI. Prácticamente todo lo que podemos
hacer a través de la interfaz web de la Consola AWS, también podemos lograrlo a
través de AWS CLI desde la línea de comandos. AWS CLI es una utilidad de línea
de comandos que envuelve las API de todos los servicios de AWS en una única
herramienta de línea de comandos.
4. La Evolución de las Implementaciones Automatizadas 64
De nuevo, AWS CLI nos permite usar APIs de bajo nivel como las del servicio EC2
para aprovisionar nuestros servidores. O, podemos usar APIs de orden superior
como CloudFormation para reducir la cantidad de pasos.
No importa qué API elijamos, con AWS CLI ahora tenemos el poder de hacer todo
lo que necesitamos desde la línea de comandos. ¡Podemos construir una línea
de despliegue totalmente automatizada!
Si usamos CloudFormation con AWS CLI, ya hemos dado otro paso hacia imple-
mentaciones automatizadas y repetibles.
Este estilo declarativo ofrece un sinfín de ventajas sobre una colección de scripts
de shell imperativos.
Podemos usar el mismo archivo de plantilla con los mismos o diferentes pará-
metros para generar una copia del entorno de producción para pruebas. Dado
que ambos entornos declaran los mismos recursos, se comportarán de manera
muy similar y nos facilitarán la vida cuando intentemos reproducir errores.
Podemos compartir pilas en toda la organización para que muchos equipos pue-
dan usarlas para crear sus recursos. Esto apoya los esfuerzos de cumplimiento y
formación. Para crear un pipeline de implementación continua, ya no tenemos
que copiar un montón de scripts de shell, sino que copiamos una plantilla bien
definida y un puñado de comandos de CLI.
Por lo tanto, al final, una aplicación de CDK sigue siendo en su mayoría un enfo-
que declarativo para la infraestructura como código. Es solo que declaramos los
recursos que necesitamos en un lenguaje de programación de nuestra elección
en lugar de en YAML o JSON. Entonces, además de usar un idioma diferente,
¿cuáles son los beneficios sobre las plantillas ordinarias de CloudFormation?
¡Vamos a crear nuestra primera aplicación CDK! Sigue los pasos de este capítulo
para crear una aplicación CDK que despliega nuestra aplicación “Hello World”
en la nube.
1 Lo más importante es que no tenemos que preocuparnos por la indentación.
5. Primeros Pasos con CDK 70
En esa app, podemos definir uno o más stacks. Y cada stack define un conjunto
de recursos que deben ser desplegados como parte de ese stack. Nota que un
stack de CDK es el mismo concepto que un stack de CloudFormation.
Una vez que tenemos una app en su lugar, la CLI de CDK nos permite desplegar o
retirar todas las stacks al mismo tiempo, o podemos elegir interactuar solo con
un stack específico.
Antes de que podamos comenzar, tenemos que cumplir con algunos requisitos
previos.
Instalando Node
Aunque estamos usando el CDK de Java, la CLI de CDK está construida con
Node.js. Por lo tanto, necesitamos instalarlo en nuestra máquina.
Con Node.js instalado, esto es tan fácil como llamar a npm install -g aws-
cdk. Esto hará que el comando CLI de CDK cdk esté disponible globalmente en
tu sistema.
Al igual que con Node.js, puedes verificar la versión de tu instalación CLI de CDK
llamando a cdk --version.
A partir de la versión 1.4 de este libro, utilizaremos AWS CDK v2. Asegú-
rate de usar una versión de cdk >= 2.0.0 para todos los ejemplos que se
presenten. Para migrar tu infraestructura existente de AWS CDK v1, sigue
la guía oficial de migración.
Vamos a crear una nueva carpeta para nuestra app, cambiar a ella, y ejecutar
este comando:
Después de que CDK ha creado nuestra aplicación, nos encontramos con este
mensaje:
5. Primeros Pasos con CDK 72
The `cdk.json` file tells the CDK Toolkit how to execute your app.
## Useful commands
Enjoy!
El mensaje anterior indica que necesitamos ejecutar mvn package para compilar
y ejecutar las pruebas. Eso significa que Maven necesita estar instalado en
5. Primeros Pasos con CDK 73
nuestra máquina. Pensando un poco más, esto también significa que Maven
necesita estar instalado en el servidor de compilación una vez que decidamos
configurar un pipeline de despliegue continuo.
Pero aún no hemos terminado del todo. ¿Recuerdas el mensaje que decía que el
archivo cdk.json le dice al CDK cómo ejecutar nuestra aplicación? Veamos ese
archivo:
{
"app": "mvn -e -q compile exec:java",
"watch": {
},
"context": {
}
}
La primera línea de esta estructura JSON indica al CDK cómo compilar y luego
ejecutar nuestra aplicación CDK. Se ha configurado para llamar a mvn por defecto.
Por lo tanto, reemplacemos eso por ./mvnw y estamos listos.
Ahora, cada vez que llamemos a un comando como cdk deploy, el CDK llamará
al Wrapper de Maven en lugar de directamente a Maven para ejecutar nuestra
aplicación CDK.
5. Primeros Pasos con CDK 74
app.synth();
}
}
public CdkStack(final Construct scope, final String id, final StackProps props) {
super(scope, id, props);
¡Eso es todo el código que necesitamos para una aplicación CDK funcional!
Cuando ejecutamos comandos CDK como cdk deploy, CDK ejecutará el método
main de CdkApp para generar los archivos de CloudFormation. El comando
deploy sabe dónde buscar estos archivos y luego los envía a la API de Cloud-
Formation para su despliegue.
Para evitar tales escenarios y completar todos los posibles pasos de configura-
ción al menos una vez, vamos a hacer bootstrap a nuestro entorno AWS.
Al hacer bootstrap a nuestro entorno AWS, el AWS CDK crea un bucket S3, roles
IAM y un ECR para propósitos internos. Estos recursos ya pueden resultar en
costos mínimos en nuestra factura de AWS. Sin embargo, son insignificantes y
deberían estar cubiertos por la capa gratuita de AWS de todos modos.
Cada entorno (es decir, una combinación de una cuenta de AWS y una región)
necesita hacer bootstrap por separado. Este es un esfuerzo de única vez, a menos
5. Primeros Pasos con CDK 76
que migremos a una nueva versión principal del CDK, en cuyo caso tendríamos
que “re-hacer bootstrap” al entorno.
Si queremos eliminar los recursos creados por este proceso de arranque, pode-
mos borrar manualmente el stack dentro de la consola de AWS.
Esto es tan fácil como ejecutar el comando cdk deploy en la carpeta de la app.
Tomará un par de segundos y seremos recompensados con un mensaje de éxito
como este:
5. Primeros Pasos con CDK 77
TestStack: deploying...
TestStack: creating CloudFormation changeset...
[========================================================] (2/2)
TestStack
Stack ARN:
arn:aws:cloudformation:ap-southeast-2:...
Esto significa que CDK ha desplegado con éxito la pila vacía. Si iniciamos sesión
en la consola web de AWS y navegamos hasta el servicio CloudFormation,
deberíamos ver una pila llamada “TestStack” desplegada en dicho servicio:
Como mencionamos, los recursos que incluimos en una pila de CDK se llaman
constructos. Para mostrar la potencia del CDK, y para mantenerlo simple por
ahora, hemos preparado un constructo con el nombre SpringBootApplica-
tionStack que incluye todos los recursos que necesitamos. Todo lo que nece-
sitamos hacer es incluir este constructo en nuestra pila de CDK.
<dependency>
<groupId>dev.stratospheric</groupId>
<artifactId>cdk-constructs</artifactId>
<version>0.1.0</version>
</dependency>
Utilizando el SpringBootApplicationStack
Por consiguiente, se modifica la clase generada CdkApp para incluir una Spring-
BootApplicationStack en lugar de un CdkStack vacío:
new SpringBootApplicationStack(
app,
"SpringBootApplication",
makeEnv(accountId, region),
"docker.io/stratospheric/todo-app-v1:latest");
app.synth();
}
Es posible que te preguntes por qué no estamos haciendo nada con la instancia
SpringBootApplicationStack. Cuando creamos un constructo, siempre pasa-
mos un constructo padre o la aplicación padre al constructor. El constructo luego
se registrará con la aplicación para que la aplicación sepa qué constructos incluir
en la pila de CloudFormation sintetizada al llamar a app.synth().
Outputs:
SpringBootApplication.loadbalancerDnsName = prod-loadbalancer-810384126.ap-southeast\
-2.elb.amazonaws.com
Stack ARN:
arn:aws:cloudformation:ap-southeast-2:494365134671:stack/SpringBootApplication/0b6b4\
410-3be9-11eb-b5d5-0a689720a8fe
Hemos desplegado con éxito una aplicación de Spring Boot con unas 20 líneas
de código Java con la ayuda de AWS CDK. Anteriormente, en el capítulo Fami-
liarizándonos con AWS, lo mismo nos ha llevado un par de cientos de líneas de
configuración YAML. ¡Es todo un logro!
Entonces, ¿por qué no detenernos aquí? ¿Por qué hay otro capítulo en profun-
didad sobre CDK? Nuestro SpringBootApplicationStack nos da todo lo que
necesitamos para desplegar una aplicación Spring Boot, ¿no es así?
5. Primeros Pasos con CDK 84
¿Qué pasa si necesitamos conectar nuestra aplicación Spring Boot a una base
de datos o colas SQS? ¿Qué pasa si la ruta a la verificación de salud de nuestra
aplicación es diferente de la predeterminada? ¿Qué pasa si nuestra aplicación
necesita más potencia de CPU que las 256 unidades predeterminadas? ¿Qué pasa
si preferimos usar HTTPS en lugar de HTTP?
Además, imagina un entorno con más de una aplicación. Tendríamos una red pa-
ra el entorno de preproducción y otra para el entorno de producción. Querríamos
desplegar múltiples aplicaciones en cada red. Esto no funciona actualmente,
porque cada SpringBootApplicationStack intentaría crear su propio VPC (lo
cual fallaría para la segunda aplicación porque intentaría usar los mismos
nombres de recursos).
Esto significa que nuestro proyecto CDK necesita ser flexible. Debe permitirnos
desplegar recursos adicionales según sea necesario y proporcionar numerosas
opciones para configurar la infraestructura y nuestra aplicación. Queremos
tener un control detallado y preciso.
Para obtener este control, necesitamos crear nuestras propias pilas y construc-
tos. Y esto es exactamente lo que vamos a hacer en el próximo capítulo.
6. Diseñando un Proyecto de Despliegue
con CDK
En el capítulo anterior, ya hemos desplegado una aplicación Spring Boot en
AWS con el CDK. Utilizamos un componente preconfigurado de “caja negra”
llamado SpringBootApplicationStack, introdujimos unos pocos parámetros,
y lo envolvimos en una aplicación CDK para desplegarlo con la interfaz de línea
de comandos de CDK.
¡Empecemos!
La visión general
El objetivo básico de este capítulo sigue siendo el mismo que en los capítulos
Calentando con AWS y Primeros Pasos con CDK: Queremos desplegar una simple
6. Diseñando un Proyecto de Despliegue con CDK 86
aplicación “Hola Mundo” de Spring Boot (en una imagen Docker) en una subred
pública de nuestra propia red virtual privada (VPC). Esta vez, sin embargo,
queremos hacerlo con componentes CDK reutilizables y estamos añadiendo
algunos requisitos más:
ECS extraerá la imagen Docker que queremos desplegar como tarea directamen-
te de nuestro repositorio Docker.
Cabe destacar que desplegaremos la pila de Red y la pila de Servicio dos ve-
ces: una para un entorno de ensayo y otra para un entorno de producción.
6. Diseñando un Proyecto de Despliegue con CDK 88
La pila de Repositorio Docker, por otro lado, la desplegaremos solo una vez.
Servirá imágenes Docker tanto al entorno de ensayo como al de producción.
Una vez que hayamos probado una imagen Docker de nuestra aplicación en el
entorno de ensayo, queremos desplegar exactamente la misma imagen Docker
en producción, así que no necesitamos un repositorio Docker separado para cada
entorno. Si tuviéramos más de una aplicación, sin embargo, probablemente
querríamos crear un repositorio Docker para cada aplicación para mantener
las imágenes Docker separadas de manera limpia. En ese caso, reutilizaríamos
nuestra pila de Repositorio Docker y la desplegaríamos una vez para cada
aplicación.
Veamos cómo podemos construir cada una de esas tres pilas con CDK de una ma-
nera eficiente y sostenible. Pasaremos por cada una de las pilas y discutiremos
cómo podemos implementarlas con componentes CDK reutilizables.
Cada pila vive en su propia aplicación CDK. Mientras discutimos cada pila,
señalaremos los conceptos que aplicamos al desarrollar los componentes y
aplicaciones CDK. Estos conceptos nos ayudaron a gestionar la complejidad de
CDK, y esperamos que también te ayuden en tus esfuerzos.
Dicho esto, por favor no consideres estas estrategias como una solución mágica
- diferentes circunstancias requerirán diferentes estrategias. Discutiremos cada
una de ellas en su propia sección para que no se pierdan en un mar de texto.
6. Diseñando un Proyecto de Despliegue con CDK 89
Antes de empezar a trabajar con CDK, sin embargo, algunas palabras sobre cómo
trabajar con CDK.
La construcción manual de pilas con CDK puede requerir mucho tiempo, espe-
cialmente cuando aún no estás familiarizado con los recursos de CloudForma-
tion que quieres usar. Ajustar los parámetros de configuración de esos recursos
y luego probarlos es un gran esfuerzo porque tienes que desplegar la pila cada
vez para probarla.
Habiendo enumerado todas esas molestias del Java CDK, no todo es malo. La
comunidad en GitHub es muy activa y ha habido una solución o solución alter-
nativa para cualquier problema que hemos encontrado hasta ahora. La inversión
de tiempo seguramente valdrá la pena una vez que hayas construido constructos
que muchos equipos de tu empresa puedan usar para desplegar rápidamente sus
aplicaciones a AWS.
Comenzaremos con la pila más simple - la pila del repositorio Docker. Esta pila
solo desplegará un único recurso de CloudFormation, es decir, un repositorio de
ECR.
app.synth();
}
Lo desglosaremos paso a paso en las próximas secciones. Podría ser una buena
idea abrir el código en tu navegador para tenerlo a mano mientras sigues
leyendo.
6. Diseñando un Proyecto de Despliegue con CDK 92
Como se discutió en Primeros Pasos con CDK, podemos pasar parámetros a una
aplicación CDK con el parámetro de línea de comandos -c o agregándolos a
la sección de contexto en el archivo cdk.json. En la aplicación, leemos los
parámetros accountId y region de esta manera:
Entonces, pasamos este objeto Environment a la stack que creamos a través del
método env() en el constructor.
No debería ser una sorpresa que recomendamos fuertemente validar todos los
parámetros de entrada. Hay pocas cosas más frustrantes que desplegar una
stack y que CloudFormation se queje 5 minutos después sobre la falta de algo
en el despliegue.
Eso es suficiente para captar una amplia gama de errores desde el principio.
Para la mayoría de los parámetros, esta simple validación será suficiente. No
queremos hacer validaciones pesadas como verificar si una cuenta o una región
realmente existe, porque CloudFormation no tarda en lanzar errores en estos
casos.
Otro concepto que estamos promoviendo es el de una única pila por aplicación
de CDK.
6. Diseñando un Proyecto de Despliegue con CDK 94
Técnicamente, CDK nos permite agregar tantas pilas como queramos a una
aplicación de CDK. Cuando interactuamos con la aplicación de CDK, podríamos
elegir qué pilas desplegar o destruir proporcionando un filtro adecuado:
Suponiendo que la aplicación CDK contiene muchos stacks, los dos primeros
comandos desplegarían exactamente un stack. El tercer comando desplegaría
todos los stacks con el prefijo “Stack”, y el último comando desplegaría todos
los stacks.
Sin embargo, existe un gran inconveniente con este enfoque. CDK creará los
archivos de CloudFormation para todos los stacks, incluso si solo queremos
desplegar un único stack. Esto significa que tenemos que proporcionar los pa-
rámetros de entrada para todos los stacks, incluso si solo deseamos interactuar
con un único stack.
El Componente DockerRepository
public DockerRepository(
final Construct scope,
final String id,
final Environment awsEnvironment,
final DockerRepositoryInputParameters dockerRepositoryInputParameters) {
super(scope, id);
cdk deploy \
-c accountId=... \
-c region=... \
-c applicationName=...
Eso funcionará mientras tengamos una única aplicación CDK, pero como po-
drías suponer ahora, vamos a construir varias aplicaciones CDK - una para cada
stack. En cuanto haya más de una aplicación en el classpath, CDK se quejará
porque no sabe a cuál de esas aplicaciones iniciar.
cdk deploy \
--app "./mvnw -e -q compile exec:java \
-Dexec.mainClass=dev.stratospheric.todoapp.cdk.DockerRepositoryApp" \
-c accountId=... \
-c region=... \
-c applicationName=...
Con el parámetro --app, podemos definir el ejecutable que CDK debería lla-
mar para ejecutar la aplicación de CDK. Por defecto, CDK llama a mvn -e -q
compile exec:java para correr una aplicación (este valor predeterminado está
configurado en cdk.json, como se discute en Primeros pasos con CDK).
Ahora hemos resuelto el problema de tener más de una aplicación de CDK pero
no queremos teclear todo eso en la línea de comandos cada vez que queramos
probar un despliegue, ¿verdad?
Para hacerlo un poco más cómodo al ejecutar un comando con muchos argu-
mentos, trasladamos los parámetros de configuración no sensibles al archivo
cdk.json:
{
"context": {
"accountId": "221875718260",
"region": "eu-central-1",
"applicationName": "todo-app"
}
}
{
"name": "stratospheric-cdk",
"version": "0.1.0",
"private": true,
"scripts": {
"repository:deploy": "cdk deploy --app ...",
"repository:destroy": "cdk destroy --app ..."
},
"devDependencies": {
"aws-cdk": "2.5.0"
}
}
Una vez que hayamos ejecutado npm install para instalar la dependencia CDK
(y sus dependencias transitivas, en ese sentido), podemos implementar nuestro
stack de repositorio Docker con un simple npm run repository:deploy.
A lo largo de este libro, añadiremos scripts para todas nuestras aplicaciones CDK
en este archivo package.json.
El único parámetro que tenemos que proporcionar al comando cdk es el pará-
metro --app, ya que este varía para cada aplicación CDK.
Con este archivo package.json, ahora tenemos una ubicación central donde
podemos buscar los comandos que tenemos a nuestra disposición para imple-
mentar o eliminar pilas de CloudFormation. Además, no tenemos que escribir
6. Diseñando un Proyecto de Despliegue con CDK 100
mucho para ejecutar uno de los comandos. Más tarde añadiremos más coman-
dos a este archivo y enriqueceremos nuestro archivo cdk.json con nuevos
parámetros de configuración. Puedes echar un vistazo al archivo completo con
las tres pilas en GitHub.
.build());
app.synth();
}
El nombre del entorno puede ser una cadena de texto cualquiera. Lo usamos en
el método stackName() para prefijar el nombre de la pila. Más tarde, veremos
6. Diseñando un Proyecto de Despliegue con CDK 102
que lo usamos dentro del constructo Network también para prefijar los nombres
de algunos otros recursos. Esto separa la pila y los otros recursos de los desple-
gados en otro entorno.
Una vez que hemos desplegado la aplicación con, por ejemplo, el nombre del
entorno “staging”, podemos desplegarla de nuevo con el nombre del entorno
“prod” y se desplegará una nueva pila. Si usamos el mismo nombre de entorno,
CDK reconocerá que una pila con el mismo nombre ya ha sido desplegada y
actualizará esa pila en lugar de tratar de crear una nueva.
El Constructo Network
// fields omitted
public Network(
final Construct scope,
final String id,
final Environment environment,
final String environmentName,
final NetworkInputParameters networkInputParameters) {
super(scope, id);
this.environmentName = environmentName;
this.vpc = createVpc(environmentName);
6. Diseñando un Proyecto de Despliegue con CDK 103
createLoadBalancer(vpc, networkInputParameters.getSslCertificateArn());
createOutputParameters();
}
Está estructurado de forma muy similar a la pila de red que creamos en el capí-
tulo Familiarizándonos con AWS. Creamos un VPC y un clúster de ECS para alojar
nuestra aplicación más tarde. Además, ahora estamos creando un balanceador
de carga y lo conectamos al clúster de ECS. Este balanceador de carga distribuirá
las solicitudes entre varios nodos de nuestra aplicación.
No entraremos en los detalles de este código, sin embargo, porque es mejor con-
sultar en los documentos de CDK y CloudFormation para entender qué recursos
usar y cómo usarlos. Si te interesa, no dudes en explorar el código del constructo
Network en GitHub y abrir los documentos de CDK en una segunda ventana del
6. Diseñando un Proyecto de Despliegue con CDK 104
navegador para leer sobre cada uno de los recursos. Si los documentos de CDK
no profundizan lo suficiente siempre puedes buscar el recurso respectivo en los
documentos de CloudFormation.
Nuestra NetworkApp crea una red en la que podemos colocar nuestra aplicación
más tarde. Otros stacks - como el stack de Servicio, que vamos a ver a continua-
ción - necesitarán conocer algunos parámetros de esa red para poder conectarse
a ella. El stack de Servicio necesitará saber en qué VPC ubicar sus recursos, a qué
balanceador de carga conectar, y en qué clúster de ECS desplegar el contenedor
Docker, por ejemplo.
Optamos por una solución más elegante que es más fácil de mantener y más
flexible: Cuando desplegamos el stack de Network, almacenamos cualquier
parámetro que otros stacks necesiten en la tienda de parámetros SSM.
// more parameters
}
Con este patrón de nombres, podemos leer los parámetros que necesitamos al
construir otros stacks sobre el stack de Network.
Para que sea más práctico recuperar los parámetros nuevamente, hemos añadi-
do métodos estáticos al constructor Network que recuperan un solo parámetro
del almacén de parámetros:
6. Diseñando un Proyecto de Despliegue con CDK 106
return StringParameter.fromStringParameterName(
scope,
PARAMETER_VPC_ID,
createParameterName(environmentName, PARAMETER_VPC_ID))
.getStringValue();
}
Luego podemos invocar este método desde otras aplicaciones CDK para obtener
todos los parámetros con una sola línea de código.
Sin embargo, estas salidas están fuertemente acopladas al stack que las crea,
mientras que queremos crear parámetros de salida para constructs que luego
pueden componerse en un stack. Además, la tienda SSM sirve como una visión
general de todos los parámetros que existen en diferentes entornos, lo que
facilita mucho la depuración de errores de configuración.
Otra razón para usar parámetros SSM es que tenemos más control sobre ellos.
Podemos nombrarlos como queramos y podemos acceder a ellos fácilmente
utilizando el patrón descrito anteriormente. Eso permite un modelo de progra-
mación conveniente.
Dicho esto, los parámetros SSM tienen la desventaja de incurrir en costos adicio-
nales de AWS con cada llamada API a la tienda de parámetros SSM. En nuestra
aplicación de ejemplo, esto es insignificante pero en una gran infraestructura
puede sumar una cantidad considerable.
completo en GitHub:
DockerImageSource dockerImageSource =
new DockerImageSource(dockerRepositoryName, dockerImageTag);
NetworkOutputParameters networkOutputParameters =
Network.getOutputParametersFromParameterStore(
serviceStack,
applicationEnvironment.getEnvironmentName());
ServiceInputParameters serviceInputParameters =
new ServiceInputParameters(
dockerImageSource,
environmentVariables(springProfile))
.withHealthCheckIntervalSeconds(30);
app.synth();
}
}
Sin embargo, aquí están ocurriendo algunas cosas nuevas. Vamos a explorarlas.
6. Diseñando un Proyecto de Despliegue con CDK 110
Mientras que para el conjunto de red era suficiente con añadir al inicio el
environmentName a los conjuntos y recursos, ahora necesitamos que el prefijo
contenga también el applicationName. Después de todo, podríamos querer
usar el conjunto de Service para implementar varias aplicaciones en la misma
red.
NetworkOutputParameters networkOutputParameters =
Network.getOutputParametersFromParameterStore(
serviceStack,
applicationEnvironment.getEnvironmentName());
Luego pasamos estos parámetros al constructor Service para que pueda usarlos
para vincular los recursos que despliega a la infraestructura de red existente.
Más adelante en este libro, haremos más uso de este mecanismo cuando este-
mos creando más stacks que expongan parámetros que la aplicación necesita,
como una URL de base de datos o parámetros de contraseña.
DockerImageSource dockerImageSource =
new DockerImageSource(dockerImageUrl);
El Service Construct
El objetivo del construct Service es crear un servicio ECS dentro del clúster ECS
que proporciona el construct Network. Para ello, crea muchos recursos en su
constructor (vea el código completo en GitHub):
public Service(
final Construct scope,
final String id,
final Environment awsEnvironment,
final ApplicationEnvironment applicationEnvironment,
final ServiceInputParameters serviceInputParameters,
final Network.NetworkOutputParameters networkOutputParameters){
super(scope,id);
CfnTargetGroup targetGroup=...
CfnListenerRule httpListenerRule=...
LogGroup logGroup=...
...
}
6. Diseñando un Proyecto de Despliegue con CDK 114
Realiza bastante más que la pila de servicios del capítulo Entrando en calor con
AWS:
• Crea una CfnTaskDefinition para definir una tarea de ECS que aloja la
imagen de Docker proporcionada.
• Añade un CfnService al clúster de ECS previamente desplegado en el
construct Network y añade las tareas a él.
• Crea un CfnTargetGroup para el balanceador de carga desplegado en el
construct Network y lo vincula al servicio de ECS.
• Crea un CfnSecurityGroup para los contenedores de ECS y lo configura para
que el balanceador de carga pueda enrutar el tráfico a los contenedores de
Docker.
• Crea un LogGroup para que la aplicación pueda enviar registros a Cloud-
Watch.
Esto resalta una posible desventaja de los constructs de alto nivel: Diferentes
proyectos de software necesitan diferentes infraestructuras, y los constructs
de alto nivel no siempre son lo suficientemente flexibles para satisfacer esas
6. Diseñando un Proyecto de Despliegue con CDK 115
Podríamos, por supuesto, crear una biblioteca de constructs que sea altamente
parametrizada y flexible para muchos requisitos diferentes. Esto podría hacer
que los constructs sean complejos y propensos a errores, sin embargo. Otra op-
ción es invertir esfuerzo para crear tu propia biblioteca de constructs adecuada
para tu proyecto (o organización).
Accediendo al Servicio
Si deseas experimentar con las aplicaciones CDK que hemos discutido anterior-
mente, siéntete libre de clonar el repositorio de GitHub y navega a la carpeta
cdk. Luego:
6. Diseñando un Proyecto de Despliegue con CDK 116
Luego, echa un vistazo en la Consola de AWS para ver los recursos que crearon
esos comandos.
Kim.
7. Construyendo un Pipeline de Despliegue Continuo 118
Un trabajo agrupa múltiples pasos en una unidad lógica y los coloca en una
secuencia. Mientras los pasos dentro de un trabajo se ejecutan en secuencia,
varios trabajos dentro de un flujo de trabajo se ejecutan en paralelo por defecto.
7. Construyendo un Pipeline de Despliegue Continuo 119
Vamos a crear un flujo de trabajo que inicialice y prepare nuestro entorno AWS.
Puede revisar el código de todos los flujos de trabajo que estamos discutiendo
en GitHub:
7. Construyendo un Pipeline de Despliegue Continuo 120
on:
workflow_dispatch
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
jobs:
deploy:
runs-on: ubuntu-20.04
name: Bootstrap CDK
if: github.ref == 'refs/heads/main'
steps:
Este flujo de trabajo tiene una única responsabilidad: Preparar nuestra cuenta de
AWS para despliegues de infraestructura y aplicaciones con el CDK. Esto incluye
7. Construyendo un Pipeline de Despliegue Continuo 121
Para llegar al punto en el que podemos ejecutar comandos CDK, nuestro trabajo
requiere un par de pasos preparatorios: obtener el código, instalar Java, y
ejecutar npm install.
Vale la pena mencionar que hemos configurado para que este flujo de trabajo
se active mediante un evento de workflow_dispatch, lo que significa que tiene
que ser activado manualmente. GitHub Actions mostrará un botón en la interfaz
de usuario que nos permite activar este flujo de trabajo.
Este flujo de trabajo necesita ser ejecutado solo una vez para un entorno de AWS.
Automatizando estos pasos con GitHub Actions, ahora tenemos una capacidad
de autoservicio para todos los desarrolladores del equipo para iniciar nuevos
entornos.
7. Construyendo un Pipeline de Despliegue Continuo 122
on:
workflow_dispatch:
inputs:
environmentName:
description: 'The name of the environment to create.'
required: true
jobs:
deploy-network-stack:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
name: Deploy the network stack
steps:
# ... preparatory steps
Nuestra aplicación Todo necesita más infraestructura que solo una red, aunque.
En capítulos posteriores, integraremos una base de datos, sistemas de men-
sajería e infraestructura de autenticación también. Otras aplicaciones pueden
requerir su propia instancia de base de datos u otra infraestructura con estado.
on:
workflow_dispatch:
inputs:
environmentName:
description: 'The name of the environment to deploy the resources to.'
required: true
jobs:
deploy-messaging-stack:
...
deploy-database-stack:
...
deploy-cognito-stack:
...
Este flujo de trabajo tiene un par de trabajos. Cada trabajo contiene pasos
muy similares a los pasos del flujo de trabajo en la sección anterior, pero
cada uno con un comando diferente. Un trabajo puede llamar a npm run de-
ploy:database para desplegar la base de datos, y otro puede llamar a npm
run deploy:messaging para iniciar una pila con una instancia de ActiveMQ.
Crearemos las aplicaciones CDK detrás de estos comandos en capítulos poste-
riores de este libro. Dado que estas pilas son independientes entre sí, podemos
desplegarlas en paralelo. Por lo tanto, colocamos cada pila en su propio trabajo.
Con los dos flujos de trabajo que hemos construido hasta ahora, un desarro-
llador puede crear su propio entorno para desplegar su propia instancia de la
aplicación Todo. Como los flujos de trabajo se activan manualmente, el entorno
no se actualizará automáticamente cuando cambiemos algo en las aplicaciones
CDK correspondientes. Sin embargo, podemos modificar fácilmente los flujos
de trabajo para que se activen por un cambio en el código CDK para volver
7. Construyendo un Pipeline de Despliegue Continuo 125
Digamos que hemos ejecutado todos los flujos de trabajo que hemos discutido
hasta ahora y hemos creado un entorno completo. Ahora es el momento de
desplegar nuestra aplicación Todo en este entorno.
on:
push:
paths:
- 'application/**'
- 'cdk/**/*Service*'
- 'cdk/pom.xml'
workflow_dispatch:
jobs:
build-and-deploy:
...
El flujo de trabajo solo tiene una tarea llamada build-and-deploy, que im-
plementa los tres pasos del diagrama anterior: “Build”, “Publish” y “Deploy”.
Podríamos haber construido una tarea separada para cada uno de los pasos, pero
7. Construyendo un Pipeline de Despliegue Continuo 127
dado que queremos compartir la salida de un paso con los siguientes, esto habría
complicado un poco más el flujo de trabajo.
El paso “Build”
El paso de “Publicación”
Estos son en realidad dos pasos: uno para la creación de una etiqueta para la
imagen Docker y otro para la creación y publicación de la imagen Docker.
Si este paso se ha finalizado con éxito, ahora tenemos una imagen Docker
actualizada en nuestro repositorio ECR, lista para ser desplegada.
env:
AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
Hasta ahora, esto es un flujo de trabajo de entrega continua. Con cada cambio al
código, entregamos una nueva versión de la aplicación a un repositorio Docker.
El Paso de “Desplegar”
Por lo tanto, aunque esta solución probablemente sea totalmente adecuada para
muchos proyectos, no es adecuada para proyectos con una alta frecuencia de
commits. Muchas ejecuciones de las acciones fallarán y no hay garantía de que
un commit se desplegará de manera oportuna.
Desplegando solo la última versión de una imagen de Docker con la ayuda de SQS y Lambda.
Cada vez que llega una solicitud de despliegue a la cola, será procesada por
una función de AWS Lambda que vamos a desarrollar. Esta función Lambda
luego desencadenará un despliegue solo si actualmente no hay un despliegue
en ejecución, ordenando efectivamente los despliegues uno tras otro, incluso si
hubo muchos commits en un corto período.
Nuestra función de manejo de AWS Lambda procesa todos los mensajes SQS
entrantes de esta manera:
// If there are more events in the queue: finish and wait for the next event.
if (await queue.hasWaitingEvents()) {
console.log(
"Skipping this event because there are more events waiting in the queue!"
);
return;
}
Una función Lambda siempre debe declarar exactamente una función controla-
dora que se ejecuta cuando la Lambda es activada. En nuestro caso, la Lambda
se ejecutará cada vez que exista un nuevo evento de implementación en nuestra
cola de implementación. En Typescript/Javascript, esta función controladora
es simplemente una función que exportamos en nuestro archivo principal
(index.ts).
{
"commitSha": "674e5044b3b269ccb8f4530193cc144f2d6a5ae6",
"ref": "main",
"owner": "stratospheric-dev",
"repo": "stratospheric",
"workflowId": "05-update-todo-app-in-staging.yml",
"dockerImageTag": "20210320051451-674e5044b3b269ccb8f4530193cc144f2d6a5ae6"
}
Cada evento contiene el nombre de la imagen Docker que necesita ser desplega-
da.
Primero, verificamos si la cola SQS tiene más eventos esperando en línea. Si hay
al menos un otro evento esperando, simplemente pasamos por alto el evento
actual. La idea es que en este caso, no queremos desplegar la imagen Docker
de este evento ya que ya hay una imagen Docker más reciente esperando ser
desplegada en la cola. En lugar de eso, desplegamos esa imagen Docker más
reciente. Un detalle importante para que esto funcione es que la cola SQS debe
estar configurada como una cola FIFO (primero en entrar, primero en salir).
Esto significa que el orden de los eventos que llegan a la función Lambda está
garantizado para ser el orden en el que los eventos fueron recibidos por la cola
SQS. Para obtener más detalles sobre cómo funciona SQS, echa un vistazo al
capítulo Compartiendo Todos con SQS y SES.
public DeploymentSequencerStack(
final Construct scope,
final String id,
final Environment awsEnvironment,
final String applicationName,
final String githubToken) {
.environment(Map.of(
"GITHUB_TOKEN", githubToken,
"QUEUE_URL", deploymentsQueue.getQueueUrl(),
"REGION", awsEnvironment.getRegion()
)).build()
)).build();
Creamos una cola SQS con fifo configurado en true. Ten en cuenta que el
nombre de una cola FIFO siempre necesita tener el sufijo “.fifo”, de lo contrario,
habrá un error durante el despliegue.
Lambda para funcionar. Ya hemos visto cómo la función Lambda lee estas
variables de entorno en el código Lambda anterior.
Esos son todos los recursos que necesitamos para apoyar nuestra idea de secuen-
ciar los despliegues.
{
...
"scripts": {
...
"deployment-sequencer:deploy": "cdk deploy \"*\" --app ...",
"deployment-sequencer:destroy": "cdk destroy \"*\" --app ..."
}
}
El elemento que ahora nos falta es entender cómo los eventos de despliegue
llegan a la cola de SQS de despliegue. Para esto, debemos modificar nuestro
existente flujo de trabajo de ‘Desplegar’.
a su propio flujo de trabajo para que pueda ser activado por nuestro Lambda. Así
es como se ve el nuevo flujo de trabajo de ‘Desplegar’:
on:
workflow_dispatch:
inputs:
docker-image-tag:
description: "The Docker image to deploy"
required: true
env:
AWS_ACCOUNT_ID: ${{ secrets.AWS_ACCOUNT_ID }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: ${{ secrets.AWS_REGION }}
jobs:
deploy-app:
runs-on: ubuntu-20.04
name: Deploy Todo App
steps:
Ahora, todo lo que queda por hacer es modificar el flujo de trabajo “Desplegar”
anterior y convertirlo en el nuevo flujo de trabajo “Publicar”. En lugar de
desplegar la imagen Docker, el flujo de trabajo “Publicar” enviará una solicitud
de despliegue a la cola de SQS después de que se haya publicado una imagen
Docker:
on:
push:
paths:
- 'application/**'
- 'cdk/**/*Service*'
- 'cdk/pom.xml'
workflow_dispatch:
jobs:
build-and-publish:
runs-on: ubuntu-20.04
name: Build and publish Todo App
steps:
¡Eso es! Cada vez que el workflow de publicación ha publicado con éxito una
imagen de Docker con una nueva versión de nuestra aplicación, ahora enviará el
nombre de esa imagen de Docker a la cola de despliegue, que la enviará a nuestra
Lambda, que a su vez decidirá si y cuándo llamar a nuestro flujo de trabajo de
despliegue con una dada etiqueta de imagen de Docker.
Puede parecer excesivo crear una solución creada desde cero como la que hici-
mos arriba para asegurarnos de que los despliegues se ejecutan en secuencia. Y
7. Construyendo un Pipeline de Despliegue Continuo 142
tienes razón si tienes acceso a herramientas que pueden hacerlo por ti. Si no,
sin embargo, invertir un par de días de trabajo en este tipo de automatización
evitará la activación manual y reactivación de despliegues fallidos para todo el
equipo y será beneficioso a largo plazo.
Por favor, tenga en cuenta que la solución descrita aquí no es infalible. La cola y
la Lambda se aseguran de que los despliegues se activan en la misma secuencia
en la que llegan a la cola. Si dos construcciones disparadas por dos commits
se están ejecutando aproximadamente al mismo tiempo, y la construcción
disparada por el commit anterior tarda más que la construcción disparada por
el commit posterior, entonces la función Lambda podría descartar la solicitud
de despliegue para la imagen de Docker que contiene el commit posterior y solo
desplegará la imagen de Docker que contiene el commit anterior. Para resolver
esto, se necesitaría algún tipo de almacenamiento de datos persistente para
almacenar la secuencia de commits, que entonces podría ser consultada cada
vez que se recibe un evento de despliegue.
Addendum: Configurando HTTPS y un
Dominio Personalizado con Route 53 y
ELB
Una vez que la aplicación esté desplegada, podemos acceder a ella a través de
HTTP y una dirección IP. ¿Recuerdas el capítulo Familiarizándose con AWS, donde
utilizamos el AWS CLI para obtener la dirección IP pública de nuestra aplicación
desplegada? Utilizando este enfoque, la URL de nuestra aplicación podría lucir
algo así:
http://13.55.30.162
https://staging-loadbalancer-1376672807.eu-central-1.elb.amazonaws.com/
Sin embargo, aunque esto pueda ser adecuado para fines de desarrollo o pruebas,
ningún usuario quiere escribir esta URL en su navegador. Generalmente preferi-
mos servir nuestra aplicación de producción a través de un nombre de dominio
personalizado como app.stratospheric.dev.
Además, queremos mantener seguros los datos de nuestros usuarios. Eso signi-
fica que queremos usar HTTPS y la Seguridad de la Capa de Transporte (TLS)/SSL
Addendum: Configurando HTTPS y un Dominio Personalizado con Route 53 y ELB 144
en lugar de HTTP sin cifrar. Esto es prácticamente un estándar hoy en día y los
usuarios lo esperan.
Pero antes de sumergirnos en cómo configurar todo esto con AWS, aprendamos
un poco sobre DNS para contextualizar todo.
Entre otros usos, los registros TXT proporcionan un enfoque conveniente para
verificar la propiedad de un subdominio. Quien controla las entradas de DNS
para un dominio controla en última instancia los recursos servidos desde ese
dominio y, por lo tanto, puede demostrar su propiedad cambiando un registro
TXT.
Más adelante haremos uso de esto para verificar nuestra identidad al solicitar
un certificado SSL para un dominio.
Aunque no es tan antiguo como el DNS, SSL (lanzado públicamente por Netscape
en 1995), su sucesor TLS, y el protocolo HTTPS que habilita son ingredientes
indispensables de Internet moderno.
Más específicamente, HTTPS puede describirse como HTTP sobre TLS: la comu-
nicación sigue siendo responsabilidad del HTTP que conocemos y usamos, mien-
tras que TLS habilita el cifrado y la verificación criptográfica de la identidad.
Addendum: Configurando HTTPS y un Dominio Personalizado con Route 53 y ELB 147
Solo las claves públicas son intercambiadas públicamente (de ahí su nombre).
Dado que el conocimiento de la clave pública por sí solo no permite a un atacante
el acceso no autorizado, esto no compromete la seguridad.
Si, por ejemplo, las máquinas que alojan nuestra aplicación de ejemplo Todo
fueran comprometidas, un actor malicioso podría hacerse pasar por la entidad
legítima detrás de app.stratospheric.dev e intentar desviar las credenciales
de usuario.
Sin embargo, los certificados autofirmados tienden a usarse con bastante fre-
cuencia en configuraciones internas de empresas, por ejemplo en intranets. En
esos casos, la propia empresa se convierte en la autoridad de confianza para los
recursos internos. Para esos casos de uso, esto podría ser aceptable dependiendo
de los requisitos exactos y las medidas de seguridad adicionales para prevenir la
intrusión. Para el conjunto de Internet, eso no es una opción ya que las partes
involucradas no necesariamente se conocen o confían entre sí.
Addendum: Configurando HTTPS y un Dominio Personalizado con Route 53 y ELB 149
Con este trasfondo, ahora hemos sentado las bases para trabajar con DNS,
HTTPS y certificados SSL/TLS. Entonces, vamos a profundizar en la configura-
ción del DNS y del SSL para el nombre de dominio de nuestra aplicación.
El nombre “Route 53” es un juego de palabras con el puerto UDP 53, que es el
puerto predeterminado para el protocolo DNS.
Con Route 53, podemos registrar un nuevo dominio o transferir uno existente
desde un registrador de dominios diferente como GoDaddy, Namecheap o Hetz-
ner. La consola de Route 53 proporciona asistentes para guiarnos a través de
cualquiera de esos procesos.
Una vez que nuestro dominio personalizado deseado ha sido registrado o trans-
ferido a Route 53, Route 53 nos proporcionará una zona alojada que podremos
configurar.
Addendum: Configurando HTTPS y un Dominio Personalizado con Route 53 y ELB 150
Ahora tenemos una visión general de cómo funcionan DNS y SSL. Hemos visto
cómo estas tecnologías pueden usarse para asignar un nombre de dominio a
nuestra aplicación y al mismo tiempo cifrar todo el tráfico entrante y saliente
para nuestra aplicación.
No obstante, automatizar este proceso sigue siendo una buena idea porque
documenta implícitamente un proceso y hace que ese proceso sea reproducible
en diferentes entornos.
Para esta tarea, creamos una nueva app de CDK llamada CertificateApp:
// ...
// ...
Addendum: Configurando HTTPS y un Dominio Personalizado con Route 53 y ELB 152
new CertificateStack(
app,
"certificate",
awsEnvironment,
applicationEnvironment,
applicationDomain,
hostedZoneDomain
);
app.synth();
}
}
{
"scripts": {
"certificate:deploy": "cdk deploy --app ... ",
"certificate:destroy": "cdk destroy --app ..."
}
}
Recuerda que una zona hospedada es un contenedor para todos los registros
DNS que pertenecen a un dominio. Usamos el hostedZoneDomain que hemos
proporcionado como entrada a nuestro CertificateApp para localizarlo.
Luego, generamos un nuevo certificado SSL que se valida mediante DNS (un
certificado validado por DNS):
� certificate (staging-todo-app-Certificate)
Outputs:
certificate.sslCertificateArn = arn:aws:acm:eu-central-1:...:certificate/...
Este parámetro de salida será importante para los siguientes pasos, ya que
estamos a punto de crear un oyente HTTPS para nuestro balanceador de carga.
Hemos diseñado el constructo subyacente Network de tal manera que solo crea
un oyente HTTP para el ELB si no se pasa ningún certificado SSL. AWS no
proporciona certificados SSL para sus dominios ELB predeterminados. Siempre
que queramos crear un oyente HTTPS, debemos proporcionar un certificado SSL
válido.
if (sslCertificateArn.isPresent()) {
IListenerCertificate certificate = ListenerCertificate
.fromArn(sslCertificateArn.get());
httpsListener = loadBalancer.addListener("httpsListener",
BaseApplicationListenerProps.builder()
.port(443)
.protocol(ApplicationProtocol.HTTPS)
.certificates(Collections.singletonList(certificate))
.open(true)
.build()
);
httpsListener.addTargetGroups("https-defaultTargetGroup",
AddApplicationTargetGroupsProps.builder()
.targetGroups(Collections.singletonList(dummyTargetGroup))
.build());
Servir nuestra aplicación con esta configuración desde ambos puertos 80 y 443
podría hacer que algunos usuarios accedan a nuestra aplicación a través de HTTP.
Para evitar cualquier tráfico inseguro, por lo tanto, debemos garantizar la opción
segura y sólo permitir el acceso a través de HTTPS. Para que esto funcione,
redireccionamos automáticamente todo tráfico no cifrado a HTTPS:
Addendum: Configurando HTTPS y un Dominio Personalizado con Route 53 y ELB 157
Lo que falta es un registro A DNS para este subdominio que señale al ELB.
// ...
// ...
new DomainStack(
app,
"domain",
awsEnvironment,
applicationEnvironment,
hostedZoneDomain,
applicationDomain
);
app.synth();
}
{
"scripts": {
"domain:deploy": "cdk deploy --app ... ",
"domain:destroy": "cdk destroy --app ..."
}
}
Network.NetworkOutputParameters networkOutputParameters =
Network.getOutputParametersFromParameterStore(
this,
applicationEnvironment.getEnvironmentName()
);
Lo que queda por hacer es ejecutar npm run domain:deploy para crear un
registro A para nuestro subdominio app dentro de Route53. Nuestra aplicación
puede que no sea accesible inmediatamente, ya que el cambio en el DNS puede
demorar algunos minutos hasta que se propague a los servidores DNS alrededor
del mundo.
Con estas dos apps de CDK, es decir CertificateApp y DomainApp, ahora hemos
configurado nuestro DNS y la configuración del certificado SSL como código. De
esta manera, la infraestructura relacionada con la configuración del dominio de
nuestra aplicación puede ser reproducida y regenerada, si fuera necesario.
En esta parte del libro, presentaremos nuestra aplicación de ejemplo Todo y nos
prepararemos para el desarrollo en local.
Luego, hablaremos sobre RDS, el servicio de Amazon para bases de datos rela-
cionales, y lo usaremos para crear una base de datos PostgreSQL para nuestra
aplicación.
Dos casos de uso importantes para cualquier aplicación son la gestión de colas
de mensajes y el envío de correos electrónicos, por lo que integraremos con
Amazon SQS y SES para implementar una función de colaboración que notifica
a un usuario cuando un Todo ha sido compartido con él.
Las aplicaciones Todo son algo así como un estándar de oro para comparar las
características de lenguajes o marcos. La lógica de negocio de tal aplicación es
simple, lo que nos permite concentrarnos en el lenguaje o marco en cuestión.
Por lo tanto, decidimos crear otra aplicación web Todo, que servirá como un
ejemplo continuo a medida que avanzamos.
Características
Registro y Acceso
Los usuarios serán identificados por sus direcciones de correo electrónico. Los
datos del usuario en sí se almacenarán en un pool de usuarios de Cognito.
Nuestra aplicación utilizará OIDC (OpenID Connect, un marco de autenticación
sobre OAuth2) para recuperar y mantener la sesión de Cognito del usuario.
Cualquier aplicación que maneje datos modificables por el usuario, en este caso,
las tareas, tiene que proporcionar algún tipo de funcionalidad CRUD (Crear,
Recuperar, Update, Delete).
Los usuarios pueden crear tareas y agregar notas de texto a esas tareas. Pueden
ver sus tareas en la pantalla y editarlas o borrarlas.
Para guardar los datos, utilizaremos una base de datos PostgreSQL que se ejecuta
en el Servicio de Bases de Datos Relacionales (RDS) de Amazon.
A veces, los usuarios podrían no solo querer trabajar en sus tareas solos, sino
colaborar también con otros usuarios. Por lo tanto, la aplicación de ejemplo
permite a los usuarios compartir sus tareas con otros a través de notificaciones
8. La Aplicación de Ejemplo Todo 166
Notificaciones Push
Arquitectura de la Aplicación
La aplicación es una simple aplicación Spring Boot que intenta seguir patrones
comunes de Spring y Spring Boot. Este libro es, ante todo, sobre cómo correr
una aplicación Spring Boot en AWS, después de todo. No queremos perdernos
en detalles técnicos o consideraciones de diseño que no son relevantes para el
tema de este libro.
Configuración
Características
Interfaz de Usuario
Dado que Thymeleaf está muy bien integrado con Spring Boot, decidimos usarlo
para renderizar las vistas al usuario. Las plantillas HTML de Thymeleaf y los
archivos estáticos se pueden encontrar en la carpeta resources, como es la
práctica común.
Almacenamiento
La aplicación utiliza Spring Data JPA para guardar datos en una base de datos
PostgreSQL. Utilizamos Flyway para configurar y luego migrar la base de datos
de la aplicación al estado requerido por el código fuente de la aplicación actual.
8. La Aplicación de Ejemplo Todo 168
Modelo de Dominio
Una tarea tiene una prioridad y un estado, por lo que podemos manejarlas de
manera diferente dependiendo de estos atributos. Podemos agregar notas de
texto y recordatorios a una tarea, y una persona puede tener tareas.
8. La Aplicación de Ejemplo Todo 169
Dependencias Principales
¿Qué tienen en común todos los excelentes proyectos de Spring Boot? Todos
fueron creados con start.spring.io. Lo mismo sucedió con nuestra aplicación de
tareas. Echemos un vistazo a la configuración y las dependencias principales de
nuestro esqueleto de proyecto.
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'io.awspring.cloud:spring-cloud-aws-starter'
El proyecto Spring Cloud AWS se encarga de alinear la versión del SDK Java de
AWS (v2 desde Spring Cloud AWS 3.0, anteriormente v1) para nosotros utilizan-
do internamente el software.amazon.awssdk:bom. Podemos sobrescribir la
versión del SDK Java de AWS, pero entonces tendríamos que garantizar nosotros
mismos su compatibilidad:
dependencyManagement {
imports {
// explicitly define the AWS Java SDK v2 version
mavenBom "software.amazon.awssdk:bom:2.20.4"
}
}
Aunque solo tenemos una dependencia de Spring Cloud (hasta ahora), estamos
utilizando el BOM de Spring Cloud para alinear las versiones de las dependen-
cias.
8. La Aplicación de Ejemplo Todo 171
El BOM de Spring Cloud AWS asegura que todas las dependencias (próximas) que
forman parte del paraguas de Spring Cloud AWS compartan la misma versión:
ext {
set('awsSpringVersion', '3.0.0-RC1')
}
dependencyManagement {
imports {
mavenBom "io.awspring.cloud:spring-cloud-aws-dependencies:${awsSpringVersion}"
}
}
Aparte del Spring Boot Starter y Spring Cloud AWS, estamos incluyendo las
siguientes dependencias relacionadas con la interfaz de usuario:
8. La Aplicación de Ejemplo Todo 172
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:2.5.3'
implementation 'org.webjars:webjars-locator-core'
implementation 'org.webjars:bootstrap:4.6.1'
implementation 'org.webjars:font-awesome:5.15.3'
• spring.cloud.aws.credentials.secret-key, y
8. La Aplicación de Ejemplo Todo 173
• spring.cloud.aws.credentials.access-key.
Sin embargo, el Elastic Container Service (ECS) proporciona una variable de en-
torno (AWS_CONTAINER_CREDENTIALS_RELATIVE_URI) para todos nuestros con-
tenedores en ejecución. Esta ruta apunta a la ubicación de nuestras credenciales
para el rol IAM adjunto a nuestra tarea de ECS. El SDK de AWS viene con un
proveedor (ContainerCredentialsProvider) que puede recuperar las creden-
ciales para esta configuración.
Como estamos usando ECS en Fargate (lo que significa que no especificamos
nuestras propias instancias de EC2, porque Fargate las especifica por nosotros),
8. La Aplicación de Ejemplo Todo 174
spring:
cloud:
aws:
region:
static: eu-central-1
Nuestra aplicación inicial ya tiene un punto de acceso público que muestra una
vista Thymeleaf:
8. La Aplicación de Ejemplo Todo 175
@Controller
public class IndexController {
@GetMapping
public String getIndex() {
return "index";
}
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout/layout}"
th:with="activeMenuItem='start', headline='Welcome to the Todo Application!'">
<head>
<title>Start</title>
</head>
<section class="section" layout:fragment="page-content">
<div class="container">
<p>There's not much to see here (yet).</p>
</div>
</section>
</html>
No hay mucho que explicar para esta vista index ya que solo estamos rende-
rizando un mensaje estático. Sin embargo, vale la pena mencionar el uso del
namespace de layout. Como queremos reutilizar la misma estructura de página
para todas nuestras vistas, vamos a definir un layout general.
<!DOCTYPE html>
<html lang="en"
xmlns="http://www.w3.org/1999/xhtml"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title layout:title-pattern="$CONTENT_TITLE | $LAYOUT_TITLE">
Todo Application
</title>
<script th:src="@{/webjars/jquery/jquery.min.js}"></script>
<script th:src="@{/webjars/popper.js/umd/popper.min.js}"></script>
<script th:src="@{/webjars/bootstrap/js/bootstrap.min.js}"></script>
</head>
<body>
<div>
<div th:replace="fragments/header :: header"></div>
</html>
Incluimos recursos de CSS y JavaScript que se requieren para cada vista y es-
tructuramos la página con un conjunto de fragmentos. Estos fragmentos tienen
varios propósitos: mostrar la barra de navegación superior, el pie de página,
renderizar mensajes informativos y un modal de confirmación.
Para una gran referencia de Spring Boot & Thymeleaf con ejemplos prác-
ticos (paginación, internacionalización, pruebas, seguridad, etc.) y una
explicación en profundidad, echa un vistazo al libro de Wim Deblauwe
Taming Thymeleaf.
Docker Image
FROM eclipse-temurin:17-jre
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
Administramos todos los ejemplos de código fuente para este libro como parte
del repositorio GitHub de Stratospheric. El repositorio contiene:
Construyendo la Aplicación
./gradlew build
Eso no es una tarea sencilla, y hay varias soluciones para esto. En el próximo
capítulo Desarrollo Local examinaremos con más detalle cómo funciona el desa-
rrollo local. Por ahora, es suficiente saber que utilizamos Docker Compose.
./gradlew bootRun
Este comando utiliza el plugin de Spring Boot para Gradle y arranca la aplicación
Spring Boot.
Como el tema principal de este libro es AWS, podemos esperar que nuestra
aplicación integre múltiples servicios de AWS. Cada vez que iniciamos la aplica-
ción en nuestras máquinas locales, debemos decidir cómo interactuar con estos
servicios de AWS.
Nuestro objetivo principal es hacer que el desarrollo local sea lo más conve-
niente posible. Tener una extensa lista de pasos de configuración con múltiples
scripts para ejecutar no solo es perjudicial para la experiencia del desarrollador,
sino que también requiere mantener la lista actualizada. Todos conocemos la
frustración de trabajar con un nuevo proyecto, seguir las instrucciones en el
README, y luego darnos cuenta de que están desactualizadas.
Analicemos diferentes opciones para hacer que el desarrollo local contra los
servicios de AWS sea lo más conveniente posible.
9. Desarrollo Local 182
Sin embargo, hay algunas desventajas importantes para este enfoque. Si solo te-
nemos un entorno de AWS, los desarrolladores podrían interferir con el entorno
de producción, y los usuarios reales podrían ver datos de prueba divertidos (o no
tan divertidos). Podemos solucionar esto proporcionando un entorno de prueba
separado o, idealmente, un entorno bajo demanda para cada desarrollador. Esto
incurre en costos adicionales por el uso de los servicios de AWS. También sería
inconveniente para los desarrolladores porque necesitarían una cuenta de AWS
y tendrían que gastar algo de dinero de AWS solo para iniciar la aplicación
localmente.
LocalStack es …
Para ahora hacer uso de este AWS S3 local mientras trabajamos con el AWS CLI,
tenemos que pasar localhost:4566 como el --endpoint-url:
Esto implica que tenemos que anular nuestros clientes de Java AWS SDK para
conectarse a LocalStack en lugar de AWS cuando iniciamos la aplicación local-
mente. Con Spring Cloud AWS, podemos anular convenientemente las URLs de
los puntos finales y las regiones para todos los clientes de AWS SDK:
9. Desarrollo Local 185
spring:
cloud:
aws:
endpoint: http://localhost:4566
region:
static: eu-central-1
spring:
cloud:
aws:
# above configuration
credentials:
secret-key: foo
access-key: bar
Lo que queda es crear nuestros recursos (por ejemplo, un bucket S3 o cola SQS)
una vez que LocalStack está en funcionamiento. Esto debe suceder antes de
iniciar la aplicación Todo. El contenedor Docker de LocalStack ejecuta cualquier
script de shell que está en la carpeta /docker-entrypoint-initaws.d cuando
todos los servicios están disponibles.
#!/bin/sh
LocalStack funciona muy bien para la mayoría de los servicios de AWS y sus
características básicas. Sin embargo, hay servicios de AWS que no son parte
de la edición comunitaria de LocalStack. Nuestra aplicación, por ejemplo, usará
PostgreSQL de RDS para la persistencia y Amazon Cognito como proveedor de
identidad, ambos solo son compatibles con la versión pro de LocalStack. Como
no podemos esperar que todos los lectores gasten dinero en la versión pro de
LocalStack, optaremos por una alternativa (aunque aún deberías respaldar este
proyecto si lo estás utilizando).
Reuniéndolo Todo
Como no queremos iniciar cada contenedor Docker uno por uno con docker run,
utilizaremos docker-compose. En los próximos capítulos, ampliaremos nuestro
archivo docker-compose.yml con contenedores adicionales o configuraciones.
Con Spring Boot, podemos separar las dos configuraciones con dos archivos:
application-aws.yml y application-dev.yml. Además de esto, tendremos
un application.yml que configura los valores por defecto para ambos perfiles.
Para las bibliotecas de JavaScript y CSS que necesita nuestra interfaz, estamos
utilizando Webjars. Esto significa que no hay necesidad de descargar ningún
recurso externo desde un CDN ya que todo está empaquetado y servido por
nuestra aplicación.
9. Desarrollo Local 188
Con este enfoque, reconocemos que nuestra configuración local no está re-
flejando totalmente la verdadera configuración de AWS. Todavía puede haber
diferencias, especialmente en cuanto a la carga de trabajo y el rendimiento. Sin
embargo, esta configuración es “suficientemente buena” y fácil de configurar y
mantener.
10. Construyendo Registro de Usuarios
e Inicio de Sesión con Amazon Cognito
Con este capítulo, estamos añadiendo las primeras características a nuestra
aplicación Todo: registro de usuarios e inicio de sesión. La gestión de grupos de
usuarios es una tarea común en todas las aplicaciones empresariales. Siempre
que tratamos con estas cuestiones de seguridad, generalmente es mejor usar
una solución prefabricada en lugar de reinventar la rueda.
Para quitarnos esta tarea delicada de las manos, hemos elegido un servicio de
AWS que podemos integrar sin problemas con nuestra aplicación Spring Boot.
Este estándar existe actualmente en dos versiones: OAuth 1.0 y OAuth 2.0.
Ambas abordan la misma idea, pero la versión 2 simplifica la integración y
ofrece más flexibilidad. Una comparación en profundidad de ambas versiones
está disponible en el blog OAuth 2.0 Simplificado.
Como el enfoque de este libro está en Spring Boot y AWS, sólo cubriremos las
partes de OAuth que son necesarias para entender la integración entre nuestra
aplicación y Amazon Cognito. Hay excelentes recursos disponibles en línea para
profundizar más en el tema. Marco Behler, por ejemplo, ha elaborado una guía
exhaustiva que se centra en OAuth 2.0 y Spring Security.
Un buen ejemplo es la plataforma de CI/CD Travis CI, que podemos usar para
construir y desplegar código de nuestros proyectos en GitHub. Después de
registrarnos en una cuenta de Travis CI, se nos pide que otorguemos a Tra-
vis acceso a nuestros repositorios de GitHub para que Travis pueda construir
nuestros proyectos en cada push. Para que esto funcione, seremos redirigidos a
GitHub, donde luego tendremos que introducir nuestras credenciales de GitHub.
Después, tenemos que confirmar que estaremos otorgando a Travis CI acceso
a un conjunto de operaciones/información que Travis CI puede ahora realizar
en nuestro nombre. Una vez concedido, veremos nuestros proyectos de GitHub
10. Construyendo Registro de Usuarios e Inicio de Sesión con Amazon Cognito 191
Para entender cómo funciona el escenario que acabamos de describir sin dar a
Travis acceso a nuestras credenciales de GitHub (lo que sería un fallo de seguri-
dad significativo), primero tenemos que definir y explicar algunos términos de
OAuth 2.0.
• Titular del Recurso: Normalmente un usuario final (nosotros) que posee re-
cursos en una aplicación de terceros (por ejemplo, repositorios en GitHub).
• Servidor de Recursos: La mayoría de las veces, un servidor (API de GitHub)
que expone recursos protegidos.
• Aplicación Cliente: Una aplicación (por ejemplo, Travis CI) que quiere
acceder a los recursos protegidos en nombre del Titular del Recurso.
• Servidor de Autenticación: Un servidor que autentica al Titular del Recurso
(usuario final) y emite tokens de acceso después de obtener la autorización.
Cómo una Aplicación Cliente obtiene un token de acceso válido para solicitar
recursos protegidos de un Titular del Recurso depende del caso de uso y de la
arquitectura de la aplicación. OAuth2.0 define diferentes flujos de trabajo (los
llamados “tipos de concesión”) sobre cómo una aplicación puede acceder a
recursos protegidos. Los tipos de concesión más comúnmente usados son los
siguientes:
En nuestro ejemplo con Travis, hemos usado la autorización del tipo Authori-
zation Code para permitir el acceso a nuestros repositorios de GitHub. Travis
necesita un token de acceso válido para acceder a las APIs de GitHub y recabar
información sobre nuestros repositorios en GitHub. Usando la autorización
Authorization Code, el procedimiento funciona así en un alto nivel:
1. La aplicación (el Client, Travis en este caso) nos redirige (al Resource Owner)
al Authorization Server (la página de inicio de sesión de GitHub).
2. El Resource Owner (nosotros) introduce las credenciales para el Authorization
Server y aprueba la solicitud para acceder a los datos en su nombre.
3. El usuario es redirigido a la aplicación (Travis) con un código de autorización
en la cadena de consulta.
4. La aplicación (Travis) intercambia este código de autorización por un token
de acceso para obtener datos desde el Resource Server (la API de GitHub).
Con la ayuda de los llamados “scopes”, un cliente define las operaciones y datos
a los que necesita tener acceso. Aunque hay diferentes operaciones y recursos
disponibles en GitHub (como “leer perfil”, “ver repositorios públicos”, “admi-
nistrar una organización”, etc.), Travis no requiere todos estos. Definiendo un
subconjunto de scopes permite a Travis realizar solo aquellas operaciones que
necesita.
En resumen, OIDC es un …
Siempre que veas “Iniciar sesión con GitHub” o “Iniciar sesión con Google” en
la página de inicio de sesión de un sitio web o una aplicación, esa aplicación o
sitio web está haciendo uso de este protocolo OpenID Connect.
Para los siguientes capítulos, basta con entender que OIDC es una capa de
identidad sobre OAuth 2.0 que permite el inicio de sesión de los usuarios con
10. Construyendo Registro de Usuarios e Inicio de Sesión con Amazon Cognito 195
una cuenta de un tercero (por ejemplo, Google, GitHub, Twitter). Esto nos libera
de tener que implementar nosotros mismos la gestión de usuarios. Puedes leer
la especificación oficial para obtener más información en mayor detalle.
Dado que la seguridad es un tema crucial, preferimos que los expertos nos
resuelvan este tema. Si se descuida al inicio de un proyecto, añadir funciones
relacionadas con la seguridad a última hora, justo antes de ponerlo en marcha,
no es beneficioso. Tener resuelto el tema de la seguridad desde el comienzo del
proyecto proporciona tranquilidad y mejora nuestro tiempo de lanzamiento al
mercado, ya que podemos concentrarnos en nuestras funcionalidades.
Por otro lado, ahora tenemos una dependencia adicional que es crucial para
que nuestra aplicación funcione: Dependemos del uptime del proveedor de
10. Construyendo Registro de Usuarios e Inicio de Sesión con Amazon Cognito 196
identidad. Por lo tanto, nuestra aplicación sería inutilizable (al menos sus áreas
protegidas) siempre que haya una interrupción del servicio.
Soluciones similares listas para usar que podríamos usar en lugar de Amazon
Cognito son Okta, Keycloak o Auth0.
Como parte del User Pool, también podemos configurar nuestra política de
contraseñas, definir atributos de usuario requeridos y opcionales, habilitar
mecanismos de recuperación de contraseña y personalizar las notificaciones por
correo electrónico.
Un User Pool App Client está asociado con un User Pool y tiene permiso para
realizar operaciones de API no autenticadas como registrando o registrar usua-
rios. Por lo tanto, cada App Client requiere un ID de cliente y una clave secreta
opcional.
Como parte de la creación del App Client, configuramos las URL de callback/lo-
gout y definimos qué flujos y alcances de OAuth 2.0 puede utilizar este cliente.
Registraremos nuestra aplicación “Todo” como un User Pool App Client para
habilitar el inicio de sesión de usuario a través de OIDC y el flujo de OAuth 2.0
Authorization Code Grant.
necesitan roles IAM para usar la aplicación, un Identity Pool no es relevante para
nosotros.
Para una comparación en profundidad de Identity Pools y User Pools echa un vis-
tazo a la publicación de blog de Jake Bennett Understanding Amazon Cognito User
and Identity Pools for Serverless Apps. No solo este artículo explica su diferencia,
sino que también muestra cómo podríamos usar ambos conceptos juntos.
Como todos los demás recursos de AWS que proporcionamos para nuestra
aplicación, preferimos la infraestructura como código sobre los esfuerzos ma-
nuales repetitivos en la Consola de AWS. Veamos cómo podemos crear nuestros
recursos de Cognito con CDK.
// omitted standard configuration values like the AWS region and sanity checks
);
new CognitoStack(
app,
"cognito",
awsEnvironment,
applicationEnvironment,
new CognitoStack.CognitoInputParameters(
applicationName,
applicationUrl,
loginPageDomainPrefix));
app.synth();
}
}
Para la definición de stack que viene, incluimos el módulo Cognito de AWS CDK
en nuestro proyecto CDK. La biblioteca construct de AWS CDK ya proporciona
constructos estables de nivel 1 y nivel 2 para User Pools:
10. Construyendo Registro de Usuarios e Inicio de Sesión con Amazon Cognito 200
Crear el UserPool
// fields omitted
public CognitoStack(
final Construct scope,
final String id,
final Environment awsEnvironment,
final ApplicationEnvironment applicationEnvironment,
final CognitoInputParameters inputParameters) {
this.applicationEnvironment = applicationEnvironment;
.userPoolName(inputParameters.applicationName + "-user-pool")
.selfSignUpEnabled(false)
.standardAttributes(StandardAttributes.builder()
.email(StandardAttribute
.builder()
.required(true)
.mutable(false)
.build())
.build())
.signInAliases(SignInAliases
.builder()
.username(true)
.email(true)
.build())
.signInCaseSensitive(true)
.autoVerify(AutoVerifiedAttrs
.builder()
.email(true)
.build())
.mfa(Mfa.OFF)
.accountRecovery(AccountRecovery.EMAIL_ONLY)
.passwordPolicy(PasswordPolicy.builder()
.requireLowercase(true)
.requireDigits(true)
.requireSymbols(true)
.requireUppercase(true)
.minLength(12)
.tempPasswordValidity(Duration.days(7))
.build())
.build();
}
}
A diferencia del enfoque de crear nuestros propios Constructs de CDK que se-
guimos en el capítulo Diseñando un Proyecto de Despliegue con CDK, aquí estamos
utilizando un enfoque ligeramente diferente: Estamos creando un Stack de CDK
que utiliza constructs oficiales de Cognito de CDK de nivel 2 en lugar de nuestros
10. Construyendo Registro de Usuarios e Inicio de Sesión con Amazon Cognito 202
.withTaskRolePolicyStatements(List.of(
PolicyStatement.Builder.create()
.sid("AllowCreatingUsers")
.effect(Effect.ALLOW)
.resources(
List.of(String.format("arn:aws:cognito-idp:%s:%s:userpool/%s", region,
accountId, cognitoOutputParameters.getUserPoolId()))
)
.actions(List.of("cognito-idp:AdminCreateUser"))
.build()
))
Con los atributos restantes, permitimos que este cliente de la piscina de usua-
rios utilice el flujo de Concesión de Código de Autorización OAuth 2.0 con un
conjunto predefinido de alcances de OAuth 2.0.
Para configurar Spring Security para nuestra aplicación Todo más adelante, ne-
cesitamos exponer los atributos dinámicos de nuestra configuración de Cognito:
StringParameter.Builder.create(this, "userPoolId")
.parameterName(createParameterName(
applicationEnvironment,
PARAMETER_USER_POOL_ID))
.stringValue(this.userPool.getUserPoolId())
.build();
StringParameter.Builder.create(this, "userPoolClientId")
.parameterName(createParameterName(
applicationEnvironment,
PARAMETER_USER_POOL_CLIENT_ID))
.stringValue(this.userPoolClient.getUserPoolClientId())
.build();
StringParameter.Builder.create(this, "logoutUrl")
.parameterName(createParameterName(
applicationEnvironment,
PARAMETER_USER_POOL_LOGOUT_URL))
.stringValue(this.logoutUrl)
.build();
StringParameter.Builder.create(this, "providerUrl")
.parameterName(createParameterName(
applicationEnvironment,
PARAMETER_USER_POOL_PROVIDER_URL))
.stringValue(this.userPool.getUserPoolProviderUrl())
.build();
AWS genera valores aleatorios tanto para userPoolId como para userPool-
ClientId. Necesitamos hacer accesibles ambos ID más adelante para recuperar
el secreto del cliente OAuth 2.
https://stratospheric.auth.eu-central-1.amazoncognito.com/logout
https://cognito-idp.eu-central-1.amazonaws.com/eu-central-1_pD8flsXa
this.userPoolClientSecret = this.userPoolClient.getUserPoolClientSecret().unsafeUnwr\
ap();
StringParameter userPoolClientSecret =
StringParameter.Builder.create(this, "userPoolClientSecret")
.parameterName(createParameterName(
applicationEnvironment,
PARAMETER_USER_POOL_CLIENT_SECRET))
.stringValue(this.userPoolClientSecret)
.build();
Como nuestra aplicación Spring Boot espera estos valores al inicio de la aplica-
ción, tenemos que desplegar o actualizar la pila de Cognito antes de iniciar un
nuevo despliegue de ECS de nuestra aplicación Todo.
Finalmente, ampliamos nuestro package.json con dos scripts para crear, ac-
tualizar, o destruir la aplicación Cognito CDK de forma cómoda:
Por sí sola, esta encriptación no tiene mucho valor, ya que pasaremos todos los
secretos como variables de entorno en texto plano al contenedor Docker con
nuestra aplicación Spring Boot. Eso significa que estarán disponibles en texto
plano durante la ejecución del programa. Además, todas las variables de entorno
se muestran en texto plano en la consola web de ECS.
Spring AWS integra el Gestor de Secretos de AWS para hacer disponibles los
10. Construyendo Registro de Usuarios e Inicio de Sesión con Amazon Cognito 209
Spring Security ya soporta tanto OAuth 2.0 como OpenID Connect 1.0 de serie.
Dado que estamos basándonos en un protocolo estándar de la industria, hay
un procedimiento común que ocurre detrás de escena que Spring Security
implementa para nosotros.
Los componentes de seguridad relevantes son parte de los siguientes dos Spring
Boot starters:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
}
Al invocar este endpoint (a través de una solicitud HTTP GET) para nuestro pool
de usuarios de Cognito obtenemos el siguiente resultado:
curl -v https://cognito-idp.eu-central-1.amazonaws.com/eu-
central-1_pXOUNokLO/.well-known/openid-configuration
{
"authorization_endpoint": "https://dev101...amazoncognito.com/oauth2/authorize",
"id_token_signing_alg_values_supported": [
"RS256"
],
"issuer": "https://...amazonaws.com/eu-central-1_pXOUNokLO",
"jwks_uri": "https:/...amazonaws.com/eu-central-1_pXOUNokLO/.well-known/jwks.json",
"response_types_supported": [
"code",
"token"
],
"scopes_supported": [
"openid",
"email",
"phone",
"profile"
],
"subject_types_supported": [
"public"
],
"token_endpoint": "https://...amazoncognito.com/oauth2/token",
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post"
10. Construyendo Registro de Usuarios e Inicio de Sesión con Amazon Cognito 211
],
"userinfo_endpoint": "https://...amazoncognito.com/oauth2/userInfo"
}
spring:
security:
oauth2:
client:
registration:
cognito:
clientId: ${COGNITO_CLIENT_ID}
clientSecret: ${COGNITO_CLIENT_SECRET}
scope: openid, profile, email
clientName: stratospheric-client
provider:
cognito:
issuerUri: ${COGNITO_PROVIDER_URL}
Los marcadores de posición ${} serán sustituidos con variables de entorno que
determinamos al desplegar nuestra aplicación Todo con ECS (usando SpEL - el
potente lenguaje de expresión de Spring).
Un User Pool vacío es bueno para nuestra factura de AWS, pero no para la
reputación de nuestra aplicación. Es hora de llenarlo con usuarios reales de
nuestra aplicación Todo.
10. Construyendo Registro de Usuarios e Inicio de Sesión con Amazon Cognito 212
Al configurar el User Pool de Cognito, especificamos que solo los usuarios admi-
nistradores pueden agregar nuevos usuarios al pool.
dependencies {
implementation 'software.amazon.awssdk:cognitoidentityprovider'
}
@Configuration
public class AwsConfig {
@Bean
@ConditionalOnProperty(prefix = "custom",
name = "use-cognito-as-identity-provider", havingValue = "true")
public CognitoIdentityProviderClient cognitoIdentityProviderClient(
AwsRegionProvider regionProvider,
AwsCredentialsProvider awsCredentialsProvider) {
return CognitoIdentityProviderClient.builder()
.credentialsProvider(awsCredentialsProvider)
.region(regionProvider.getRegion())
.build();
}
}
@NotBlank
private String username;
@Email
private String email;
@ValidInvitationCode
private String invitationCode;
Estamos utilizando Bean Validation para validar los datos entrantes al enviar el
formulario. Un endpoint básico del controlador Spring MVC expone esta vista e
instancia un objeto Registration vacío para que el usuario lo llene:
10. Construyendo Registro de Usuarios e Inicio de Sesión con Amazon Cognito 214
@Controller
@RequestMapping("/register")
public class RegistrationController {
@GetMapping
public String getRegisterView(Model model) {
model.addAttribute("registration", new Registration());
return "register";
}
}
Para poder procesar esta solicitud, tenemos que agregar un nuevo mapeo de
punto final a nuestro RegistrationController:
10. Construyendo Registro de Usuarios e Inicio de Sesión con Amazon Cognito 217
@PostMapping
public String registerUser(
@Valid Registration registration,
BindingResult bindingResult,
Model model, RedirectAttributes redirectAttributes) {
if (bindingResult.hasErrors()) {
model.addAttribute("registration", registration);
return "register";
}
try {
registrationService.registerUser(registration);
redirectAttributes.addFlashAttribute("message",
"You successfully registered for the Todo App. " +
"Please check your email inbox for further instructions."
);
redirectAttributes.addFlashAttribute("messageType", "success");
return "redirect:/";
} catch (CognitoIdentityProviderException exception) {
model.addAttribute("registration", registration);
model.addAttribute("message", exception.getMessage());
model.addAttribute("messageType", "danger");
return "register";
}
}
Primero, nos aseguramos de que nuestro modelo cumpla con nuestras normas
de validación (por ejemplo, no se permite un correo electrónico o nombre de
usuario vacíos). Aunque también contamos con una validación básica del lado
del cliente, nunca sabemos cómo los clientes llegan a nuestro punto de acceso
y, por lo tanto, siempre debemos validar el payload.
@Service
@ConditionalOnProperty(prefix = "custom",
name = "use-cognito-as-identity-provider", havingValue = "true")
public class CognitoRegistrationService implements RegistrationService {
public CognitoRegistrationService(
CognitoIdentityProviderClient cognitoIdentityProviderClient,
@Value("${COGNITO_USER_POOL_ID}") String userPoolId) {
this.cognitoIdentityProviderClient = cognitoIdentityProviderClient;
this.userPooldId = userPoolId;
}
@Override
public void registerUser(Registration registration) {
AdminCreateUserRequest registrationRequest = new AdminCreateUserRequest()
.withUserPoolId(userPooldId)
.withUsername(registration.getUsername())
.withUserAttributes(
new AttributeType()
.withName("email")
.withValue(registration.getEmail()),
new AttributeType()
.withName("email_verified")
.withValue("true")
)
.withDesiredDeliveryMediums(DeliveryMediumType.EMAIL)
.withForceAliasCreation(Boolean.FALSE);
cognitoIdentityProviderClient.adminCreateUser(registrationRequest);
}
}
configuramos nuestra User Pool de Cognito para enviar una contraseña temporal
al buzón del usuario tras el registro.
Sin embargo, la vista previa para el proceso de registro debe estar disponible
sin ninguna autenticación. Además, la página de inicio de nuestra aplicación y
el endpoint /health tienen requisitos similares. Lo mismo ocurre con cualquier
recurso estático (JavaScript, CSS, etc.) que nuestra aplicación Spring Boot sirve
al navegador.
Cualquier otra parte de la aplicación debe estar protegida y solo ser accesible
para los usuarios que iniciaron sesión a través de OIDC y sus credenciales de
Cognito.
Spring Security nos permite definir nuestras propias reglas de seguridad pro-
porcionando un bean SecurityFilterChain. Podemos inyectar la instancia
de HttpSecurity de la aplicación para especificar nuestras restricciones de
seguridad utilizando un constructor en cadena:
10. Construyendo Registro de Usuarios e Inicio de Sesión con Amazon Cognito 221
@Configuration
public class WebSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity)
throws Exception {
httpSecurity
.csrf()
.and()
.oauth2Login()
.and()
.authorizeRequests()
.requestMatchers(PathRequest.toStaticResources().atCommonLocations())
.permitAll()
.requestMatchers("/", "/health", "/register").permitAll()
.anyRequest().authenticated();
return httpSecurity.build();
}
}
Una vez que el usuario ingresa las credenciales correctas, será redirigido de
nuevo a nuestra aplicación, y Spring Security creará una sesión válida para
este usuario. Técnicamente hablando, Spring Security crea un Principal en la
forma de un OidcUser. Entonces podemos inyectar ese Principal en nuestros
controladores de Spring MVC:
10. Construyendo Registro de Usuarios e Inicio de Sesión con Amazon Cognito 224
@GetMapping
public String getIndex(Model model, @AuthenticationPrincipal OidcUser user) {
// extract attributes from the user object
return "index";
}
Tener una aplicación “stateless” permite escalar y distribuir el tráfico sin nin-
gún problema. Desafortunadamente, dos detalles técnicos nos impiden tener
una aplicación Todo completamente “stateless”. Echemos un vistazo al prime-
ro.
https://authorization-server.com/oauth/authorize
?client_id=a17c21ed
&response_type=code
&state=5ca75bd30
&redirect_uri=https%3A%2F%2Fexample-app.com%2Fauth
&scope=openid
new Service(
serviceStack,
"Service",
awsEnvironment,
applicationEnvironment,
new Service.ServiceInputParameters(...)
.withStickySessionsEnabled(true)
);
Proceso de Deslogueo
Como nuestra aplicación de Spring Boot crea y almacena una sesión para cada
uno de nuestros usuarios, el primer paso es invalidar esta sesión una vez que el
10. Construyendo Registro de Usuarios e Inicio de Sesión con Amazon Cognito 228
usuario decide cerrar la sesión. Esto hará que el usuario actual sea desconocido
para nuestra aplicación. Posteriormente, todas las partes isAnonymous() de
nuestras vistas Thymeleaf se evaluarán como true.
Sin embargo, el usuario aún tiene una sesión activa en el proveedor de identidad.
Con cada intento subsiguiente de volver a iniciar sesión en nuestra aplicación,
los usuarios no tienen que proporcionar sus credenciales para Cognito (a menos
que la sesión expire) y se inician sesión automáticamente.
@Configuration
public class WebSecurityConfig {
public WebSecurityConfig(
ClientRegistrationRepository clientRegistrationRepository) {
this.clientRegistrationRepository = clientRegistrationRepository;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity)
throws Exception {
httpSecurity
.csrf()
.and()
.oauth2Login()
.and()
.authorizeRequests()
.requestMatchers(PathRequest
.toStaticResources()
.atCommonLocations())
.permitAll()
.requestMatchers("/", "/health", "/register")
.permitAll()
.anyRequest().authenticated()
.and()
.logout()
.logoutSuccessHandler(oidcLogoutSuccessHandler());
return httpSecurity.build();
}
}
Esto funciona bien para los proveedores de identidad que ya soportan este
10. Construyendo Registro de Usuarios e Inicio de Sesión con Amazon Cognito 230
borrador. La mala noticia es que Cognito no sigue esta especificación (aún). Sin
embargo, Cognito expone un endpoint que podemos usar para cerrar la sesión
de un usuario final. Para que esto funcione, tenemos que implementar nuestro
propio LogoutSuccessHandler.
public CognitoOidcLogoutSuccessHandler(
String logoutUrl,
String clientId) {
this.logoutUrl = logoutUrl;
this.clientId = clientId;
}
@Override
protected String determineTargetUrl(
HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) {
return UriComponentsBuilder
.fromUri(URI.create(logoutUrl))
10. Construyendo Registro de Usuarios e Inicio de Sesión con Amazon Cognito 231
.queryParam("client_id", clientId)
.queryParam("logout_uri", baseUrl)
.encode(StandardCharsets.UTF_8)
.build()
.toUriString();
}
}
@Configuration
public class LogoutSuccessHandlerConfig {
@Bean
@ConditionalOnProperty(prefix = "custom",
name = "use-cognito-as-identity-provider", havingValue = "true")
public CognitoIdentityProviderClient cognitoIdentityProviderClient(
AwsRegionProvider regionProvider,
AwsCredentialsProvider awsCredentialsProvider) {
return CognitoIdentityProviderClient.builder()
.credentialsProvider(awsCredentialsProvider)
.region(regionProvider.getRegion())
.build();
10. Construyendo Registro de Usuarios e Inicio de Sesión con Amazon Cognito 232
}
}
@Configuration
public class WebSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity)
throws Exception {
httpSecurity
// ...
.logout()
.logoutSuccessHandler(logoutSuccessHandler);
return httpSecurity.build();
}
}
Para iniciar y detener una instancia local de Keycloak usamos Docker Com-
pose. Como Keycloak es el único componente de infraestructura que nues-
tra aplicación Todo necesita en este punto del libro, nuestro archivo docker-
compose.yml es bastante directo:
version: '3.3'
services:
keycloak:
image: quay.io/keycloak/keycloak:18.0.0-legacy
ports:
- 8888:8080
environment:
- KEYCLOAK_USER=keycloak
- KEYCLOAK_PASSWORD=keycloak
- DB_VENDOR=h2
- JAVA_OPTS=\
-Dkeycloak.migration.action=import \
-Dkeycloak.migration.provider=singleFile \
-Dkeycloak.migration.file=/tmp/stratospheric-realm.json
volumes:
- ./src/test/resources/keycloak/stratospheric-realm.json:\
/tmp/stratospheric-realm.json
spring:
security:
oauth2:
client:
registration:
cognito:
clientId: spring-boot-application
clientSecret: 27b07baf-53ba-42c6-b11f-6384769cada3
scope: openid
provider:
cognito:
issuerUri: http://localhost:8888/auth/realms/stratospheric
@Service
@ConditionalOnProperty(prefix = "custom",
name = "use-cognito-as-identity-provider", havingValue = "false")
public class LocalRegistrationService implements RegistrationService {
@Override
public void registerUser(Registration registration) {
// no registration as we use a local Keycloak instance
// with a pre-defined set of users
}
}
Aún podemos acceder a la aplicación Todo utilizando uno de los usuarios prede-
finidos:
@Configuration
public class LogoutSuccessHandlerConfig {
@Bean
@ConditionalOnProperty(prefix = "custom", name = "use-cognito-as-identity-provider\
", havingValue = "false")
public LogoutSuccessHandler oidcLogoutSuccessHandler(ClientRegistrationRepository \
clientRegistrationRepository) {
OidcClientInitiatedLogoutSuccessHandler successHandler = new OidcClientInitiated\
LogoutSuccessHandler(clientRegistrationRepository);
successHandler.setPostLogoutRedirectUri("{baseUrl}");
return successHandler;
}
}
11. Conexión a una base de datos con
Amazon RDS
En cuanto queremos dar a nuestros usuarios un medio de mantener sus datos
a través de las sesiones, necesitamos algún tipo de almacenamiento de datos.
Nuestra aplicación de muestra Todo no es la excepción, ya que permitirá a los
usuarios crear, editar y compartir tareas.
Las aplicaciones web suelen utilizar sistemas de gestión de bases de datos relaciona-
les (SGBDR) para el almacenamiento de datos. Debido al lenguaje generalmente
utilizado para interactuar con ellos, estos sistemas de bases de datos también
se conocen más comúnmente como bases de datos SQL (Structured Query
Language).
Fue con la aparición de las aplicaciones Web 2.0 y el enfoque NoSQL promovido
en ese contexto que alternativas como almacenes de documentos o bases de
datos de objetos volvieron a ser ampliamente consideradas. Hoy en día, estas
bases de datos no relacionales a menudo se utilizan en conjunto con bases de
datos relacionales. En el capítulo Rastreo de acciones de usuario con DynamoDB
hablaremos más sobre las bases de datos NoSQL en el contexto de AWS.
Sin embargo, las bases de datos relacionales no van a desaparecer, ya que siguen
siendo útiles para operaciones comunes de CRUD (Create, Read, Update, Delete)
11. Conexión a una base de datos con Amazon RDS 238
o para datos de series temporales, por ejemplo. Por lo tanto, utilizaremos una
base de datos de este tipo para nuestra aplicación de muestra Todo.
Pero antes de profundizar en los detalles de cómo usar la base de datos desde
nuestra aplicación Spring Boot, veamos qué ofrece Amazon RDS y cómo pode-
mos automatizar la provisión de una base de datos para nuestra aplicación.
RDS nos permite crear y administrar bases de datos relacionales en AWS utili-
zando sus herramientas y técnicas habituales como AWS CLI, IAM y CDK.
Como es común con los servicios de AWS, RDS proporciona una infraestructura
escalable que se adapta a las necesidades de nuestra aplicación y nuestros
usuarios. El entorno subyacente está completamente gestionado por AWS, lo
que nos libera de la carga del mantenimiento.
Además, RDS soporta SSL para encriptar datos en tránsito y con el Servicio de
Gestión de Claves de AWS (KMS) podemos encriptar nuestros datos en reposo
también. RDS se integra con AWS Config para proporcionarnos un medio de
garantizar el cumplimiento registrando y auditando cambios en las configura-
ciones de la base de datos.
A estas alturas, asumimos que los conceptos básicos de IAM ya han sido configu-
rados como se describe en el capítulo Manejando Permisos con IAM. Para acceder
a los recursos de RDS durante el desarrollo, simplemente necesitamos adjuntar
la política AmazonRDSFullAccess gestionada por AWS a aquellos grupos de IAM
en los que se encuentran nuestros desarrolladores de aplicaciones.
Este acceso es necesario para desplegar una nueva base de datos o hacer cambios
a una instancia de base de datos existente a través de la Consola de AWS o a
través de la aplicación CDK que vamos a construir para ese propósito. Una vez
que la base de datos está desplegada y lista para usar, es mejor eliminar el acceso
completo de nuevo, al menos para entornos similares a la producción.
Como hicimos antes para los recursos de Cognito, ahora crearemos una aplica-
ción CDK que nos permita desplegar una base de datos con un simple comando
de CLI.
código en GitHub.
// ...
public PostgresDatabase(
final Construct scope,
final String id,
final Environment awsEnvironment,
final ApplicationEnvironment applicationEnvironment,
final DatabaseInputParameters databaseInputParameters) {
// ...
Network.NetworkOutputParameters networkOutputParameters =
Network.getOutputParametersFromParameterStore(
this,
applicationEnvironment.getEnvironmentName());
// ...
}
}
Repasemos el código del PostgresDatabase para ver qué está haciendo con
todos estos parámetros.
Más tarde pasaremos tanto el nombre del grupo de subred como el ID del grupo
de seguridad al constructo CfnDBInstance que crea nuestra instancia de base
de datos.
{
"username": "<value of DBUserName parameter>",
"password": "<generated password>"
}
Adjuntar el Secreto
CfnSecretTargetAttachment.Builder.create(this, "secretTargetAttachment")
.secretId(databaseSecret.getSecretArn())
.targetId(dbInstance.getRef())
.targetType("AWS::RDS::DBInstance")
.build();
Esto vincula el secreto con la base de datos, así podemos aprovechar la función
de rotación de secretos proporcionada por el AWS Secrets Manager.
Parámetros de Salida
StringParameter endpointAddress =
StringParameter.Builder.create(this, "endpointAddress")
.parameterName(createParameterName(
this.applicationEnvironment,
PARAMETER_ENDPOINT_ADDRESS))
.stringValue(this.dbInstance.getAttrEndpointAddress())
.build();
StringParameter endpointPort =
StringParameter.Builder.create(this, "endpointPort")
.parameterName(createParameterName(
this.applicationEnvironment,
PARAMETER_ENDPOINT_PORT))
.stringValue(this.dbInstance.getAttrEndpointPort())
.build();
StringParameter databaseName =
StringParameter.Builder.create(this, "databaseName")
.parameterName(createParameterName(
this.applicationEnvironment,
PARAMETER_DATABASE_NAME))
.stringValue(this.dbInstance.getDbName())
.build();
11. Conexión a una base de datos con Amazon RDS 246
StringParameter securityGroupId =
StringParameter.Builder.create(this, "securityGroupId")
.parameterName(createParameterName(
this.applicationEnvironment,
PARAMETER_SECURITY_GROUP_ID))
.stringValue(this.databaseSecurityGroup.getAttrGroupId())
.build();
StringParameter secret =
StringParameter.Builder.create(this, "secret")
.parameterName(createParameterName(
this.applicationEnvironment,
PARAMETER_SECRET_ARN))
.stringValue(this.databaseSecret.getSecretArn())
.build();
new PostgresDatabase(
databaseStack,
"Database",
awsEnvironment,
applicationEnvironment,
11. Conexión a una base de datos con Amazon RDS 248
new PostgresDatabase.DatabaseInputParameters());
app.synth();
}
// ...
}
Para interactuar con el nuevo stack de la base de datos, añadimos los scripts
database:deploy y database:destroy al archivo package.json en nuestro
proyecto CDK.
Ahora podemos ejecutar estos scripts desde la línea de comandos para imple-
mentar un stack de la base de datos:
Lo que queda es informar a nuestra aplicación Spring Boot que utilice la nueva
base de datos. Para este propósito, modificamos el stack de servicios existente
que es responsable de implementar el contenedor Docker donde reside nuestra
aplicación. Agregamos las variables de entorno por defecto que Spring Boot
utiliza para definir la conexión a la base de datos:
11. Conexión a una base de datos con Amazon RDS 249
• SPRING_DATASOURCE_URL,
• SPRING_DATASOURCE_USERNAME, y
• SPRING_DATASOURCE_PASSWORD.
Spring Boot automáticamente lee estas variables de entorno y crea una fuente
de datos apuntando a nuestra nueva base de datos.
PostgresDatabase.DatabaseOutputParameters databaseOutputParameters =
PostgresDatabase.getOutputParametersFromParameterStore(
parametersStack,
applicationEnvironment);
// ...
vars.put("SPRING_DATASOURCE_URL",
String.format("jdbc:postgresql://%s:%s/%s",
databaseOutputParameters.getEndpointAddress(),
databaseOutputParameters.getEndpointPort(),
databaseOutputParameters.getDbName()));
vars.put("SPRING_DATASOURCE_USERNAME",
databaseSecret.secretValueFromJson("username").toString());
vars.put("SPRING_DATASOURCE_PASSWORD",
databaseSecret.secretValueFromJson("password").toString());
// ...
return vars;
}
// ...
}
jdbc:postgresql://<EndpointAddress>:<EndpointPort>/<DBName>
Las APIs de bases de datos como JPA y los marcos ORM (object-relational
mapping) como Hibernate pueden generar o modificar un esquema de base de
datos automáticamente al iniciar la aplicación. La ventaja de este enfoque es
que la estructura de la base de datos generada corresponderá automáticamente
a nuestras clases de modelo y código de aplicación en general. Por lo tanto,
no necesitamos mantener manualmente scripts SQL separados en este caso.
11. Conexión a una base de datos con Amazon RDS 253
Spring Boot se integra con ambas herramientas. Elegimos Flyway por su sim-
plicidad y por ser el enfoque predeterminado de facto para las aplicaciones
Spring Boot. En lugar de un lenguaje basado en XML para definir migraciones,
como el que utiliza Liquibase, Flyway utiliza scripts SQL simples para aplicar
migraciones. Aunque no es tan agnóstico en RDBMS, este enfoque es mucho
más simple ya que no incurre en la complejidad adicional que implica aprender
otro idioma para mantener nuestra aplicación.
Sin embargo, sujeto a tus requisitos, Liquibase aún puede ser una elección válida.
Por ejemplo, dependiendo del entorno, es posible que desees admitir múltiples
RDBMS diferentes como PostgreSQL o Oracle Database sin tener que mantener
11. Conexión a una base de datos con Amazon RDS 254
scripts de migración individuales para cada dialecto SQL que viene con esos. En
ese caso, debido a su enfoque más agnóstico, Liquibase podría tener sus méritos.
dependencies {
// ...
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'org.postgresql:postgresql'
// ...
}
Ahora, una vez que hemos iniciado el conjunto de servicios, nuestra aplicación
Spring Boot se conectará automáticamente a la base de datos PostgreSQL uti-
11. Conexión a una base de datos con Amazon RDS 255
Dado que hemos decidido usar Flyway para este propósito, agregamos esta
dependencia al archivo build.gradle:
dependencies {
// ...
implementation 'org.flywaydb:flyway-core'
// ...
}
Cualquier archivo .sql que coloquemos en esa carpeta que siga las conven-
ciones de nomenclatura de Flyway se ejecutará automáticamente al iniciar la
aplicación.
Si ya estás familiarizado con el uso de JPA en una aplicación Spring Boot, puedes
omitir esta sección sin problema.
contienen los artefactos de código relacionados con cada una de esas caracte-
rísticas. Estos paquetes incluyen controladores, interfaces de servicio (y sus
implementaciones), repositorios de Spring Data JPA y clases de modelo de
datos.
Nos centraremos en la clase Todo y cómo se utiliza. Esta clase está anotada con
la anotación @Entity del paquete jakarta.persistence (la API proporcionada
por JPA / Jakarta Persistence). Esta anotación marca la clase como una entidad
de base de datos. Por defecto, el nombre de clase no calificado (en lugar de su
nombre completamente calificado, que incluiría el nombre del paquete) se utiliza
como el nombre de la tabla de la base de datos de la entidad.
-- ...
-- ...
Esta declaración creará la tabla de base de datos necesaria con las columnas
requeridas para almacenar la información de cada “todo”. Al examinar más de
cerca la clase Todo, podemos ver que estas columnas se reflejan en los atributos
de la clase:
// ...
@Entity
public class Todo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank
@Size(max = 30)
private String title;
@Size(max = 100)
private String description;
@NotNull
11. Conexión a una base de datos con Amazon RDS 259
@Future
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate dueDate;
@Enumerated(EnumType.STRING)
private Status status;
@ManyToOne
@JoinColumn(name = "owner_id")
private Person owner;
// ...
}
-- ...
-- ...
Restricciones y Validación
@Override
List<T> findAll();
@Override
List<T> findAll(Sort sort);
@Override
List<T> findAllById(Iterable<ID> ids);
@Override
<S extends T> List<S> saveAll(Iterable<S> entities);
// ...
}
Para utilizar esto en una entidad de base de datos específica, como nuestra clase
Todo, debemos ampliar esta interfaz con nuestra propia interfaz TodoReposi-
tory:
// ...
@Service
public class TodoService {
public TodoService(
TodoRepository todoRepository,
PersonRepository personRepository) {
this.todoRepository = todoRepository;
this.personRepository = personRepository;
}
return todoRepository.save(todo);
}
}
Aquí es donde entra en juego una instancia de base de datos local. Podemos
usar Docker para iniciar una instancia añadiendo un servicio al archivo docker-
compose.yml ubicado en el directorio raíz de nuestra aplicación:
services:
postgres:
image: postgres:12.9
ports:
- 5432:5432
environment:
- POSTGRES_USER=stratospheric
- POSTGRES_PASSWORD=stratospheric
- POSTGRES_DB=stratospheric
# ...
Lo único que tenemos que hacer ahora es añadir estas líneas al archivo de
propiedades application-dev.yml que se encuentra en la carpeta src/main/-
resources de nuestra aplicación:
11. Conexión a una base de datos con Amazon RDS 264
spring:
datasource:
url: jdbc:postgresql://localhost:5432/stratospheric
username: stratospheric
password: stratospheric
# ...
Dado que nuestro archivo build.gradle ya configura Gradle para usar dev
como Perfil de Spring (utilizando -Dspring.profiles.active=dev como un
argumento JVM), estos ajustes serán detectados automáticamente cuando se
ejecute ./gradlew bootrun:
// ...
bootRun {
jvmArgs = [
"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005",
"-Dspring.profiles.active=dev",
]
}
12. Compartiendo Tareas con Amazon
SQS y Amazon SES
Con este capítulo, implementaremos la próxima funcionalidad para nuestra
aplicación de tareas e integraremos dos nuevos servicios de AWS: SQS (Simple
Queue Service) y SES (Simple Email Service).
Hasta el momento, los usuarios de nuestra aplicación solo trabajan en sus tareas
por sí mismos. Dado que esto puede resultar monótono, nos gustaría fomentar
alguna colaboración entre nuestra base de usuarios.
Antes de integrar Amazon SQS con nuestra aplicación Spring Boot, veamos
cómo funciona este servicio de AWS.
Los mensajes que procesamos con SQS pueden tener una carga útil con un
tamaño máximo de 256 KB. Permite el cifrado del lado del servidor de nuestros
mensajes, y podemos controlar el acceso a él. Interactuamos con SQS a través
de una API HTTPS genérica, similar a otros servicios de AWS. Dependiendo de
la configuración, SQS puede guardar nuestro mensaje durante un “período de
retención” de hasta 14 días.
Con el tipo de cola Estándar, los mensajes se entregarán con un esfuerzo por
12. Compartiendo Tareas con Amazon SQS y Amazon SES 267
mantener el orden. SQS no garantiza un orden estricto de los mensajes para este
tipo de cola, y ocasionalmente los mensajes pueden entregarse fuera de orden.
Además, los mensajes se entregan al menos una vez. Nuestro receptor podría
recibir el mismo mensaje más de una vez. A pesar de tener estos inconvenientes,
este tipo de cola ofrece una capacidad de procesamiento casi ilimitada (según la
documentación de AWS). También es la opción más económica.
Las operaciones básicas que realizamos contra la API de SQS son SendMessa-
ge, ReceiveMessage y DeleteMessage. Cada mensaje permanecerá en la cola
hasta que un consumidor lo elimine activamente de la cola. En otras palabras,
Amazon SQS no elimina automáticamente un mensaje cuando se consume, pero
el consumidor tiene que reconocer el procesamiento exitoso del mensaje al
eliminarlo.
Para evitar recibir el mismo mensaje no procesable una y otra vez (por ejemplo
debido a una carga útil inválida o a un tiempo de inactividad de un sistema
remoto), Amazon SQS admite el concepto de las llamadas Colas de Letras
Muertas (DLQ por sus siglas en inglés).
Una cola de letras muertas es simplemente otra cola de Amazon SQS que
almacena mensajes no procesables. El tipo de esta cola tiene que coincidir con el
tipo de la cola de origen. En otras palabras, una cola FIFO solo puede tener como
objetivo una cola FIFO como una cola de letras muertas. Las DLQ son opcionales,
por lo que podemos definir colas de Amazon SQS sin especificar una DLQ.
Sin embargo, es una buena práctica general crear una DLQ para cada una de
nuestras colas de procesamiento por varias razones:
12. Compartiendo Tareas con Amazon SQS y Amazon SES 269
La estrategia para manejar estos mensajes muertos depende mucho del caso
de uso y del escenario de error. En caso de que haya habido un contratiempo
temporal (por ejemplo, un sistema remoto del que dependemos no estaba
disponible), podemos mover los mensajes de vuelta a nuestra cola de origen
para su procesamiento.
Tanto Amazon SQS como AWS SNS son altamente escalables y no requieren
ninguna configuración ya que AWS los gestiona completamente.
Para nuestro caso de uso, la cola estándar de Amazon SQS se ajusta a nuestro
propósito. No necesitamos un orden estricto de mensajes porque realmente
no importa qué correo electrónico enviemos primero. Además, no nos importa
mucho si recibimos un mensaje dos veces, porque podríamos almacenar en la
base de datos a quién ya le enviamos un correo electrónico, para que el mismo
usuario no reciba el correo electrónico dos veces.
Crearemos una nueva aplicación CDK para las partes relacionadas con la men-
sajería de nuestra aplicación. Puedes encontrar el código de esta aplicación en
GitHub. Para esta MessagingApp no necesitamos ningún parámetro de entrada
adicional excepto los predeterminados:
12. Compartiendo Tareas con Amazon SQS y Amazon SES 271
// omitted standard configuration values like the AWS region and sanity checks
new MessagingStack(
app,
"messaging",
awsEnvironment,
applicationEnvironment);
app.synth();
}
}
Como el módulo CDK de Amazon SQS viene con constructos de nivel 2 estables,
podemos crear nuestros recursos de Amazon SQS de manera conveniente y no
tenemos que trabajar con constructos de CloudFormation de bajo nivel. Dentro
de MessagingStack, creamos tanto nuestras colas de procesamiento como las
de dead-letter de Amazon SQS:
12. Compartiendo Tareas con Amazon SQS y Amazon SES 272
Luego, tenemos que revelar el nombre de la cola SQS con la que se conectará
nuestra aplicación:
StringParameter.Builder.create(this, "todoSharingQueueName")
.parameterName(createParameterName(
applicationEnvironment,
PARAMETER_TODO_SHARING_QUEUE_NAME))
.stringValue(this.todoSharingQueue.getQueueName())
.build();
.withTaskRolePolicyStatements(List.of(
// ... skipping other PolicyStatements,
PolicyStatement.Builder.create()
.sid("AllowSQSAccess")
.effect(Effect.ALLOW)
.resources(List.of(
String.format("arn:aws:sqs:%s:%s:%s", region, accountId,
messagingOutputParameters.getTodoSharingQueueName())
))
.actions(Arrays.asList(
"sqs:DeleteMessage",
"sqs:GetQueueUrl",
"sqs:ListDeadLetterSourceQueues",
"sqs:ListQueues",
"sqs:ListQueueTags",
"sqs:ReceiveMessage",
"sqs:SendMessage",
"sqs:ChangeMessageVisibility",
"sqs:GetQueueAttributes"))
.build()
))
Esto otorga a nuestra aplicación los permisos necesarios para enviar, recibir
y eliminar mensajes para cualquier cola de Amazon SQS, puesto que hemos
utilizado el comodín *. Ahora estamos en condiciones de empezar a integrar
Amazon SQS en nuestra aplicación Todo.
Como primer paso, debemos añadir una nueva dependencia a nuestro proyecto:
12. Compartiendo Tareas con Amazon SQS y Amazon SES 275
dependencies {
// omitted other dependencies
implementation 'io.awspring.cloud:spring-cloud-starter-aws-messaging'
}
Para preparar nuestra aplicación de Spring Boot para SQS, solo queda un paso
por dar. El SqsTemplate que utilizaremos para acceder cómodamente a Amazon
SQS no viene auto-configurado por defecto. En lugar de esta abstracción y para
evitar cualquier interacción con el AWS Java SDK de bajo nivel, añadimos un
bean de tipo SqsTemplate a nuestro contexto de aplicación:
12. Compartiendo Tareas con Amazon SQS y Amazon SES 276
@Configuration
public class MessagingTemplateConfig {
@Bean
public SqsTemplate sqsTemplate(
SqsAsyncClient sqsAsyncClient) {
return SqsTemplate.newTemplate(sqsAsyncClient);
}
}
Comencemos con el primer paso y enviemos un mensaje a SQS cada vez que un
usuario decida invitar a un colaborador a uno de sus todos.
Creamos este elemento como parte de la <table> que visualiza cada todo dentro
de la vista dashboard.html:
@Controller
@RequestMapping("/todo")
public class TodoCollaborationController {
public TodoCollaborationController(
TodoCollaborationService todoCollaborationService) {
this.todoCollaborationService = todoCollaborationService;
}
@PostMapping("/{todoId}/collaborations/{collaboratorId}")
public String shareTodoWithCollaborator(
@PathVariable("todoId") Long todoId,
@PathVariable("collaboratorId") Long collaboratorId,
RedirectAttributes redirectAttributes
) {
return "redirect:/dashboard";
}
}
@Service
@Transactional
public class TodoCollaborationService {
todo.getCollaborationRequests().add(collaboration);
todoCollaborationRequestRepository.save(collaboration);
sqsTemplate.send(todoSharingQueueName,
new TodoCollaborationNotification(collaboration));
return collaborator.getName();
}
}
necesaria para enviar un correo electrónico. Esto desacopla aún más la función
de envío de correos electrónicos del resto de nuestra aplicación. Sin embargo,
hay una desventaja en esta solución: la persona invitada podría ver información
desactualizada sobre la tarea, ya que el propietario puede actualizar la tarea
entre el envío de la solicitud de colaboración y la espera de la confirmación. Sin
embargo, decidimos que esto es aceptable.
Como último paso, usamos el SqsTemplate para enviar un mensaje a una cola
SQS especificada. El método send() serializará el objeto Java a una cadena JSON
antes de enviarlo a SQS.
Además, la cola SQS entre estas dos operaciones actúa como un búfer. Esto nos
da un mecanismo de reintento implícito porque, con nuestro maxReceiveCount,
intentamos procesar el mensaje hasta cuatro veces. Como estamos construyen-
do un sistema distribuido y tenemos que integrarnos con un proveedor de la
nube, prácticamente cualquier cosa puede fallar. Por lo tanto, deberíamos estar
preparados para el fracaso. Además de esto, podemos controlar el rendimiento
del envío de correos electrónicos limitando el número de mensajes que consu-
mimos. Esto ayuda a mantenerse dentro de los límites de posibles límites de
envío de correo electrónico.
@Component
public class TodoSharingListener {
@SqsListener(value = "${custom.sharing-queue}")
public void listenToSharingMessages(TodoCollaborationNotification payload) {
}
}
Como parte del argumento del método del receptor, podemos definir el objeto
Java requerido. Spring Cloud AWS, y en este caso particular su integración
con Spring Messaging, es responsable de extraer la carga útil del mensaje y
resolverla utilizando un PayloadMethodArgumentResolver. Por defecto, esto
hace uso del MappingJackson2MessageConverter para deserializar el mensaje
JSON entrante (de tipo String) a un objeto Java utilizando el ObjectMapper de
Jackson.
El SDK Java SQS de AWS proporciona un método de bajo nivel para este propósi-
to: deleteMessage(). Este método espera el identificador de recibo del mensaje.
Este identificador puede tener hasta 1024 caracteres de longitud e identifica un
evento de consumición de mensaje en particular.
12. Compartiendo Tareas con Amazon SQS y Amazon SES 283
Spring Cloud AWS proporciona una abstracción sobre esto para que no tenga-
mos que hacer un seguimiento de todos los identificadores de recibo y gestionar
la eliminación de mensajes por nuestra cuenta. Para los receptores SQS basados
en anotaciones, podemos especificar una política de eliminación. Si estamos
utilizando el método receive() de la SqsTemplate, el mensaje se eliminará
inmediatamente.
Spring Cloud AWS define las siguientes cuatro políticas de eliminación como
parte del conjunto SqsMessageDeletionPolicy:
Dado que, con nuestra cola de mensajes no entregados, hemos configurado una
política de reintento para nuestra configuración SQS, la política de eliminación
NO_REDRIVE predeterminada es suficiente. Siempre que nuestro procesamiento
de mensajes finalice sin una excepción, Spring Cloud AWS eliminará el men-
saje. Si nuestro procesamiento de mensajes lanza una excepción, el mensaje
permanecerá dentro de la cola, e intentaremos procesar nuevamente el mensaje
después de que el visibilityTimeout haya transcurrido, hasta que llegue a la
cola de mensajes no entregados después de demasiados intentos fallidos.
Sin embargo, veamos cómo sería una política de eliminación de NEVER. Podemos
configurar la deletionPolicy por receptor SQS como parte de la anotación
@SqsListener:
12. Compartiendo Tareas con Amazon SQS y Amazon SES 284
// perform processing
if(successfulProcessing) {
acknowledgment.acknowledge();
}
Con esta política de eliminación, Spring Cloud AWS nunca borrará automáti-
camente un mensaje para nosotros. Podemos inyectar un objeto Acknowled-
gement y tener el control total sobre cuándo confirmar (es decir, borrar) el
mensaje.
Además, podemos obtener las cabeceras de un mensaje SQS si éstas son rele-
vantes para nuestro procesamiento de mensajes:
@SqsListener(value = "${custom.sharing-queue}")
public void listenToSharingMessages(
TodoCollaborationNotification payload,
@Headers Map<String, Object> headers) {
aquí y cómo podemos integrar este servicio específico de AWS con nuestro
backend de Spring Boot en la siguiente sección.
Podemos interactuar con Amazon SES a través del SDK de AWS, la interfaz SMTP
de Amazon SES o la API de Amazon SES directamente.
Los posibles casos de uso para Amazon SES van desde correos electrónicos tran-
saccionales clásicos (como confirmaciones de registro) hasta correspondencia
de correo electrónico de marketing y boletines informativos. Se nos facturará
en función del número de correos electrónicos que enviamos y recibimos. No
hay costos iniciales por licencias o recursos de servidor.
Además, Amazon SES está disponible en varias regiones. Cada cuenta de AWS
tiene acceso al entorno de pruebas de Amazon SES en las regiones disponibles
por defecto. Aprenderemos más sobre este acceso al entorno de pruebas en la
12. Compartiendo Tareas con Amazon SQS y Amazon SES 286
siguiente sección.
Amazon SES viene con cuotas de envío de correo electrónico para prevenir el
fraude y mantener una buena reputación de la dirección IP. Estas cuotas definen
cuántos correos electrónicos podemos enviar por día y el número máximo de
correos electrónicos que podemos enviar por segundo. Podemos configurar esas
cuotas por región poniéndonos en contacto con el Centro de Soporte de AWS.
.withTaskRolePolicyStatements(List.of(
// ... existings policy statements
PolicyStatement.Builder.create()
.sid("AllowSendingEmails")
.effect(Effect.ALLOW)
.resources(
List.of(String.format(
"arn:aws:ses:%s:%s:identity/stratospheric.dev", region, accountId))
)
.actions(List.of(
"ses:SendEmail",
"ses:SendRawEmail"
))
.build()
))
Cuando trabajas con Amazon SES, es crucial entender las implicaciones del
acceso a sandbox. Como su nombre indica, viene con varias restricciones:
El acceso a sandbox de Amazon SES al que estamos asignados por defecto limita
el uso para producción. No solo las cuotas de envío son bajas sino que también
tenemos que verificar cada dirección de correo electrónico antes de siquiera
contactar al usuario por primera vez. Esto impide la operación sin problemas.
Es por eso que debemos solicitar acceso a producción tan pronto como nuestra
aplicación esté a punto de salir en vivo por primera vez. Solicitar acceso a
producción es un esfuerzo manual único a través de un ticket de soporte.
Para obtener una guía paso a paso actualizada y visual sobre cómo solicitar
acceso a producción, echa un vistazo a la documentación de Amazon SES.
Dado que la consola de Amazon SES está actualmente en transición de v1
a v2, las capturas de pantalla podrían quedar obsoletas bastante pronto.
número de correos electrónicos por día). Este paso es opcional y se puede lograr
con un ticket de soporte adicional.
Verificando un Dominio
Para enviar correos electrónicos con Amazon SES, debemos verificar cada
identidad (nombre de dominio) que vamos a usar como parte de “De”,
“Fuente”, “Remitente” o “Ruta de retorno” para evitar el uso no autorizado.
Nuestro objetivo es enviar correos electrónicos de confirmación desde
noreply@stratospheric.dev para implementar nuestra función de compartir
tareas pendientes.
Para verificar que somos los propietarios del dominio, necesitamos actualizar
la configuración DNS de nuestro dominio. Al final del proceso de creación de
la identidad de dominio, podemos descargar un archivo .csv con los cambios
requeridos en el conjunto de registros.
Tanto el marco Spring como Spring Cloud AWS ofrecen un excelente soporte
para enviar correos electrónicos desde nuestro backend de Java. Spring define
dos interfaces centrales para hacerlo: MailSender y JavaMailSender. Java-
MailSender hereda de MailSender y añade soporte para enviar mensajes MIME
por correo electrónico.
dependencies {
implementation 'io.awspring.cloud:spring-cloud-aws-starter-ses'
}
Se nos auto-configura una instancia del cliente SDK de Amazon SES SesClient.
Podemos interactuar directamente con el cliente SDK o usar el ya menciona-
do MailSender o JavaMailSender, para los cuales Spring Cloud AWS auto-
configura una implementación, que a su vez utiliza Amazon SES internamente.
@Component
public class TodoSharingListener {
mailSender.send(message);
}
}
Como hemos confirmado la identidad del dominio para nuestro dominio stra-
tospheric.dev, podemos utilizar noreply@stratospheric.dev como el re-
mitente del correo electrónico. Con el acceso de producción de Amazon SES
habilitado, también podemos enviar correos electrónicos a cualquier destino sin
tener que confirmar cada objetivo manualmente.
Una vez que la persona invitada recibe nuestro correo electrónico y hace clic en
12. Compartiendo Tareas con Amazon SQS y Amazon SES 294
Aceptación de Confirmaciones
<domain>/todo/{todoId}/collaborations/{collaboratorId}/confirm?token={token}
@Controller
@RequestMapping("/todo")
public class TodoCollaborationController {
@GetMapping("/{todoId}/collaborations/{collaboratorId}/confirm")
public String confirmCollaboration(
@PathVariable("todoId") Long todoId,
@PathVariable("collaboratorId") Long collaboratorId,
@RequestParam("token") String token,
@AuthenticationPrincipal OidcUser user,
RedirectAttributes redirectAttributes
) {
if(todoCollaborationService
.confirmCollaboration(user.getEmail(), todoId, collaboratorId, token)) {
12. Compartiendo Tareas con Amazon SQS y Amazon SES 295
redirectAttributes.addFlashAttribute("message",
"You've confirmed that you'd like to collaborate on this todo.");
redirectAttributes.addFlashAttribute("messageType", "success");
}else {
redirectAttributes.addFlashAttribute("message",
"Invalid collaboration request.");
redirectAttributes.addFlashAttribute("messageType", "danger");
}
return "redirect:/dashboard";
}
}
if (!collaborator.getId().equals(collaboratorId)) {
return false;
}
if (collaborationRequest == null ||
!collaborationRequest.getToken().equals(token)) {
return false;
}
todo.addCollaborator(collaborator);
collaborationRequestRepository.delete(collaborationRequest);
return true;
}
}
@Entity
public class Todo {
@ManyToMany
@JoinTable(name = "todo_collaboration",
joinColumns = @JoinColumn(name = "todo_id"),
inverseJoinColumns = @JoinColumn(name = "collaborator_id")
)
private List<Person> collaborators = new ArrayList<>();
Una vez que la persona invitada confirma con éxito la colaboración, informare-
mos al propietario del todo con una notificación push. Vamos a implementar
esta característica en el próximo capítulo Notificaciones Push con Amazon MQ.
version: '3.3'
services:
# ... existing Docker container definitions
localstack:
image: localstack/localstack:0.14.4
ports:
- 4566:4566
environment:
- SERVICES=sqs,ses
- DEFAULT_REGION=eu-central-1
- USE_SINGLE_REGION=true
volumes:
- ./src/test/resources/localstack/local-aws-infrastructure.sh:\
/docker-entrypoint-initaws.d/init.sh
Para inicializar y configurar nuestra nube AWS local, montamos un script dentro
del contenedor Docker (local-aws-infrastructure.sh). LocalStack ejecutará
cualquier script dentro de la carpeta docker-entrypoint-initaws.d durante
la inicialización del contenedor. Como parte de nuestro script de inicialización,
12. Compartiendo Tareas con Amazon SQS y Amazon SES 299
#!/bin/sh
spring:
cloud:
aws:
endpoint: http://localhost:4566
region:
static: eu-central-1
credentials:
secret-key: foo
access-key: bar
Con esta configuración, los clientes del SDK de AWS autoconfigurados se dirigen
a LocalStack.
@Component
public class TodoSharingListener {
// ...
public TodoSharingListener(
@Value("${custom.auto-confirm-collaborations}")
boolean autoConfirmCollaborations) {
this.autoConfirmCollaborations = autoConfirmCollaborations;
}
// ...
if (autoConfirmCollaborations) {
LOG.info("Auto-confirm collaboration request");
todoCollaborationService.confirmCollaboration(
payload.getCollaboratorEmail(), payload.getTodoId(),
payload.getCollaboratorId(), payload.getToken());
}
}
}
Aquí es donde entran en juego las notificaciones push. En este capítulo, explora-
remos las notificaciones push, crearemos la infraestructura de AWS requerida
e implementaremos en nuestra aplicación Spring Boot.
Para tener en cuenta tanto la escalabilidad como una gestión más fácil de los
suscriptores, los intermediarios de mensajes como Apache ActiveMQ generali-
zan esta idea siguiendo el patrón de publicación-suscripción. Este patrón nos
permite tener una relación n:m entre publicadores y suscriptores con múltiples
publicadores publicando mensajes a un tema o canal y múltiples suscriptores
recibiendo mensajes de ese tema:
Un intermediario de mensajes conecta a los publicadores con los suscriptores a través de temas
a los que un suscriptor puede suscribirse.
Aún así, WebSocket no solo cumple con nuestras necesidades para nuestro
caso de uso particular, sino que también ofrece un mejor rendimiento y menor
latencia que HTTP.
En el capítulo Compartiendo Tareas con SQS y SES hemos visto cómo podemos
comunicarnos y colaborar con otros usuarios con Amazon Simple Queue Service
(SQS) y Amazon Simple Email Service (SES).
Hasta ahora, sin embargo, esa comunicación ha sido solo de una vía: permitimos
a un usuario compartir sus tareas con otros usuarios pero el dueño de la tarea
aún no recibe una notificación en caso de que sus colaboradores hayan avanzado
en una tarea compartida. Para enmendar esa situación, implementaremos una
función de notificación que informará al usuario de dichas actualizaciones.
Emplearemos un servicio de AWS para ese servicio con estado. Dentro del
ecosistema de AWS, hay bastantes servicios que nos permiten implementar
13. Notificaciones Push con Amazon MQ 306
Amazon Pinpoint
Nuestra aplicación es una aplicación web, y esto nos impide usar Pinpoint
para nuestros propósitos. Además, Pinpoint tiene mucho más que ofrecer en
términos de comunicaciones de marketing y segmentación de usuarios, lo cual
es irrelevante para nuestro caso de uso en el mejor de los casos y añade una
13. Notificaciones Push con Amazon MQ 307
“Amazon IoT Core te permite conectar dispositivos IoT a la nube de AWS sin
la necesidad de aprovisionar o administrar servidores.”
Aunque Amazon IoT está claramente orientado al caso de uso de permitir que
los objetos físicos se comuniquen entre sí en el Internet de las Cosas, uno de los
protocolos utilizados para hacerlo es de hecho WebSocket.
Amazon SNS
SNS solo nos permite enviar mensajes a estos tipos de puntos finales de suscrip-
tor:
• HTTP/HTTPS
• correo electrónico
• Amazon Kinesis Data Firehose
• Amazon SQS
• una función personalizada de AWS Lambda
• un punto final de aplicación de plataforma
• SMS
SNS en primer lugar: ¿Por qué usar un servicio si realmente no nos beneficiamos
de hacerlo?
Por lo tanto, SNS, lamentablemente, tampoco es una opción viable para nuestro
escenario.
Amazon SQS
¿Y qué hay de SQS? Después de todo, ya hemos estado usándolo para compartir
tareas. Entonces, ¿por qué no usarlo también para comunicar el estado de una
tarea?
De nuevo, esto suena como algo que podría prestarse a nuestro caso. Como
hemos visto cuando implementamos nuestra función de compartir tareas, un
suscriptor puede escuchar una cola SQS y enviar un correo electrónico para cada
mensaje entrante.
Esto podría usarse para informar a los suscriptores sobre las actualizaciones
entrantes de sus tareas también.
Amazon MQ
Por lo tanto, es el único que cumple con nuestros requisitos específicos, por lo
que utilizaremos Amazon MQ para implementar nuestra función de “notifica-
ciones push”.
Habiendo elegido Amazon MQ, la próxima decisión a tomar es entre las dos
opciones de corredor de mensajes disponibles: Apache ActiveMQ, o RabbitMQ.
Sin embargo, ActiveMQ no solo admite de forma nativa los protocolos Web-
Socket y STOMP, sino también la API del Servicio de Mensajes de Java (JMS) y
los protocolos MQTT y AMQP. Por lo tanto, parece ser la solución más versátil,
especialmente en un entorno basado en Java. Si surge la necesidad de conectar
directamente el corredor de mensajes a nuestra aplicación Spring Boot a través
de JMS, podríamos hacerlo sin cambiar la tecnología de corredor de mensajes
que subyace.
// ...
public ActiveMqStack(
final Construct scope,
final String id,
final Environment awsEnvironment,
final ApplicationEnvironment applicationEnvironment,
final String username
) {
// ...
this.applicationEnvironment = applicationEnvironment;
this.username = username;
this.password = generatePassword();
// ...
}
// ...
}
Primero, necesitamos crear una lista de usuarios que necesitan tener acceso a
nuestra instancia de ActiveMQ:
esto no resulta ser una buena idea en términos de seguridad y uso de recursos ya
que potencialmente expondría nuestra cola de mensajes al mundo, haciéndola
vulnerable a ataques DDoS.
// ...
}
13. Notificaciones Push con Amazon MQ 314
Recopilamos los parámetros que hemos creado hasta ahora y los proporciona-
mos como parámetros de entrada a la clase CfnBroker.Builder de CDK para
crear nuestro ActiveMQ broker:
13. Notificaciones Push con Amazon MQ 315
Dado que por ahora queremos ahorrar costos, elegimos el tipo de instancia más
pequeño y económico posible aquí con mq.t2.micro.
Al haber optado por utilizar ActiveMQ, definimos ACTIVEMQ como nuestro tipo
de motor de ejecución. Utilizaremos 5.16.2 como la versión del motor ActiveMQ,
que hasta el momento de escritura, es la última versión soportada por Amazon
MQ.
StringParameter.Builder.create(this, PARAMETER_USERNAME)
.parameterName(createParameterName(
applicationEnvironment,
PARAMETER_USERNAME))
.stringValue(username)
.build();
StringParameter.Builder.create(this, PARAMETER_PASSWORD)
.parameterName(createParameterName(
applicationEnvironment,
PARAMETER_PASSWORD))
.stringValue(password)
.build();
StringParameter.Builder.create(this, PARAMETER_STOMP_ENDPOINT)
.parameterName(createParameterName(
applicationEnvironment,
PARAMETER_STOMP_ENDPOINT))
.stringValue(Fn.select(0, this.broker.getAttrStompEndpoints()))
.build();
StringParameter.Builder.create(this, PARAMETER_SECURITY_GROUP_ID)
.parameterName(createParameterName(
applicationEnvironment,
PARAMETER_SECURITY_GROUP_ID))
.stringValue(this.securityGroupId)
.build();
Lo único que queda por hacer es crear una instancia de nuestro ActiveMqStack
y hacer que CDK sintetice una pila de CloudFormation a partir de esa pila:
// ...
new ActiveMqStack(
app,
"activeMq",
awsEnvironment,
applicationEnvironment,
username);
app.synth();
}
// ...
}
Para configurar nuestra aplicación para acceder al broker, tenemos que modi-
ficar un poco nuestro ServiceApp. Primero, necesitamos proporcionar acceso
a nivel de red desde el contenedor administrado por ECS al broker. Para esto,
añadimos el grupo de seguridad del broker a la lista de grupos desde los cuales
se permite el ingreso:
13. Notificaciones Push con Amazon MQ 319
// ...
// ...
vars.put(
"WEB_SOCKET_RELAY_ENDPOINT",
activeMqOutputParameters.getStompEndpoint());
vars.put(
13. Notificaciones Push con Amazon MQ 320
"WEB_SOCKET_RELAY_USERNAME",
activeMqOutputParameters.getActiveMqUsername());
vars.put(
"WEB_SOCKET_RELAY_PASSWORD",
activeMqOutputParameters.getActiveMqPassword());
// ...
}
}
De esta manera, la aplicación Spring Boot puede leer esas variables de entorno
y conectarse al broker de mensajes.
Primero, sin embargo, daremos un vistazo rápido a los protocolos que vamos a
utilizar para implementar esta función.
Una vez que se ha establecido una conexión WebSocket, el servidor puede enviar
actualizaciones al cliente siempre que ocurran. Este modelo ha demostrado ser
mucho más eficiente que los modelos anteriores de sondeo y sondeo prolonga-
do.
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.springframework.boot:spring-boot-starter-activemq'
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
public WebSocketConfig(
@Value("${custom.web-socket-relay-endpoint:#{null}}")
String websocketRelayEndpoint,
@Value("${custom.web-socket-relay-username:#{null}}")
String websocketUsername,
@Value("${custom.web-socket-relay-password:#{null}}")
String websocketPassword,
@Value("${custom.web-socket-relay-use-ssl:#{false}}")
boolean websocketUseSsl
) {
this.websocketEndpoint = Endpoint.fromEndpointString(websocketRelayEndpoint);
this.websocketUsername = websocketUsername;
this.websocketPassword = websocketPassword;
this.websocketUseSsl = websocketUseSsl;
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry
13. Notificaciones Push con Amazon MQ 323
.enableStompBrokerRelay("/topic")
.setAutoStartup(true)
.setClientLogin(this.websocketUsername)
.setClientPasscode(this.websocketPassword)
.setSystemLogin(this.websocketUsername)
.setSystemPasscode(this.websocketPassword);
}
// ...
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry
.addEndpoint("/websocket")
.withSockJS();
}
// ...
}
custom:
# ...
web-socket-relay-endpoint: ${WEB_SOCKET_RELAY_ENDPOINT}
web-socket-relay-username: ${WEB_SOCKET_RELAY_USERNAME}
web-socket-relay-password: ${WEB_SOCKET_RELAY_PASSWORD}
@Configuration
public class WebSecurityConfig {
// ...
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity)
throws Exception {
httpSecurity
.csrf()
.ignoringRequestMatchers(
"/stratospheric-todo-updates/**",
"/websocket/**"
)
.and()
// ...
return httpSecurity.build();
}
}
Idealmente, eso habría sido todo lo que teníamos que hacer en el lado del ser-
vidor para proporcionar funcionalidad WebSocket y STOMP a través de nuestra
aplicación Spring Boot.
Dado que las instancias de Amazon MQ solo están disponibles -y con toda razón-
mediante conexiones encriptadas con SSL y nombres de dominio en lugar de di-
recciones IP (lo cual es prácticamente un requisito para usar SSL), tenemos que
recurrir a la creación de nuestro cliente TCP basado en el ReactorNettyTcpClient
de Reactor:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
// ...
// ...
}
13. Notificaciones Push con Amazon MQ 326
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
// ...
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
ReactorNettyTcpClient<byte[]> customTcpClient =
this.websocketUseSsl ?
getCustomTcpClientWithSSLSupport() :
getCustomTcpClientWithoutSSLSupport();
registry
.enableStompBrokerRelay("/topic")
//...
.setTcpClient(customTcpClient);
}
}
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
// ...
private ReactorNettyTcpClient<byte[]>
createRoundRobinTcpClient(Endpoint endpoint) {
final List<InetSocketAddress> addressList = new ArrayList<>();
// ...
implementation 'org.webjars:sockjs-client:1.1.2'
implementation 'org.webjars:stomp-websocket:2.3.3-1'
<head>
<meta charset="UTF-8">
<title layout:title-pattern="$CONTENT_TITLE | $LAYOUT_TITLE">
Todo Application
</title>
<!-- ... --->
<script th:src="@{/js/todo-updates.js}" sec:authorize="isAuthenticated()"></script>
</head>
<body>
<div>
<script sec:authorize="isAuthenticated()">
connectToWebSocketEndpoint('[[${#authentication.principal.attributes.email}]]');
</script>
</div>
</body>
</html>
En caso de que el usuario haya iniciado sesión (ya que en nuestro caso de
uso solo tiene sentido que los usuarios autenticados reciban mensajes sobre el
progreso de sus tareas pendientes), llamamos a la función connectToWebSoc-
ketEndpoint() de ese archivo JavaScript con la dirección de correo electrónico
del usuario autenticado como argumento:
<script sec:authorize="isAuthenticated()">
connectToWebSocketEndpoint('[[${#authentication.principal.attributes.email}]]');
</script>
function connectToWebSocketEndpoint(email) {
const socket = new SockJS('/websocket');
stompClient = Stomp.over(socket);
stompClient.connect({}, () => {
stompClient.subscribe('/topic/todoUpdates', function (message) {
$('#message').html(message.body);
$('#toast').toast('show');
});
if (email) {
stompClient.subscribe('/topic/todoUpdates/' + email, function (message) {
13. Notificaciones Push con Amazon MQ 330
$('#message').html(message.body);
$('#toast').toast('show');
});
}
});
}
function disconnectFromWebSocketEndpoint() {
if (stompClient !== null) {
stompClient.disconnect();
}
}
$(document).ready(function () {
$('#toast').toast({delay: 5000});
});
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org">
<head>
</head>
<body>
<div th:remove="tag" th:fragment="toast">
<div
id="toast"
class="toast"
role="alert"
aria-live="assertive"
aria-atomic="true">
<div class="toast-header">
<strong class="mr-auto">Collaboration confirmed.</strong>
<button
type="button"
class="ml-2 mb-1 close"
data-dismiss="toast"
aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div id="message" class="toast-body">
Message
</div>
</div>
</div>
</body>
</html>
@Service
@Transactional
public class TodoCollaborationService {
// ...
public TodoCollaborationService(
@Value("${custom.sharing-queue}") String todoSharingQueueName,
TodoRepository todoRepository,
PersonRepository personRepository,
TodoCollaborationRequestRepository todoCollaborationRequestRepository,
SqsTemplate sqsTemplate,
SimpMessagingTemplate simpMessagingTemplate) {
this.todoRepository = todoRepository;
this.personRepository = personRepository;
this.todoCollaborationRequestRepository = todoCollaborationRequestRepository;
this.sqsTemplate sqsTemplate;
this.todoSharingQueueName = todoSharingQueueName;
this.simpMessagingTemplate = simpMessagingTemplate;
}
// ...
TodoCollaborationRequest collaborationRequest =
collaborationRequestRepository
.findByTodoIdAndCollaboratorId(todoId, collaboratorId);
if (collaborationRequest != null
&& collaborationRequest.getToken().equals(token)) {
// existing logic for confirming the collaboration
// ...
simpMessagingTemplate.convertAndSend(
"/topic/todoUpdates/" + ownerEmail, subject + " " + message);
return true;
}
return false;
}
}
Cuando un usuario que recibió una solicitud de colaboración por correo electró-
nico confirma esa solicitud haciendo clic en el enlace de ese correo electrónico,
el método confirmCollaboration() de nuestro TodoCollaborationService
será invocado desde el TodoCollaborationController:
@Controller
@RequestMapping("/todo")
public class TodoCollaborationController {
public TodoCollaborationController(
TodoCollaborationService todoCollaborationService) {
this.todoCollaborationService = todoCollaborationService;
}
// ...
@GetMapping("/{todoId}/collaborations/{collaboratorId}/confirm")
public String confirmCollaboration(
@PathVariable("todoId") Long todoId,
13. Notificaciones Push con Amazon MQ 335
return "redirect:/dashboard";
}
}
Toast message
13. Notificaciones Push con Amazon MQ 336
Para un proceso de desarrollo fluido, aún falta un pequeño detalle: una instancia
de ActiveMQ funcionando localmente, de modo que no tengamos que conectar-
nos a una instancia remota, lo cual sería complicado y propenso a errores.
version: '3.3'
services:
# ...
activemq:
image: stratospheric/activemq-docker-image
ports:
- 5672:5672
- 61613:61613
- 61614:61614
- 61616:61616
# ...
custom:
# ...
web-socket-relay-endpoint: localhost:61613
web-socket-relay-username: admin
web-socket-relay-password: admin
web-socket-relay-use-ssl: false
Sin embargo, con el auge de las aplicaciones Web 2.0 y un renovado interés en las
bases de datos no relacionales desde alrededor de 2010, en estos días las bases
de datos relacionales a menudo van acompañadas y complementadas con las
llamadas bases de datos NoSQL.
Aunque a primera vista el término parece abogar contra el uso de bases de datos
SQL, simplemente se refiere a bases de datos “no SQL” o “no relacionales” que
a menudo se usan en conjunto con bases de datos relacionales más tradicionales
para satisfacer necesidades específicas de gestión de datos.
Las bases de datos NoSQL vienen en una variedad de sabores, tales como:
14. Rastreando las Acciones del Usuario con Amazon DynamoDB 339
Estos son bastante diferentes entre sí, tanto en términos de los casos de uso
que soportan como en lo que respecta a su implementación. Casi lo único que
tienen en común es que no utilizan SQL como lenguaje de consulta o conceptos
relacionales para gestionar y normalizar datos.
Los procesos que generan una gran cantidad de eventos a alta frecuencia son
casos de uso ideales para DynamoDB. Un caso de uso en el contexto de nuestra
aplicación de ejemplo Todo sería rastrear el recorrido del usuario a través de
nuestra aplicación, por ejemplo, para generar datos para optimizar la experien-
cia del usuario. Al saber qué enlaces se clickean más frecuentemente y qué ca-
racterísticas se utilizan más a menudo, podemos tomar decisiones informadas
sobre qué partes de nuestra aplicación optimizar en lugar de hacer suposiciones
sobre qué áreas de nuestra aplicación merecen primero nuestra atención.
Hay una advertencia importante con respecto a esta característica, sin embargo:
Dependiendo de la información personal identificable que almacene sobre los
usuarios individuales (por ejemplo, las direcciones IP de los usuarios) esto
podría violar su privacidad y leyes y regulaciones como la Regulación General
de Protección de Datos (GDPR). Por lo tanto, tanto por el bien de sus usuarios
como por el suyo propio, es posible que desee tener precaución al aplicar efecti-
vamente una función de rastreo del usuario en una aplicación de producción.
Para reflexionar sobre por qué vamos a usar una base de datos NoSQL, demos un
paso atrás y consideremos qué características esperamos que tengan nuestras
bases de datos. Hay dos conceptos clave (y acrónimos útiles derivados de estos)
en ese sentido: El teorema CAP y ACID.
Teorema CAP
ACID
Así, mientras los RDBMS se centran en que los datos sean consistentes y estén
disponibles, los sistemas de bases de datos NoSQL enfatizan la disponibilidad
de datos y la tolerancia a fallos.
• Atomicidad: Cada transacción se trata como una sola unidad. O tiene éxito
en su totalidad o no tiene éxito en absoluto.
• Consistencia: Cada transacción lleva una base de datos de un estado válido
a otro.
• Aislamiento: Las transacciones concurrentes producen el mismo resultado
global como si se hubieran ejecutado en secuencia.
• Durabilidad: Una vez que se ha comprometido una transacción, se man-
tendrá el estado resultante de ella incluso después de un fallo posterior del
sistema.
Por lo tanto, debido a que los sistemas de bases de datos NoSQL solo garantizan
la disponibilidad de datos y la tolerancia a fallos, renuncian al estricto requisito
de consistencia de ACID a cambio de disponibilidad, rendimiento y escalabili-
dad.
Aunque con los sistemas de bases de datos NoSQL las transacciones suelen ser
aún atómicas, duraderas y, hasta cierto punto y dependiendo de la implemen-
14. Rastreando las Acciones del Usuario con Amazon DynamoDB 344
Esta característica a veces se resume con el acrónimo BASE, que se deriva del
opuesto químico en el mundo real de un ácido. Este acrónimo a su vez representa
estas propiedades de un sistema de bases de datos:
Escenarios
Dado que los sistemas de bases de datos NoSQL ofrecen tolerancia al particiona-
miento, nos permiten no solo escalar fácilmente (es decir, hacerlo más rápido
agregando más RAM o un procesador más rápido a una sola instancia) sino
14. Rastreando las Acciones del Usuario con Amazon DynamoDB 345
Beneficios de DynamoDB
Ahora que hemos abordado consideraciones más generales para cuándo optar
por un sistema de bases de datos NoSQL en lugar de uno RDBMS, es hora de
exponer los argumentos a favor del uso específico de DynamoDB.
Estas características son más adecuadas para escenarios donde estamos tra-
tando con un gran número de eventos cuya cantidad y/o frecuencia no se
conoce de antemano. El control de acceso detallado y un tiempo de vida para
cada ítem nos permiten automatizar procesos y comportamientos sin tener
que implementarlos nosotros mismos o ahogarnos en datos de transmisión de
eventos que no podríamos manejar manualmente.
Por lo tanto, utilizaremos DynamoDB para los datos de los eventos, aunque
para nuestro caso de almacenamiento de flujos de usuarios en una aplicación
14. Rastreando las Acciones del Usuario con Amazon DynamoDB 347
de muestra con solo unos pocos usuarios, admitimos que realmente no nece-
sitamos DynamoDB y el rico conjunto de características y características de
rendimiento que proporciona.
Terminología de DynamoDB
DynamoDB utiliza tablas para almacenar ítems. Cada uno de estos ítems puede
tener varios atributos escalares o anidados. Aparte de los atributos clave, las
tablas de DynamoDB son sin esquema predefinido, lo que significa que cada
ítem puede tener un conjunto diferente de atributos.
Una tabla de DynamoDB tiene un nombre único en una cuenta y región de AWS.
Cada tabla requiere un atributo de clave de partición, que sirve como clave
primaria para identificar ítems individuales en una tabla.
Para escalabilidad, DynamoDB almacena los ítems de una tabla en muchas par-
ticiones. Demasiadas operaciones de lectura o escritura en una sola partición en
un corto plazo pueden causar estrangulamiento - este efecto también se llama
partición caliente. Para evitar particiones calientes, las claves de partición de
los ítems en una tabla deben estar tan distribuidas como sea posible.
bién podemos definir atributos de índice secundario. Estos nos permiten acce-
der a nuestros datos por atributos clave alternativos.
Por ejemplo, una consideración clave es qué atributos deberían tener en co-
mún los ítems en una tabla particular desde el principio. Si decidimos agregar
atributos a cada ítem ya existente más adelante, esto podría resultar en una
gran cantidad de operaciones de escritura costosas, dependiendo del número
de ítems en nuestra tabla en ese momento.
Decidir qué atributos usar como claves para buscar e identificar ítems en nuestra
14. Rastreando las Acciones del Usuario con Amazon DynamoDB 349
Por tanto, al trabajar con bases de datos NoSQL, buscamos diseñar nuestro es-
quema de datos de tal manera que podamos ejecutar las consultas y operaciones
más comunes de la forma más eficiente posible, a costa de la normalización de
los datos y la flexibilidad.
Si deseas saber más sobre las mejores prácticas para diseñar esquemas
DynamoDB, echa un vistazo a estos recursos: Charla de AWS re:Invent sobre
modelado de datos con Amazon DynamoDB, El Libro de DynamoDB, Diseño
NoSQL para DynamoDB, Mejores prácticas para diseñar y arquitecturar con
DynamoDB,
14. Rastreando las Acciones del Usuario con Amazon DynamoDB 350
Siguiendo la organización por funcionalidad, las nuevas clases de Java para esta
función están ubicadas en dev.stratospheric.todoapp.tracing.
// ...
new BreadcrumbsDynamoDbTable(
dynamoDbStack,
"BreadcrumbTable",
applicationEnvironment,
new BreadcrumbsDynamoDbTable.InputParameter("breadcrumb")
);
14. Rastreando las Acciones del Usuario con Amazon DynamoDB 351
app.synth();
}
}
Esta aplicación CDK crea una nueva pila llamada DynamoDbStack. Como parte de
esta pila, encargamos la creación de una tabla para almacenar nuestras trazas.
public BreadcrumbsDynamoDbTable(
final Construct scope,
final String id,
final ApplicationEnvironment applicationEnvironment,
final InputParameter inputParameters
) {
super(scope, id);
new Table(
this,
"BreadcrumbsDynamoDbTable",
TableProps.builder()
.tableName(applicationEnvironment.prefix(inputParameters.tableName))
.partitionKey(
Attribute.builder().type(AttributeType.STRING).name("id").build())
.encryption(TableEncryption.AWS_MANAGED)
.billingMode(BillingMode.PROVISIONED)
.readCapacity(10)
.writeCapacity(10)
.removalPolicy(RemovalPolicy.DESTROY)
.build());
14. Rastreando las Acciones del Usuario con Amazon DynamoDB 352
La clase Table es una construcción de nivel 2 de AWS CDK que abstrae las
propiedades fundamentales de CloudFormation. Nuestra construcción perso-
nalizada toma el nombre de la tabla como parámetro de entrada y prefija el
nombre del entorno y de la aplicación, por ejemplo, production-todo-app-
breadcrumb. Esto nos permite crear la tabla para múltiples entornos y reutilizar
la construcción para diferentes aplicaciones.
Conectando a DynamoDB
Primero, añadimos el Spring Cloud AWS DynamoDB starter como una depen-
dencia al archivo build.gradle de nuestra aplicación:
implementation 'io.awspring.cloud:spring-cloud-aws-starter-dynamodb'
Aunque estas son funciones muy útiles, especialmente para casos de uso más
complejos, nuestro caso de uso es bastante simple. No necesitamos ninguna de
estas funciones, por eso evitamos la complejidad de añadir otra dependencia
aquí.
Para almacenar datos en nuestra nueva tabla DynamoDB desde nuestra aplica-
ción Java, debemos mapear dicha tabla a una clase modelo. Por lo tanto, creamos
esta nueva clase Breadcrumb:
14. Rastreando las Acciones del Usuario con Amazon DynamoDB 354
@DynamoDbBean
public class Breadcrumb {
@DynamoDbPartitionKey
public String getId() {
return id;
}
La anotación @DynamoDbBean del SDK de DynamoDB marca una clase como una
representación de una tabla de DynamoDB.
@Configuration
public class AmazonDynamoDBConfig {
@Bean
public DynamoDbTableNameResolver
dynamoDbTableNameResolver(Environment environment) {
spring:
application:
name: todo-app
custom:
environment: production
public TracingEvent(
Object source,
String uri,
String username
) {
super(source);
this.uri = uri;
this.username = username;
}
// getters
// ...
}
@Component
public class TraceDao {
@Async
@EventListener(TracingEvent.class)
public Breadcrumb create(TracingEvent tracingEvent) {
Breadcrumb breadcrumb = new Breadcrumb();
// ...
dynamoDbTemplate.save(breadcrumb);
return breadcrumb;
}
}
@EnableAsync
@Configuration
public class AsyncConfig {
@Bean
@Primary
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setMaxPoolSize(10);
executor.initialize();
return executor;
}
}
@Controller
public class IndexController {
// ...
@GetMapping
@RequestMapping("/")
public String getIndex(Principal principal) {
this.eventPublisher.publishEvent(
new TracingEvent(
this,
"index",
principal != null
? principal.getName()
: "anonymous"
)
);
return "index";
}
}
Para que el rol asumido por nuestra aplicación de ejemplo Todo pueda crear
nuevas tablas de DynamoDB y elementos en esas tablas, agregamos una nueva
PolicyStatement de IAM a la aplicación de servicio CDK ServiceApp:
// ...
new Service(
serviceStack,
"Service",
awsEnvironment,
applicationEnvironment,
new Service.ServiceInputParameters(
// ...
.withTaskRolePolicyStatements(List.of(
PolicyStatement.Builder.create()
.sid("AllowDynamoTableAccess")
.effect(Effect.ALLOW)
.resources(
List.of(String.format("arn:aws:dynamodb:%s:%s:table/%s",
region, accountId, applicationEnvironment.prefix("breadcrumb")))
)
.actions(List.of(
"dynamodb:Scan",
"dynamodb:Query",
"dynamodb:PutItem",
"dynamodb:GetItem",
"dynamodb:BatchWriteItem",
"dynamodb:BatchWriteGet"
))
.build()
))
// ...
);
// ...
}
}
14. Rastreando las Acciones del Usuario con Amazon DynamoDB 362
Con el próximo despliegue, esta política será añadida a las políticas IAM para
nuestra aplicación de ejemplo Todo, otorgándole los permisos necesarios.
Nuestra clave de partición username nos permite solicitar una lista de todos los
elementos Breadcrumb para un usuario específico, así como una lista de eventos
para un usuario dentro de un marco de tiempo específico:
14. Rastreando las Acciones del Usuario con Amazon DynamoDB 363
@Component
public class TraceDao {
// ...
return dynamoDbTemplate.query(
QueryEnhancedRequest
.builder()
.queryConditional(
QueryConditional.keyEqualTo(
Key
.builder()
.partitionValue(breadcrumb.getId())
.build()
)
)
.build(),
Breadcrumb.class
).items()
.stream()
.toList();
}
return dynamoDbTemplate.query(
QueryEnhancedRequest
.builder()
.queryConditional(
QueryConditional.keyEqualTo(
Key
.builder()
.partitionValue(breadcrumb.getId())
.build()
)
14. Rastreando las Acciones del Usuario con Amazon DynamoDB 364
)
.filterExpression(
Expression
.builder()
.expression("timestamp > :twoWeeksAgo")
.putExpressionValue(":twoWeeksAgo",
AttributeValue
.builder()
.s(twoWeeksAgo.toString())
.build()
)
.build()
)
.build(),
Breadcrumb.class
)
.items()
.stream()
.toList();
}
}
Queremos que nuestro entorno de desarrollo local sea lo más parecido posible
a la producción. Por ejemplo, nos gustaría probar nuestra nueva característica
antes de desplegarla en nuestro entorno de ensayo, y mucho menos en el
entorno de producción.
services:
# ...
localstack:
# ...
environment:
- SERVICES=sqs,ses,dynamodb
Lo que queda es crear la tabla como parte de la fase de arranque del contenedor
de LocalStack. Por lo tanto, extendemos nuestro script de inicio de LocalStack
con el siguiente comando:
#!/bin/sh
echo "Initialized."
Parte III: Preparación para la
Producción con AWS
Hemos aprendido cómo desplegar una aplicación de Spring Boot en AWS y cómo
hacer que se comunique con diferentes servicios de AWS para implementar
ciertos casos de uso. ¡Eso ya es bastante impresionante!
Sin embargo, eso no es suficiente. ¿Cómo sabemos que nuestra aplicación está
funcionando correctamente y haciendo lo que esperamos que haga? En la nube,
ningún equipo de administración de sistemas nos notificará cuando algo vaya
mal. Nosotros lo construimos, nosotros lo mantenemos.
Yo (Tom) recuerdo proyectos en los que teníamos que enviar una solicitud por
correo electrónico al equipo de administradores del sistema, diciéndoles que
necesitábamos los registros de un cierto intervalo de tiempo. Un administrador
del sistema se conectaría a las máquinas de producción y copiaría el archivo
de registro de cada servidor en un lugar seguro donde podríamos descargarlo a
través de SFTP. A veces, este proceso llevaría horas porque el administrador del
sistema estaba en un descanso. Afortunadamente, en la nube, este engorroso
proceso es cosa del pasado. ¡Solo imagina a los administradores del sistema de
AWS respondiendo a las solicitudes de archivos de registro de sus millones de
clientes en la nube!
Idealmente, los eventos de registro no solo contienen texto plano sino infor-
15. Registro Estructurado con Amazon CloudWatch 369
mación estructurada que podemos usar para filtrar eventos de registro. Esta
información estructurada puede contener el ID del usuario actual, por ejemplo,
para que podamos agrupar eventos de registro del mismo usuario.
Muchos servicios de AWS pueden enviar sus registros a CloudWatch, sin que
tengamos que configurar nada. Podemos hacer que RDS envíe registros de Post-
greSQL, por ejemplo. Si eligiéramos usar un proveedor de registro diferente, la
configuración podría volverse más complicada. En muchos casos, los registros
tienen que pasar por CloudWatch de todos modos y solo se envían al proveedor
de registro externo desde allí.
En este capítulo, nos enfocamos en los registros de aplicación, porque son los
más importantes en las operaciones diarias.
CfnTaskDefinition.ContainerDefinitionProperty container =
CfnTaskDefinition.ContainerDefinitionProperty.builder()
...
.logConfiguration(CfnTaskDefinition.LogConfigurationProperty.builder()
.logDriver("awslogs")
.options(Map.of(
"awslogs-group", logGroup.getLogGroupName(),
"awslogs-region", awsEnvironment.getRegion(),
"awslogs-stream-prefix", applicationEnvironment.prefix("stream"),
"awslogs-datetime-format", serviceInputParameters.awslogsDateTimeFormat))
.build())
...
.build();
bre del grupo de registros se verá algo así en nuestro ejemplo: prod-todo-app-
logs. Al crear el grupo de registros, podemos definir cuánto tiempo queremos
conservar los registros y una política de eliminación para decidir qué sucede si
el grupo de registros se elimina del proyecto CDK (con la política DESTROY, los
registros se eliminarán si se despliega una versión de la pila CDK que no incluya
el grupo de registros).
Una vez que desplegamos el constructo Service CDK con esta configuración,
los registros de nuestra aplicación se encontrarán disponibles en CloudWatch.
Sin embargo, aún no hemos configurado nuestra aplicación Spring Boot para
registrar logs en un formato específico. Además, en nuestro ServiceApp no
estamos pasando ninguna configuración de registro específica al constructo
15. Registro Estructurado con Amazon CloudWatch 374
Podemos seleccionar uno o más grupos de logs para realizar una consulta de
búsqueda, filtrarla por un marco de tiempo y ver los resultados en forma de texto
15. Registro Estructurado con Amazon CloudWatch 375
o gráfica.
Hasta ahora, todo va bien, pero los registros solo son valiosos si podemos buscar
en ellos palabras clave y patrones. Supongamos que queremos encontrar errores
en los registros. Podemos utilizar el comando filter para filtrar y mostrar solo
los eventos de registro que contienen la palabra “error”:
tráfico de registro, pero será muy molesto para aplicaciones con muchos even-
tos de registro.
Construir una consulta con una expresión regular puede ser bastante engorroso
y propenso a errores. Un carácter incorrecto en la expresión regular a menudo
significa que todos los campos estarán vacíos.
Además, ejecutar una expresión regular bastante compleja a través de los datos
de registro de una aplicación con un volumen de registro alto no parece ser una
buena idea. El equipo de CloudWatch probablemente ha incorporado algunas
optimizaciones ingeniosas, pero aún así … interpretar registros ‘al vuelo’ con
una expresión regular no puede ser bueno para el rendimiento.
Para que esto funcione con nuestra configuración, tenemos que configurar
nuestra aplicación Spring para emitir eventos de registro en JSON. Para hacer el
registro estructurado aún más útil, añadiremos un campo personalizado a estos
eventos de registro que luego será consultable a través de CloudWatch Insights.
15. Registro Estructurado con Amazon CloudWatch 379
hacemos es desplazarnos hasta el final del rastreo de pila para ver la causa
raíz de la excepción. Podemos reducir el esfuerzo de analizar rastreos de
pila agregando el nombre de la excepción de causa raíz como un campo en
el evento de registro. De esta manera, podemos filtrar los registros por una
causa raíz específica y ver qué impacto tuvo esa causa raíz.
Hay muchos más campos que podemos agregar para hacer que los eventos de
registro sean más valiosos con campos personalizados, muchos de ellos muy
específicos a la aplicación que estamos construyendo.
La abstracción de registro que las aplicaciones de Spring Boot utilizan por defec-
to es SLF4J. SLF4J soporta la noción de un “Contexto Diagnóstico de Mensajes”
(o MDC en corto) que nos permite agregar campos personalizados a los eventos
de registro.
@Override
public boolean preHandle(
final HttpServletRequest request,
final HttpServletResponse response,
final Object handler) {
Authentication authentication =
SecurityContextHolder.getContext().getAuthentication();
String userId = getUserIdFromPrincipal(authentication.getPrincipal());
MDC.put("userId", userId);
return true;
}
@Override
public void afterCompletion(
final HttpServletRequest request,
15. Registro Estructurado con Amazon CloudWatch 382
Este interceptor extrae el nombre de usuario del objeto Principal del usuario
actual, que puede ser una String o un OidcUser en nuestra aplicación. Luego
agrega el nombre de usuario al campo userId en el MDC. Si hay un error al
extraer el nombre de usuario, el interceptor establece el identificador de usuario
a “no conocido”.
Dado que el MDC está adjunto al thread actual, y dado que un thread podría ser
reutilizado más adelante (porque es parte de un pool de threads), tenemos que
eliminar el MDC una vez que se ha procesado la solicitud. Hacemos esto en el
método afterCompletion() de nuestro interceptor.
Lo único que queda por hacer es configurar Spring para utilizar nuestro Logging-
ContextInterceptor:
@Component
class LoggingContextConfiguration implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoggingContextInterceptor());
}
}
Con este código establecido, todos los eventos de registro que se emiten en
el camino del código que comienza con una solicitud web entrante ahora ten-
drán un campo userId. Pero la configuración de registro de Spring Boot no
registra campos personalizados por defecto, por lo que aún no hemos logrado
nada. Necesitamos configurar nuestra aplicación para generar registros JSON
estructurados para ver este campo.
implementation 'de.siegmar:logback-awslogs-json-encoder:1.1.0'
<configuration>
<springProfile name="dev">
<root level="INFO">
<appender-ref ref="PLAIN"/>
</root>
</springProfile>
<springProfile name="aws">
<root level="WARN">
<appender-ref ref="JSON"/>
</root>
</springProfile>
</configuration>
Podríamos, por supuesto, elegir también generar logs en JSON durante el desa-
rrollo local. Sin embargo, leer logs textuales es más fácil para nosotros los
humanos que leer JSON, y, al trabajar localmente, a menudo no disponemos de
un servidor de logs que analice estos registros por nosotros. Perder nuestros
campos personalizados en los logs textuales no es tan malo para el desarrollo
local, generalmente, ya que no estamos analizando incidentes en logs locales.
Pero si quisiéramos, también podríamos añadir algunos campos personaliza-
dos a nuestros logs textuales incluyendo %X{userId} en el patrón de log, por
ejemplo.
{
"timestamp":"2021-05-15T07:43:37.860000+1000",
"level":"INFO",
"logger":"dev.stratospheric.todoapp.todo.TodoController",
"thread":"http-nio-8080-exec-5",
"message":"successfully created todo",
"mdc":{
"userId":"duke"
}
}
Ahora que la aplicación Spring Boot genera registros en formato JSON, tenemos
que asegurarnos de que nuestro contenedor ECS los envíe correctamente a
15. Registro Estructurado con Amazon CloudWatch 386
CloudWatch.
CfnTaskDefinition.ContainerDefinitionProperty container =
CfnTaskDefinition.ContainerDefinitionProperty.builder()
...
.logConfiguration(CfnTaskDefinition.LogConfigurationProperty.builder()
.logDriver("awslogs")
.options(Map.of(
"awslogs-group", logGroup.getLogGroupName(),
"awslogs-region", awsEnvironment.getRegion(),
"awslogs-stream-prefix", applicationEnvironment.prefix("stream"),
"awslogs-datetime-format", serviceInputParameters.awslogsDateTimeFormat))
.build())
...
.build();
new Service(
serviceStack,
"Service",
awsEnvironment,
applicationEnvironment,
new Service.ServiceInputParameters(...)
.withAwsLogsDateTimeFormat("%Y-%m-%dT%H:%M:%S.%f%z")
...
.build()
Ahora, ¿qué podemos hacer con estos logs estructurados? Veamos cómo pode-
mos consultarlos.
Ahora que los logs están formateados en JSON, CloudWatch permitirá que cada
campo de ese JSON esté disponible en nuestras consultas de logs. Esto nos da
mucho poder para escribir consultas para responder a nuestras preguntas de
logs.
Además del campo del sistema @timestamp, ahora también podemos incluir los
campos level, logger, message (sin el @), y todos los demás campos disponibles
en los eventos de registro JSON en nuestras consultas. La consulta anterior de-
vuelve una tabla estructurada con el timestamp, el nivel de registro, el nombre
del logger y el mensaje de cada evento de registro. El resultado se filtra por los
campos level y mdc.userId para mostrarnos solo los eventos de registro que
nos interesan en este momento.
El campo mdc.userId también nos permite tener una idea de cuántos usuarios
activos tiene la aplicación cada día, lo que a menudo se utiliza como una métrica
de éxito para una aplicación:
filter level='ERROR'
| stats count_distinct(mdc.userId) as distinct_users by bin(24h)
Esta consulta mostrará una tabla con el número de eventos de registro en cada
nivel de registro. Es importante notar que cada vez que ejecutamos una consulta,
seleccionamos un intervalo de tiempo en la interfaz de CloudWatch Insights,
por lo que podemos decidir el intervalo de tiempo para el cual deseamos visua-
lizar la cuenta de los niveles de registro.
Ahora también podemos filtrar por el campo logger, para investigar los errores
que un cierto componente ha registrado:
Estos son solo algunos ejemplos de lo que podemos hacer con los logs estructu-
rados. Si agregamos más campos a nuestros eventos de log en JSON, como un
punto de entrada, una causa raíz en caso de una excepción, o cualquier campo
que sea significativo en el contexto de la aplicación, los logs se convierten en un
recurso de alto valor no solo para investigar errores sino también para obtener
métricas de uso y de negocio de nuestra aplicación.
Para categorizar aún más nuestras métricas, podemos agregar hasta diez di-
mensiones a cada punto de datos. Estas dimensiones son pares de clave/valor
y nos permiten etiquetar métricas. De esta manera, podemos crear múltiples
variaciones de la misma métrica para contar tanto las inscripciones exitosas
como fallidas de los usuarios, por ejemplo.
Amazon ECS
o un servicio en el clúster.
• MemoryUtilization: El porcentaje de memoria principal (RAM) utilizada
por el clúster o un servicio en el clúster.
AWS ELB
AWS diferencia entre los códigos de respuesta del ELB y del destino. El código de
respuesta del ELB es el código de respuesta que ELB devolvió al cliente y el código
de respuesta del destino es el código de respuesta que el destino subyacente
(en nuestro caso, nuestra aplicación Spring Boot) devolvió a ELB. En caso de
una configuración incorrecta del equilibrador de carga, las solicitudes entrantes
pueden no llegar a nuestra aplicación, y por lo tanto, no veremos ningún fallo
dentro de nuestros registros de la aplicación.
Además, con las métricas del ELB, podemos obtener una primera visión general
del número de solicitudes y los tiempos de respuesta medios para un periodo
determinado.
Amazon Cognito
Amazon SQS
Amazon RDS
Amazon DynamoDB
Amazon SES
Amazon MQ
Amazon S3
AWS Lambda
Configuración
Una introducción detallada a todas las características del Spring Boot Actuator
está fuera del alcance de este libro. Nos vamos a centrar en las partes relaciona-
das con las métricas.
Detrás de escena, el módulo Actuator utiliza Micrometer como una capa adicio-
nal de abstracción.
El proyecto Spring Cloud AWS ofrece un módulo para una integración perfecta
entre Actuator y Amazon CloudWatch que está alineada con la configuración de
AWS de nuestra aplicación.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-registry-cloudwatch'
}
Para evitar enviar métricas a AWS CloudWatch tanto para nuestras pruebas de
integración como cuando se ejecuta localmente, desactivamos la exportación en
nuestro archivo de configuración application-dev.yml:
16. Métricas con Amazon CloudWatch 404
management:
metrics:
export:
cloudwatch:
enabled: false
management:
metrics:
export:
cloudwatch:
enabled: true
namespace: stratospheric
step: 1m
tags:
environment: ${ENVIRONMENT_NAME}
Como alternativa a etiquetar las métricas con el nombre del entorno, también
podemos crear un espacio de nombres separado para cada entorno.
Falta una configuración final. Para enviar métricas desde nuestro back-end,
tenemos que agregar una nueva PolicyStatement a nuestro rol de tarea ECS
y permitir la acción PutMetricData de Amazon CloudWatch.
.withTaskRolePolicyStatements(
List.of(
// ... existings statements
PolicyStatement.Builder.create()
.effect(Effect.ALLOW)
.resources(singletonList("*"))
.actions(singletonList("cloudwatch:PutMetricData"))
.build()
)
Micrometer define tres tipos de métricas que demostramos como parte de este
capítulo: Counter, Timer, y Gauge.
@Service
public class CognitoRegistrationService implements RegistrationService {
nes de usuarios para nuestra aplicación Todo. Podemos lograr esto creando una
instancia de la clase Counter de Micrometer e incrementándolo cada vez que se
registra un nuevo usuario:
@Override
public void registerUser(Registration registration) {
awsCognitoIdentityProvider.adminCreateUser(registrationRequest);
successCounter.increment();
}
meterRegistry.counter(
"stratospheric.registration.users",
Tags.of("outcome", "success"))
.increment();
@Timed(
value = "stratospheric.collaboration.sharing",
description = "Measure the time how long it takes to share a todo"
)
@PostMapping("/{todoId}/collaborations/{collaboratorId}")
public String shareTodoWithCollaborator() {
// ...
}
stratospheric.collaboration.sharing.sum
stratospheric.collaboration.sharing.count
stratospheric.collaboration.sharing.avg
stratospheric.collaboration.sharing.max
Otro tipo de métrica que podemos crear con Micrometer es la denominada gauge.
Con los gauges, monitoreamos el valor actual de un componente. A diferencia
de un simple Counter, que aumenta monótonamente (como el número de
invocaciones de un método), un gauge representa el valor actual de algo que
puede aumentar y disminuir con el tiempo.
Para nosotros, no queda nada por hacer, ya que Micrometer se encarga de enviar
regularmente lotes de los diferentes puntos de datos a Amazon CloudWatch en
segundo plano. Dependiendo de nuestra configuración de step en Micrometer,
las métricas aparecerán en segundos o en cuestión de minutos dentro de Ama-
zon CloudWatch.
Una vez que las métricas se envían a Amazon CloudWatch, es hora de visuali-
zarlas.
Cada servicio de AWS que publica métricas en Amazon CloudWatch viene con un
tablero predefinido al que podemos acceder como parte de la consola de Amazon
CloudWatch:
Con fines demostrativos, vamos a crear un tablero mínimo que visualice algunas
métricas y nos brinde más información sobre nuestra aplicación.
Empecemos creando una nueva aplicación CDK que engloba toda la infraestruc-
tura de monitoreo.
new MonitoringStack(app,
"monitoring",
awsEnvironment,
applicationEnvironment);
app.synth();
}
}
Al igual que todas las demás aplicaciones CDK que hemos creado hasta ahora,
estamos añadiendo tanto monitoring:deploy como monitoring:destroy a
nuestro package.json para un mecanismo de sincronización eficaz como parte
de nuestro pipeline (flujo de trabajo) de CI/CD.
• TextWidget
• SingleValueWidget
• GraphWidget
• LogQueryWidget
• AlarmWidget
• AlarmStatusWidget
Vamos a empezar con el TextWidget. Con este widget, podemos mostrar infor-
mación basada en texto arbitrario utilizando la sintaxis Markdown:
Por defecto, cada tipo de widget viene con un height y width predefinidos.
Podemos sobrescribir ambos valores para definir manualmente el tamaño del
widget o para alinear el ancho de todos los widgets, por ejemplo. Al especificar
manualmente el ancho de nuestros widgets, debemos tener en cuenta que cada
fila tiene un ancho total de 24.
SingleValueWidget.Builder
.create()
.title("User Registrations")
.setPeriodToTimeRange(true)
.metrics(List.of(new Metric(MetricProps.builder()
.namespace("stratospheric")
.metricName("stratospheric.registration.signups.count")
.region(awsEnvironment.getRegion())
.statistic("sum")
.dimensionsMap(Map.of(
"outcome", "success",
"environment", applicationEnvironment.getEnvironmentName()
))
.build())))
.build()
La definición de la métrica ocurre dentro del método metrics del constructor del
widget. Este método espera una lista de instancias Metric. Para este ejemplo,
nos estamos refiriendo a una de nuestras métricas personalizadas y mostramos
la suma de los registros de usuarios. Es importante agregar la dimensión envi-
ronment correcta para la definición de la métrica, ya que estamos añadiendo la
etiqueta del entorno a cada métrica personalizada.
GraphWidget.Builder.create()
.title("User Sign In")
.view(GraphWidgetView.BAR)
.left(List.of(new Metric(MetricProps.builder()
.namespace("AWS/Cognito")
.metricName("SignInSuccesses")
.region(awsEnvironment.getRegion())
.period(Duration.minutes(15))
.dimensionsMap(Map.of(
"UserPoolClient", cognitoOutputParameters.getUserPoolClientId(),
"UserPool", cognitoOutputParameters.getUserPoolId()))
.statistic("sum")
.build())))
.right(List.of(new Metric(MetricProps.builder()
.namespace("AWS/Cognito")
.metricName("TokenRefreshSuccesses")
.region(awsEnvironment.getRegion())
.period(Duration.minutes(15))
.dimensionsMap(Map.of(
"UserPoolClient", cognitoOutputParameters.getUserPoolClientId(),
16. Métricas con Amazon CloudWatch 418
"UserPool", cognitoOutputParameters.getUserPoolId()))
.statistic("sum")
.build())))
.build()
LogQueryWidget.Builder
.create()
.view(LogQueryVisualizationType.TABLE)
.title("Backend Logs")
.logGroupNames(List.of(applicationEnvironment + "-logs"))
.queryString(
"fields @timestamp, @message" +
"| sort @timestamp desc" +
"| limit 20")
.build()
Equipados con estos cuatro tipos de widgets, ahora podemos construir paneles
interesantes para diversos propósitos, por ejemplo para mostrar:
Para una plantilla de panel operacional lista para usar para la aplicación de
muestra Stratospheric, dirígete al repositorio de GitHub y echa un vistazo al
constructo OperationalCloudWatchDashboard.
Hay muchas herramientas disponibles que cubren ambos casos de uso mientras
que otras se enfocan en solo una parte. Prometheus, por ejemplo, es una base
de datos de series de tiempo que podría ser perfecta para una solución local de
autohospedaje. Aunque tiene una visualización básica de métricas incorporada,
generalmente estamos mejor eligiendo una herramienta como Grafana para
crear paneles elegantes.
• Datadog
• New Relic
• Splunk
• Dynatrace
• Prometheus
• Grafana
• AppOptics
• etc.
Grafana, por ejemplo, ya viene con soporte nativo para Amazon CloudWatch. En
caso de que nuestro proveedor no se integre con CloudWatch, también podemos
recurrir a los streams de métricas de Amazon CloudWatch para hacer que los
puntos de datos estén disponibles en sistemas de terceros.
17. Alertando con Amazon CloudWatch
Las métricas, los paneles y las percepciones en los registros de nuestra apli-
cación hacen que la operación de nuestra aplicación sea transparente. En todo
momento, sabemos lo que está sucediendo y podemos identificar los problemas
de nuestra infraestructura, arquitectura o implementación.
Aún peor, ¿qué pasa con la operación de nuestra aplicación por la noche? ¡No
queremos tener que mirar los registros toda la noche!
Afortunadamente, existe una solución automatizada para este caso de uso: las
17. Alertando con Amazon CloudWatch 423
alarmas.
Comencemos.
Cada alarma está en uno de los siguientes tres estados en cualquier momento
dado:
17. Alertando con Amazon CloudWatch 424
• OK: Yay - Todo está bien. Estamos por debajo de nuestro umbral definido.
• ALARM: Ouch - El umbral para nuestra métrica o expresión matemática ha
sido superado.
• INSUFFICIENT_DATA: No hay o no hay suficientes puntos de datos para la
métrica disponibles. Esto suele ser el caso cuando acabamos de crear la
alarma, o nuestra métrica ya no se está emitiendo.
Una vez que una alarma pasa a un nuevo estado, podemos activar acciones
automáticas con Amazon CloudWatch. Exploraremos las acciones disponibles
en la sección Añadiendo acciones de alarma a continuación.
Otra configuración esencial que hacer para cada alarma es cómo maneja los
datos métricos ausentes. ¿Deberían los datos faltantes superar nuestro umbral,
podemos ignorarlo, o debería la alarma pasar al estado INSUFFICIENT_DATA?
Esto depende en gran medida de la métrica que estamos evaluando. Por ejemplo,
podríamos tratar los datos faltantes como una transgresión para métricas donde
esperamos datos continuos como la métrica de utilización de CPU de nuestra
tarea ECS. Por otro lado, podemos simplemente ignorar los datos faltantes para
métricas como el conteo de API 5xx ya que este es el estado esperado (no error).
Una última palabra sobre el costo de nuestras alarmas. Se nos factura depen-
diendo de la resolución (estándar vs. alta resolución) de la métrica de alarma que
estamos evaluando. Además, cada alarma compuesta y detección de anomalías
viene con un precio fijo por mes.
Comenzaremos con una alarma básica que monitorea nuestra aplicación Todo
para respuestas lentas. Las respuestas lentas pueden tener múltiples causas.
Por ejemplo, sistemas remotos que responden lentamente de los que depen-
demos, contratiempos generales, o alta presión sobre la base de datos. Para las
APIs críticas para la misión que incluso podríamos exponer a clientes externos,
es vital hacer un seguimiento de la rapidez con la que responde nuestro backend.
Para este ejemplo de alarma, estamos utilizando una métrica que AWS ELB
emite de manera predeterminada. Aunque nuestro backend de Spring Boot
también emite información de tiempo sobre nuestros controladores, el tiempo
de respuesta del ELB es más relevante ya que esto incluye el tiempo total de ida
y vuelta para nuestros clientes y no solo cuánto tiempo tomó la invocación al
controlador.
public MonitoringStack(
// ...
final Environment awsEnvironment) {
// ...
}
}
arn:aws:elasticloadbalancing:{REGION}:{ACCOUNT_ID}:
loadbalancer/app/staging-loadbalancer/2280d6069948c49e
app/staging-loadbalancer/2280d6069948c49e
Para extraer la última parte del ARN del equilibrador de carga, estamos separan-
do el parámetro de salida del constructo Network:
Network.NetworkOutputParameters networkOutputParameters =
Network.getOutputParametersFromParameterStore(this,
applicationEnvironment.getEnvironmentName());
String loadBalancerName = Fn
.split(":loadbalancer/", networkOutputParameters.getLoadBalancerArn(), 2)
.get(1);
System.out.println(networkOutputParameters.getLoadBalancerArn());
${Token[TOKEN.60]}
Lo que sigue es definir cómo nuestra alarma trata los datos faltantes. Para
nuestro ejemplo, no hay puntos de datos para esta métrica siempre que no haya
tráfico a nuestra aplicación (por la noche, por ejemplo). Por lo tanto, se espera
que falten datos y no se sobrepasará el límite.
En el contexto de nuestra aplicación Todo, otras alarmas que vale la pena crear
son las siguientes:
Como próximo paso, agregaremos una acción a nuestra alarma para recibir
notificaciones activas cuando nuestro backend responde lentamente.
17. Alertando con Amazon CloudWatch 432
• Activar acciones de EC2, por ejemplo, para reiniciar o terminar una instan-
cia.
• Realizar una operación de autoscaling, por ejemplo, para escalar cuando la
utilización de recursos es alta.
• Enviar una notificación a un tema de SNS.
• Crear un incidente u OpsItem dentro del AWS Systems Manager.
SNS ofrece varios tipos de suscriptores para notificar, enrutar, activar acciones
siempre que haya una nueva notificación:
Como primer paso, creamos un tema de SNS de Amazon que actúa como un
centro para enrutar las notificaciones de alarma más allá:
17. Alertando con Amazon CloudWatch 433
snsAlarmingTopic.addSubscription(EmailSubscription.Builder
.create(confirmationEmail)
.build()
);
snsAlarmingTopic.addSubscription(UrlSubscription.Builder
.create("https://my-alarming-tool.com/alarms/incoming")
.build());
elbSlowResponseTimeAlarm.addAlarmAction(new SnsAction(snsAlarmingTopic));
elbSlowResponseTimeAlarm.addInsufficientDataAction(new SnsAction(anotherTopic));
elbSlowResponseTimeAlarm.addOkAction(new AutoScalingAction(...));
Como siguiente paso, crearemos una alarma basada en los registros de nuestra
aplicación Spring Boot. Gracias al trabajo de preparación del capítulo Registro es-
tructurado con Amazon CloudWatch, podemos filtrar y contar la salida del registro
por nivel de registro con facilidad.
Pondremos una alarma para nuestro próximo ejemplo que monitorea de forma
continua la cantidad de registros ERROR que produce nuestra aplicación.
Existen numerosas razones por las cuales nuestro backend podría generar
registros ERROR. No todas ellas justifican despertar a alguien en mitad de la
noche. Imagina un atacante malicioso intentando penetrar en nuestras APIs. El
17. Alertando con Amazon CloudWatch 435
{ $.level = "ERROR" }
Como paso final, creamos una alarma basada en esta métrica. Construimos
la alarma directamente desde la instancia de la métrica (a diferencia de new
Alarm() que se utilizó para la alarma anterior). De esta forma, ahorramos en
17. Alertando con Amazon CloudWatch 437
Para este ejemplo, la alarma cambiará al estado ALARM una vez que tengamos
cinco o más registros de errores en tres puntos de datos dentro de un intervalo
de quince minutos.
Quizás te estés preguntando: “¿Qué sentido tiene una alarma que no genera
alertas?”. Utilizaremos la alarma de registro de errores y la combinaremos con
otros estados de alarma en una Alarma Compuesta.
lógica para tener una regla más específica sobre cuándo deberíamos ser alerta-
dos en medio de la noche:
Nuestra AlarmRule crea una expresión lógica AND para ambas alarmas:
17. Alertando con Amazon CloudWatch 440
Dado que esta alarma combinada es ahora un mejor indicador de una posi-
ble interrupción, habilitamos las acciones que esta alarma desencadena con
actionsEnabled(true). Además, añadimos una SnsAction para publicar una
notificación en nuestro tema SNS existente siempre que esta alarma compuesta
esté en el estado ALARM:
compositeAlarm.addAlarmAction(new SnsAction(snsAlarmingTopic));
ni siquiera estar familiarizada con nuestro código. Como una buena práctica,
podemos (y deberíamos) colocar runbooks dentro de nuestro repositorio o
documentarlos en otro lugar.
Los archivos Markdown básicos son suficientes para este propósito. Si es posi-
ble, cada alarma debería tener su propio runbook.
Estos runbooks deben contener instrucciones claras sobre qué verificar in-
mediatamente y posibles sugerencias para mitigar el síntoma o solucionar el
problema subyacente. Algo similar a las listas de verificación que los pilotos
tienen en su cabina para manejar fallos del motor.
Una vez que identificamos el impacto a un alto nivel, la persona de guardia debe
decidir qué hacer a continuación. Por ejemplo, ¿vale la pena investigar más el
problema, despertar a otras personas para que lo solucionen, o volver a dormir?
Para una operación real de nuestra aplicación, también podemos definir políti-
cas de escalada para propagar la alarma a varios equipos o personas individuales
si no logramos reconocer la alarma a tiempo, por ejemplo, cuando la persona de
turno olvidó quitar el modo silencio de su teléfono.
sistema y arquitectura. Técnicamente podemos crear una alarma para casi todo,
pero eso sería un simple ritual sin base lógica. Desde el punto de vista del
desarrollador, podríamos tener una intuición para aquellas partes de nuestro
sistema que - si fallan - tienen un impacto considerable en nuestra operación
general. Ese es un buen punto de partida para nuestras alarmas.
• Opsgenie
• Splunk On-Call
17. Alertando con Amazon CloudWatch 444
• AlertOps
• AWS Incident Manager (parte de AWS Systems Manager)
• etc.
Pero aún no podemos estar seguros en este punto si los casos de uso más
importantes de nuestra aplicación están funcionando como se esperaba. Puede
haber algo mal en la interfaz de usuario, en nuestra aplicación, o en un servicio
del que depende la aplicación que no crea registros de errores o métricas de las
que nos alertamos.
Dado que estas comprobaciones sintéticas son a veces algo inestables, y por
lo tanto requieren cierto esfuerzo para mantener, no cubrimos todos los casos
de uso de nuestra aplicación con estas comprobaciones, sino sólo los más
importantes.
18. Monitoreo Sintético con Amazon CloudWatch 446
La solución que AWS ofrece para monitoreo sintético es parte del paraguas
CloudWatch y se llama “CloudWatch Synthetics”. Cualquier script de monito-
reo que creamos y ejecutamos con este servicio se llama “canarios”, así que nos
quedaremos con este término.
Ahora que tenemos una comprensión compartida de lo que son los canarios,
vamos a repasar el proceso de crear uno para nuestra aplicación Todo.
18. Monitoreo Sintético con Amazon CloudWatch 448
Por supuesto, podríamos extender el script de canario para abarcar todos los
demás casos de uso también. Podríamos verificar si todos los botones en la
interfaz de usuario funcionan como se espera, si la paginación en la vista
del panel funciona correctamente, si podemos editar un elemento de tarea y
agregarle una nota, y si la funcionalidad de “compartir” funciona.
18. Monitoreo Sintético con Amazon CloudWatch 449
Sin embargo, los scripts de canario que dependen de la interfaz de usuario web
de una aplicación son muy vulnerables al cambio. Si el id de un botón o el
diseño de una página cambia, por ejemplo, un script de canario puede fallar
aunque un usuario real no se vea afectado en absoluto. Cuantos más casos de
uso cubran nuestros scripts de canario, más inestables se vuelven y más caros
son de mantener.
Entonces, el compromiso es seleccionar los pocos casos de uso que hacen que
la aplicación sea utilizable para un usuario y solo cubrir esos con uno o más
canarios. Es por eso que hemos elegido el script anterior para la aplicación de
tareas. Para otras aplicaciones, los casos de uso principales se verán completa-
mente diferentes. Si una aplicación tiene un impacto muy alto y cambia muy
raramente, incluso podría tener sentido cubrir todos los casos de uso.
Ten en cuenta que hemos creado un usuario de prueba específicamente para este
canario. En una prueba de canario, no queremos iniciar sesión como un usuario
real por dos razones: tendríamos que compartir la contraseña del usuario real
con el script de canario, y si algo cambia en la configuración del usuario real,
podría romper el canario.
Luego pasamos por los pasos que queremos grabar en el navegador como si
fuésemos un usuario habitual de la aplicación. Una vez que la grabación está
hecha, podemos copiar el script generado desde el complemento del navegador
al interfaz de “Crear Canario” en la Consola de CloudWatch Synthetics y com-
pletar el resto del formulario.
await navigationPromise
await navigationPromise
18. Monitoreo Sintético con Amazon CloudWatch 451
};
exports.handler = async () => {
return await recordedScript();
};
Por defecto, el grabador utiliza selectores CSS como .container > .section >
.container > div > .btn para identificar un elemento en el HTML de la pági-
na web. Es importante destacar que esto no es óptimo, ya que es muy susceptible
a cambios en el diseño del HTML. Si queremos hacerlo más robusto, deberíamos
asignar a cada elemento con el que queremos interactuar un atributo id y luego
usar el selector #id en el script, porque es mucho menos probable que cambie.
Manteniéndolo Sencillo
Como podrías haber adivinado de la sección anterior, crear un script canario con
CloudWatch Synthetics implica editar un script en un área de texto en la interfaz
de usuario web de CloudWatch. Aunque ese área de texto tiene resaltado de
sintaxis, no reemplaza a un IDE y no es adecuado para editar scripts complejos.
Puppeteer para el desarrollo local, pero eso estaría en nosotros para explorar y
configurar ya que no hay soporte oficial para esto.
Si un script canario se vuelve demasiado complejo, ¡es posible que algo esté
mal con el propio script! El mantenimiento del script requerirá mucho esfuerzo
con cada cambio de la aplicación y la resistencia al cambio del equipo de
desarrolladores crecerá.
Una vez que hemos creado y probado nuestro script canario, queremos asegurar-
nos de que se despliegue cada vez que se despliega nuestra aplicación. Para esto,
ampliaremos nuestro proyecto de CDK existente para incluir el script canario.
18. Monitoreo Sintético con Amazon CloudWatch 454
Este es un rol simple con una política integrada que contiene exactamente
los permisos que un Canary necesita para realizar su trabajo. Necesita subir
capturas de pantalla a S3, y enviar métricas y registros a CloudWatch.
CfnCanary.Builder.create(this, "canary")
.name(canaryName)
.runtimeVersion("syn-nodejs-puppeteer-3.9")
.artifactS3Location(bucket.s3UrlForObject("create-todo-canary"))
.startCanaryAfterCreation(Boolean.TRUE)
.executionRoleArn(executionRole.getRoleArn())
.schedule(ScheduleProperty.builder()
.expression("rate(15 minutes)")
.build())
.runConfig(RunConfigProperty.builder()
.environmentVariables(Map.of(
"TARGET_URL", targetUrl,
"USER_NAME", username,
"PASSWORD", password
))
.timeoutInSeconds(30)
.build())
.code(CodeProperty.builder()
.handler("recordedScript.handler")
.script(getScriptFromResource("canaries/create-todo-canary.js"))
.build())
.build();
}
Este ejemplo muestra que, con todos los controles que tenemos a nuestra
disposición, no es sencillo configurar correctamente las alertas. Si queremos
recibir advertencias más tempranas, debemos reducir tanto el intervalo de
ejecución del canario como el período de evaluación de la alarma. Es por esto
que hemos situado la alarma en el CanaryStack y no junto a las demás alarmas
en el MonitoringStack que usamos en capítulos anteriores. Es recomendable
agrupar elementos que tienden a cambiar juntos, tanto en el código de software
como en el código de infraestructura.
Si aún no has construido software “en la nube”, esperamos que este libro haya
despertado tu interés en el desarrollo en la nube en general y en la construcción
de software con Spring Boot y AWS en particular. ¿Quizá incluso lo suficiente
como para sugerir el inicio del camino hacia el desarrollo en la nube en tu lugar
de trabajo?
De todas formas, ¡haznos saber cómo fue tu viaje a la nube! Envíanos un correo
electrónico a info@stratospheric.dev. ¡Nos encantaría escuchar tu historia!
Recursos Adicionales
• Java Testing Toolbox - 30 Testing Tools and Libraries Every Java Developer
Must Know (de Philip)
– Explora el ecosistema de pruebas de Java con introducciones breves a
las herramientas y bibliotecas.
– Piensa en Java Testing Toolbox como un libro de recetas: práctico, con-
ciso y con ejemplos prácticos.
– Cubre marcos de pruebas, bibliotecas de afirmaciones, marcos de simu-
lación, así como pruebas de rendimiento, infraestructura y bibliotecas
de utilidades.
– Obtén el ebook por $9 con el código de descuento u13VONHHTIy6
463
• Libro de fundamentos de AWS: Cubre todos los aspectos de AWS, desde có-
mo empezar hasta los bloques de construcción fundamentales. No depende
del lenguaje de programación ni del marco de trabajo:
– Cubre todas las categorías principales de servicios (Compute, Storage,
Networking, etc.).
464
• AmazonRDSFullAccess
• AmazonEC2ContainerRegistryFullAccess
• SystemAdministrator
• AmazonEC2FullAccess
• AmazonECS_FullAccess
• AWSCertificateManagerFullAccess
• AWSLambda_FullAccess
• AWSCloudFormationFullAccess
• AmazonCognitoPowerUser
• AmazonS3FullAccess
• AmazonSSMFullAccess
• IAMReadOnlyAccess
• AWSKeyManagementServicePowerUser
• AWSCloudFormationFullAccess
• AWSLambda_FullAccess
• AmazonMQApiFullAccess
466
• CloudWatchSyntheticsFullAccess
Dado que AWS impone un límite de 10 políticas por usuario (“No puede exceder
la cuota para PoliciesPerUser: 10”), necesitas crear grupos de usuarios, adjuntar
las políticas a los grupos y agregar el usuario a esos grupos.
Por ejemplo, puedes crear un grupo llamado EC2_users, adjuntar las siguientes
políticas y posteriormente agregar tu usuario IAM a ese grupo (repite para
cualquier otra política de permisos requerida):
• AmazonEC2ContainerRegistryFullAccess
• AmazonEC2FullAccess
• AmazonECS_FullAccess
Guía de Despliegue
Sigue esta guía paso a paso si deseas desplegar toda la infraestructura Stratosp-
heric, incluyendo la aplicación de muestra, en tu cuenta de AWS.
Requisitos previos:
• Has creado un certificado SSL dentro del AWS Certificate Manager para ese
dominio y tienes el ARN para el certificado SSL.
• Has configurado un perfil con nombre para la CLI de AWS (por ejemplo,
stratospheric) con suficientes privilegios para crear / eliminar recursos.
• Tu Docker Engine está en funcionamiento.
• Tienes Node >= 16 instalado: node -v
• Tienes Java 17 instalado: java -version
• Estás usando un procesador x64, o puedes crear una imagen Docker para
esta arquitectura (mira este artículo si estás usando un Apple M1).
Suponemos que estás usando un perfil con nombre para la CLI de AWS llamado
stratospheric. Si estás usando el perfil predeterminado, puedes eliminar --
--profile statrospheric de todos los siguientes comandos:
Si está utilizando Route 53 para su dominio, puede automatizar este proceso con
este comando:
cd application
./gradlew build
En Apple M1:
cd cdk
npm run monitoring:deploy -- --profile stratospheric
cd cdk
npm run canary:deploy -- --profile stratospheric
Conectando a una Base de Datos con RDS. Preferencia por la carga de diferentes
Spring beans a través de propiedades personalizadas en lugar de perfiles
(ver justificación aquí).