# Despliegue de Leemons en AWS ECS + Cloudfront
A continuación se detallan los pasos necesarios para realizar el despliegue de Leemons en 3 capas desacopladas:
- [Backend](#Backend-clúster-en-AWS-ECS): como clúster en [AWS ECS](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/Welcome.html).
- [MQTT Plugin](#MQTT-Plugin): como servicio desacoplado utilizando [AWS IoT](https://docs.aws.amazon.com/es_es/iot/latest/developerguide/what-is-aws-iot.html).
- [Frontend](#Frontend-Despliegue-en-Cloudfront): como distribución CDN en [AWS Cloudfront](https://docs.aws.amazon.com/es_es/AmazonCloudFront/latest/DeveloperGuide/Introduction.html).
- [Carga inicial de datos](#Carga-de-datos-a-partir-de-un-Excel): a partir de una plantilla Excel
## Requerimientos previos
Para poder seguir correctamente todos los pasos que se presentan a continuación, es necesario contar un proyecto Leemons descargado en tu máquina. Para ello, vamos a clonar el repositorio en una carpeta para nuestro proyecto, que llamaremos `my-leemons`:
```bash
git clone https://github.com/leemonade/leemons.git my-leemons
cd my-leemons
```
Nos cambiamos a la rama de `dev` que es la que tiene las últimas features de Leemons:
```bash
git checkout dev
```
Por último, vamos a instalar todas las dependencias. Leemons es un monorepo gestionado por [Yarn workspaces](https://classic.yarnpkg.com/lang/en/docs/workspaces/), por lo tanto utilizaremos `yarn` como gestor de paquetes:
```bash
yarn install
```
También necesitaremos tener Docker (linux) o Docker Desktop (Windos/MacOS).
> **IMPORTANTE**
> A lo largo de este documento, siempre haremos referencia al proyecto Leemons como `my-leemons`
## Backend: clúster en AWS ECS
AWS ECS necesita que el backend de Leemons esté dockerizada. La solución más sencilla es utilizar la imagen Docker disponible en Docker hub.
Si deseas crear tu propia imagen Docker para el Backend de Leemons, puedes saltar hasta la sección [Creación de una imagen Docker personalizada](#2-Creación-de-una-imagen-Docker-personalizada), de lo contrario, sigue leyendo.
### 1. Utilizar la imagen de Docker Hub
La imagen del backend de Leemons disponible en Docker Hub, viene cargada con todos los `plugins` y `providers` disponibles hasta ahora.
Está testeada haciendo uso de 2 clústers de Base de datos diferentes:
- **MySQL**: a través del servicio [AWS RDS](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_MySQL.html)
- **MongoDB**: a través del servicio de [Mongo Atlas](https://www.mongodb.com/es/atlas/database)
> **IMPORTANTE**:
> Esta imagen docker NO está preparada para ser ejecutada únicamente con un clúster de MongoDB como persistencia de datos, necesita obligatoriamente tener ambas bases de datos disponibles. Si deseas utilizar únicamente MongoDB, salta hasta la sección [Creación de una imagen Docker personalizada](#2-Creación-de-una-imagen-Docker-personalizada).
Para utilizar la imagen de Docker Hub en tu clúster ECS, necesitas primero descargarla en tu máquina:
```bash
docker pull leemonade/leemons-back:dev
```
### 2. Creación de una imagen Docker personalizada
Si deseas personalizar la imagen Docker del backend para, por ejemplo, incluir algún `plugin` personalizado, o forzar a que todos los plugins utilicen únicamente una misma base de datos, puedes crear una imagen Docker en tu máquina.
**Supuesto**: Supongamos que queremos que todos los plugins utilicen un único clúster de `MongoDB` para persistir los datos. Para ello, vamos a editar el siguiente archivo `my-leemons/examples/docker/config/database.js`, y dejarlo de la siguiente manera:
```js
module.exports = {
connections: {
mongo: {
connector: 'mongoose',
settings: {
database: process.env['NOSQL_DATABASE'],
authDatabase: process.env['NOSQL_AUTH_DATABASE'],
username: process.env['NOSQL_USERNAME'],
password: process.env['NOSQL_PASSWORD'],
port: process.env['NOSQL_PORT'],
host: process.env['NOSQL_HOST'],
srv: process.env['NOSQL_SRV'],
pool: {
min: 5,
max: 1000,
},
},
},
},
defaultConnection: 'mongo',
};
```
Hemos eliminado toda la conexión de `MySQL`, y solo hemos dejado la conexión de `MongoDB`. **IMPORTANTE**: la línea `defaultConnection: 'mongo'`.
Finalmente, creamos nuestra imagen Docker del backend de Leemons, ejecutando en la raíz de `my-leemons`:
```bash
docker build -t leemonade/leemons-back:dev .
```
### 3. Subir la imagen docker a AWS ECR
Una vez tengas la imagen Docker en tu máquina, sigue los pasos que te indica AWS para subirla a tu repositorio privado en ECR: https://docs.aws.amazon.com/AmazonECR/latest/userguide/docker-push-ecr-image.html
### 4. Crear el clúster AWS ECS
Una vez tenemos nuestra imagen Docker subida al nuestro repositorio ECR, podemos crear nuestra clúster de tareas en ECS.
Para ello, podemos seguir el siguiente video tutorial a partir del minuto 1: https://youtu.be/o7s-eigrMAI?t=94
Nosotros recomendamos utilizar un clúster ECS basado en Fargate en lugar de instancias EC2, por los siguientes motivos:
- **Precio**: Si su carga de trabajo es pequeña con ráfagas ocasionales, como una aplicación que tiene tráfico durante el día pero poco tráfico por la noche (caso de Leemons), entonces AWS Fargate es una opción fantástica. Puede escalar a un contenedor diminuto por la noche, con un costo muy bajo, pero seguir escalando durante el día, mientras que sólo paga por los núcleos de CPU y gigabytes de memoria que su tarea requiere.
- **Seguridad**: Gestionar un gran clúster de instancias EC2 puede ser algo difícil. Hay que asegurarse de que todas están parcheadas, seguras y actualizadas a la última versión de Docker y del agente ECS. Si no quieres lidiar con esta sobrecarga, AWS Fargate puede ser una gran opción. Por ejemplo, cuando se anunció la vulnerabilidad Spectre / Meltdown, los clientes que ejecutaban EC2 tuvieron que asegurarse de parchear y actualizar, mientras que los clientes que ejecutaban AWS Fargate fueron protegidos automáticamente entre bastidores por los ingenieros de AWS que parchearon la infraestructura subyacente.
- **Testing**: Para un entorno de pruebas pequeño, AWS Fargate es perfecto. Por lo general, es un desperdicio ejecutar un entorno de pruebas pequeño en una instancia EC2 porque la instancia EC2 es demasiado potente, y le resultará difícil obtener un buen porcentaje de utilización.
#### Definición de la Tarea
Durante la creación de la tarea en ECS, que debes hacer manualmente, puedes fijarte en el siguiente JSON, que muestra un ejemplo de algunas partes de una tarea de pruebas:
```json
{
"family": "leemons-dev-task",
"networkMode": "awsvpc",
"revision": 1,
"volumes": [],
"status": "ACTIVE",
"placementConstraints": [],
"compatibilities": [
"EC2",
"FARGATE"
],
"requiresCompatibilities": [
"FARGATE"
],
"cpu": "1024",
"memory": "4096",
"runtimePlatform": {
"cpuArchitecture": "X86_64",
"operatingSystemFamily": "LINUX"
},
"tags": [],
"containerDefinitions": [
{
"name": "leemons-dev-task",
"image": "945639520906.dkr.ecr.eu-central-1.amazonaws.com/leemons-dev-backend:latest",
"essential": true,
"cpu": 1024,
"memory": 4096,
"memoryReservation": 3584,
"portMappings": [
{
"name": "leemons-dev-task-8080-tcp",
"containerPort": 8080,
"hostPort": 8080,
"protocol": "tcp",
"appProtocol": "http"
}
],
"mountPoints": [],
"volumesFrom": [],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-create-group": "true",
"awslogs-group": "/ecs/leemons-dev-task",
"awslogs-region": "eu-central-1",
"awslogs-stream-prefix": "ecs"
}
},
"environment": [
{
"name": "DATABASE_HOST",
"value": "mydb.ID.eu-central-1.rds.amazonaws.com"
},
{
"name": "DATABASE_PORT",
"value": "3306"
},
{
"name": "DATABASE_DATABASE",
"value": "leemons-ecs"
},
{
"name": "DATABASE_USERNAME",
"value": "leemons"
},
{
"name": "DATABASE_PASSWORD",
"value": "******"
},
{
"name": "NOSQL_HOST",
"value": "cluster0.ID.mongodb.net"
},
{
"name": "NOSQL_PORT",
"value": "undefined"
},
{
"name": "NOSQL_DATABASE",
"value": "leemons-ecs"
},
{
"name": "NOSQL_USERNAME",
"value": "leemons"
},
{
"name": "NOSQL_PASSWORD",
"value": "******"
},
{
"name": "NOSQL_AUTH_DATABASE",
"value": "admin"
},
{
"name": "NOSQL_SRV",
"value": "true"
},
{
"name": "CORS",
"value": "true"
},
{
"name": "USE_CUSTOM_ROLLBACK",
"value": "true"
},
{
"name": "MQTT_PLUGIN",
"value": "mqtt-aws-iot"
}
]
}
],
"requiresAttributes": [
{
"name": "com.amazonaws.ecs.capability.logging-driver.awslogs"
},
{
"name": "ecs.capability.execution-role-awslogs"
},
{
"name": "com.amazonaws.ecs.capability.ecr-auth"
},
{
"name": "com.amazonaws.ecs.capability.docker-remote-api.1.19"
},
{
"name": "com.amazonaws.ecs.capability.docker-remote-api.1.21"
},
{
"name": "ecs.capability.execution-role-ecr-pull"
},
{
"name": "com.amazonaws.ecs.capability.docker-remote-api.1.18"
},
{
"name": "ecs.capability.task-eni"
},
{
"name": "com.amazonaws.ecs.capability.docker-remote-api.1.29"
}
]
}
```
Ten especial consideración en las variables de entorno, ya que necesitarás establecerlas cuidadosamente.
### NOTAS IMPORTANTES:
Leemons necesita ejecutarse la primera vez en una única instancia, que será la que haga el setup de las Bases de datos al iniciarse.
Para ello, es importante indicar en la cofiguración del `Servicio -> Configuración del despliegue`, en el momento de la creación del clúster ECS, que la cantidad "Deseada de Tareas" sea inicialmente "1".
Una vez Leemons haya realizado el Setup de las bases de datos y plugins, entonces podemos cambiar este valor a la cantidad de Tareas que veamos necesarias tener iniciadas siempre.
### 5. Balanceador de Carga
Durante la configuración del Servicio que gestionará el clúster de tareas, necesitarás configurar un balanceador de carga.
Asegúrate de que el balanceador de carga redireccione el puerto `443` del tráfico entrante, al puerto `8080` de los contenedores.
**IMPORTANTE**: En el `Target group` del balanceador de carga, ajusta el `health check`, para que el intervalo de comprobación sea cada 60 segundos con un timeout de 20 segundos. De lo contrario el servicio del ECS puede estar indefinidamente reiniciando las tareas.
Además, es muy importante que generes e incluyas un certificado SSL para tu dominio, que incluya un wildcard, por ejemplo `*.my-leemons.com`, porque también lo necesitarás más adelante en el despliegue del [Frontend](#Frontend-Despliegue-en-Cloudfront).
Finalmente, crea un subdominio para redirigir el tráfico al balanceador de carga desde AWS Route53, por ejemplo `backend.my-leemons.com`. Puedes seguir este turorial: https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-to-elb-load-balancer.html
## MQTT Plugin
Leemons utiliza un `message broker` para publicar y recibir mensajes entre backend y frontend en diversos plugins. Uno de ellos es el plugin de `Comunica`, que hace un uso intensivo de este servicio.
Para el caso de un despliegue en clúster ECS, vamos a utilizar un `plugin` específico, que nos permita desacoplar esta capa y hacerla a la vez escalable para todos los contenedores dentro del clúster.
Para ello, vamos a indicarle a Leemons que utilice otro `provider` de MQTT como `message broker`, más concretamente el servicio de [AWS IoT](https://docs.aws.amazon.com/es_es/iot/latest/developerguide/mqtt.html).
Esto lo podemos conseguir, simplemente indicando en la definición de la tarea que utilice el plugin a través de una variable de entorno, tal como lo hemos hecho en el ejemplo de [JSON](#Definición-de-la-Tarea) de la definición de tarea mostrado anteriormente:
```json
...
{
"name": "MQTT_PLUGIN",
"value": "mqtt-aws-iot"
}
...
```
> **IMPORTANTE**: La configuración del servicio AWS IoT y del plugin de `mqtt-aws-iot` se indicará en la sección de [Carga de datos a partir de un Excel](#Carga-de-datos-a-partir-de-un-Excel)
## Frontend: Despliegue en Cloudfront
Para poder desplegar el frontend de Leemons como una distribución de Cloudfront, primero debemos compilarlo. Es importante que compilemos el frontend con todos los plugins que hemos utilizado en la generación del Docker para el backend.
Para ello, iremos a la carpeta del proyecto `my-leemons/examples/docker` y lanzaremos el compilador:
```bash
API_URL='https://backend.my-leemons.com' yarn front:build
```
Nota que hay que establecer la variable de entorno `API_URL` en el lanzamiento del comando de compilación. Esta variable de entorno, debe apuntar a la ruta creada en el último paso de la sección [Balanceador de Carga](#5-Balanceador-de-Carga).
El comando de compilación, generará la carpeta `./build` en la misma ruta en la que has lanzado el comando.
Finalmente, deberás subir el contenido de la carpeta `build` a un bucket AWS S3 y crear la distribución Cloudfront los pasos indicados en este video a partir del minuto 3: https://youtu.be/J40PCO7rvcA?t=180
Puedes utilizar el certificado SSL creado en la sección [Balanceador de Carga](#5-Balanceador-de-Carga) en el paso requerido de la creación de la distribución Cloudfront, y también crear un subdominio similar al del backend, por ejemplo `app.my-leemons.com`, o apuntar directamente la raíz de tu dominio a la distribución Cloudfront.
### BrowserRouter
El frontend de Leemons utiliza la estratégia de [BrowserRouting](https://reactrouter.com/en/main/router-components/browser-router) para navegar entre las distintas pantallas.
Esta estratégia no es compatible con la navegación entre carpetas de un bucket S3, por lo que para solventar esta situación, debemos realizar los siguientes ajustes:
- En nuestro ejemplo, primero creamos el bucket `app.my-leemons.com` y hemos cargado allí los archivos del frontend.
- Le damos permisos de acceso público al bucket S3, y le activamos la propiedad de “Sitio web estático”, indicando que tanto la página por defecto como la página de error sea el `index.html`. Esto hace que el bucket S3 ya de por si sea un servidor web estático, y que todas las páginas que utilicen el `browserRouting` con rutas anidadas, en lugar de dar un error de bucket `Access denied`, sean gestionadas por el `index.html`.
- Luego creamos la distribución cloudfront, pero en lugar de que el `origin` sea directamente el bucket S3, le indicamos el dominio del sitio web estático que genera el propio S3. Es decir:
- En lugar de indicar al cloudfront que el `origin` sea el bucket `app.my-leemons.com`, le indicamos que sea el dominio del sitio web estático que genera el S3 `app.my-leemons.com.s3-website.eu-central-1.amazonaws.com` (debe incluir el `s3-website`).
Esto hace que cloudfront cree una cache de todas las rutas según se vayan solicitando desde el navegador, respetando todas las rutas del `browserRouter`.
## Carga de datos a partir de un Excel
> **IMPORTANTE:** No se debe configurar absolutamente nada dentro de Leemons antes de realizar la carga desde el Excel. Si se ha configurado algo, la carga no funcionará correctamente.
Una vez tengamos todo desplegado y funcionando, por ejemplo en:
- Frontend: `https://app.my-leemons.com`
- Backend: `https://backend.my-leemons.com`
Podremos cargar los datos, tanto de configuración como de contenidos, desde un archivo Excel.
Para ello, accedemos al archivo compartido de [Google Sheets](https://docs.google.com/spreadsheets/d/1b5Z207A0O_lVEYiKQnctbc7oix70FOdkVZ91I0IUn2A/edit?usp=sharing) que ya tiene el template cargado, y editamos la hoja con nombre `providers`.
Antes de continuar, necesitamos crear un usuario de consola en [AWS IAM](https://docs.aws.amazon.com/es_es/IAM/latest/UserGuide/introduction.html), con permisos suficientes para utilizar los siguientes servicios de AWS:
- [AWS SES](https://docs.aws.amazon.com/es_es/ses/latest/dg/Welcome.html): utilizado para enviar correos desde el backend de Leemons.
- [AWS S3](https://docs.aws.amazon.com/es_es/AmazonS3/latest/userguide/Welcome.html): utilizado para almacenar los archivos y contenido de la librería de Leemons.
- [AWS IoT](https://docs.aws.amazon.com/es_es/iot/latest/developerguide/what-is-aws-iot.html): utilizado como `message broker` en la publicación y recepción de mensajes en Leemons.
El JSON de políticas para un usuario de IAM con estas características, luciría así:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject",
"s3:GetObjectTagging",
"s3:PutObjectTagging",
"s3:DeleteObjectTagging"
],
"Resource": [
"arn:aws:s3:::my-leemons-bucket/*"
]
},
{
"Effect": "Allow",
"Action": "ses:SendRawEmail",
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"iot:Connect",
"iot:Subscribe",
"iot:Publish",
"iot:Receive",
"iot:DescribeEndpoint",
"sts:GetFederationToken"
],
"Resource": "*"
}
]
}
```
Una vez creado el usuario IAM y teniendo a mano su `AccessKey` y `SecretAccessKey`, volvemos a la pestaña `providers` del Excel, y completamos las celdas:
- En las filas `3`, `4` y `5`, necesitamos completar las columnas `E` y `F` con los datos de acceso del usuario IAM.
- En la fila `3`, también necesitamos indicar en la celda `G3` el nombre del Bucket S3 donde se guardarán los archivos y que indicamos en el campo "Resource" del JSON de políticas sobre el S3, en nuestro JSON de ejemplo: `my-leemons-bucket`.
Cuando tengamos todo listo, debemos descargar el archivo en formato `Microsoft Excel(*.xlsx)` y almacenarlo en nuestra máquina, por ejemplo con el nombre de `data.xlsx`.
Finalmente, podremos iniciar la carga de los datos en Leemons haciendo la siguiente llamada POST:
```bash
curl --location 'https://backend.my-leemons.com/api/bulk-template/load-from-file' \
--form 'doc=@"data.xlsx"'
```
Esto iniciará la carga masiva de datos. Este proceso puede durar varios minutos. Para comprobar el estado de la carga, puedes realizar una consulta haciendo una llamada GET:
```bash
curl --location --request GET 'https://backend.my-leemons.com/api/bulk-template/load-from-file'
```
Esta llamada debe mostrar un objeto JSON con el estado de la carga, por ejemplo:
```json
{
"status": 200,
"currentPhase": "Proccessing file",
"overallProgress": "10%"
}
```
Al finalizar, el JSON devuelto por la llamada mostrará un JSON como el siguiente:
```json
{
"status": 200,
"currentPhase": "{Último plugin cargado}",
"overallProgress": "100%"
}
```
Lo importante es que obtengas el siguiente resultado`"overallProgress": "100%"`.