Esta es la segunda guía para construir una API Serverless con AWS, la primera la puedes encontrar acá la diferencía son las bases de datos, si quieres saber un poco más sobre que opino al respecto de Serverless pros y contras en ese árticulo explico un poco más, en este vamos a ir directo a donde las papas queman.
Objetivo
Terminar la guía con una API desplegada de AWS y conectada con Aurora Serverless SQL.
Resultado
Puedes encontrar el resultado de esta guía aquí
Requisitos previos
- Tener nodejs 10 o superior
- Cuenta en AWS
- CLI Tool de Serverless
Primeros pasos
La base
Con las herramientas que nos ofrece el CLI de Serverless podemos iniciar el proyecto muy rápidamente usando el comando:
sls create -t aws-nodejs -p note-api && cd note-api
En un par de minutos ya tendrás la base del proyecto lista para comenzar.
Las dependencias
Serverless tiene un gran sistema de plugins que podemos utilizar, uno de las más utilizadas es serverless-offline
que vamos a instalar en conjunto con los módulos que necesitamos para conectarnos con la base de datos, para ello ejecutamos los siguientes comandos en la raíz del proyecto:
npm init -y
npm i --save-dev serverless-offline
npm i --save mysql2 sequelize
Comenzamos a escribir código
En la base de nuestro proyecto /note-api
tendrás un archivo serverless.yml
con configuraciones previas por defecto, ese archivo lo vamos a sustituir complementa con el siguiente contenido:
serverless.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
service: note-api
custom:
secrets: ${file(secrets.json)}
provider:
name: aws
runtime: nodejs12.x
timeout: 30
stage: ${self:custom.secrets.NODE_ENV}
environment:
NODE_ENV: ${self:custom.secrets.NODE_ENV}
DB_NAME: ${self:custom.secrets.DB_NAME}
DB_USER: ${self:custom.secrets.DB_USER}
DB_PASSWORD: ${self:custom.secrets.DB_PASSWORD}
DB_HOST: ${self:custom.secrets.DB_HOST}
DB_PORT: ${self:custom.secrets.DB_PORT}
vpc:
securityGroupIds:
- ${self:custom.secrets.SECURITY_GROUP_ID}
subnetIds:
- ${self:custom.secrets.SUBNET1_ID}
- ${self:custom.secrets.SUBNET2_ID}
- ${self:custom.secrets.SUBNET3_ID}
- ${self:custom.secrets.SUBNET4_ID}
- ${self:custom.secrets.SUBNET5_ID}
- ${self:custom.secrets.SUBNET6_ID}
functions:
healthCheck:
handler: handler.healthCheck
events:
- http:
path: /
method: get
cors: true
create:
handler: handler.create
events:
- http:
path: notes
method: post
cors: true
getOne:
handler: handler.getOne
events:
- http:
path: notes/{id}
method: get
cors: true
getAll:
handler: handler.getAll
events:
- http:
path: notes
method: get
cors: true
update:
handler: handler.update
events:
- http:
path: notes/{id}
method: put
cors: true
destroy:
handler: handler.destroy
events:
- http:
path: notes/{id}
method: delete
cors: true
plugins:
- serverless-offline
En este archivo estan pasando varias cosas, primero declaramos el nombre del servicio o de la app que contendrá las funciones lambda, luego cargamos un archivo de configuración secrets: ${file(secrets.json)}
personalizado que nos ayudará más adelante para definir variables de entorno, en el provider.environment
definimos las variables de entorno para el proyecto, en functions
todas las funciones y los endpoints de las mismas, al final pero no menos importante plugins
donde cargamos los plugins que vamos a utilizar en este proyecto.
secrets.json
En la raíz del proyecto al lado de nuestro archivo serverless.yml
crearemos un nuevo archivo llamado secrests.json
que deberá verse así:
{
"DB_NAME": "test",
"DB_USER": "root",
"DB_PASSWORD": "root",
"DB_HOST": "127.0.0.1",
"DB_PORT": 3306,
"NODE_ENV": "dev",
"SECURITY_GROUP_ID": "sg-xx",
"SUBNET1_ID": "subnet-xx",
"SUBNET2_ID": "subnet-xx",
"SUBNET3_ID": "subnet-xx",
"SUBNET4_ID": "subnet-xx",
"SUBNET5_ID": "subnet-xx",
"SUBNET6_ID": "subnet-xx"
}
En este archivo vamos a sustituir las variables cuando creemos la base de datos en la consola de AWS, recuerda agregarlo a tu archivo `.gitignore
Las funciones
En nuestro archivo serverless.yml
ya definimos las funciones, nombres y el endpoint correspondiente e indicamos que esas funciones estarían en el archivo handlers.js
pero en este momento aún no están, para agregarlas debemos sustituir el código por defecto que tiene este archivo por este:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
module.exports.healthCheck = async () => {
await connectToDatabase()
console.log('Connection successful.')
return {
statusCode: 200,
body: JSON.stringify({ message: 'Connection successful.' })
}
}
module.exports.create = async (event) => {
try {
const { Note } = await connectToDatabase()
const note = await Note.create(JSON.parse(event.body))
return {
statusCode: 200,
body: JSON.stringify(note)
}
} catch (err) {
return {
statusCode: err.statusCode || 500,
headers: { 'Content-Type': 'text/plain' },
body: 'Could not create the note.'
}
}
}
module.exports.getOne = async (event) => {
try {
const { Note } = await connectToDatabase()
const note = await Note.findByPk(event.pathParameters.id)
if (!note) throw new HTTPError(404, `Note with id: ${event.pathParameters.id} was not found`)
return {
statusCode: 200,
body: JSON.stringify(note)
}
} catch (err) {
return {
statusCode: err.statusCode || 500,
headers: { 'Content-Type': 'text/plain' },
body: err.message || 'Could not fetch the Note.'
}
}
}
module.exports.getAll = async () => {
try {
const { Note } = await connectToDatabase()
const notes = await Note.findAll()
return {
statusCode: 200,
body: JSON.stringify(notes)
}
} catch (err) {
return {
statusCode: err.statusCode || 500,
headers: { 'Content-Type': 'text/plain' },
body: 'Could not fetch the notes.'
}
}
}
module.exports.update = async (event) => {
try {
const input = JSON.parse(event.body)
const { Note } = await connectToDatabase()
const note = await Note.findByPk(event.pathParameters.id)
if (!note) throw new HTTPError(404, `Note with id: ${event.pathParameters.id} was not found`)
if (input.title) note.title = input.title
if (input.description) note.description = input.description
await note.save()
return {
statusCode: 200,
body: JSON.stringify(note)
}
} catch (err) {
return {
statusCode: err.statusCode || 500,
headers: { 'Content-Type': 'text/plain' },
body: err.message || 'Could not update the Note.'
}
}
}
module.exports.destroy = async (event) => {
try {
const { Note } = await connectToDatabase()
const note = await Note.findByPk(event.pathParameters.id)
if (!note) throw new HTTPError(404, `Note with id: ${event.pathParameters.id} was not found`)
await note.destroy()
return {
statusCode: 200,
body: JSON.stringify(note)
}
} catch (err) {
return {
statusCode: err.statusCode || 500,
headers: { 'Content-Type': 'text/plain' },
body: err.message || 'Could destroy fetch the Note.'
}
}
}
Con esto tendremos definidas las funciones, puede parecer confuso todo lo que está pasando en este gran trozo de código pero básicamente es un controlador que trabajar en conjunto con nuestro Modelo de Note (o notas), para crear un CRUD.
Conexión a la base de datos
Ahora que ya tenemos los endpoint en el serverless.yml
y el controlador en el handler.js
nos hace falta la conexión con la base de datos y el modelo, para esto vamos a crear un archivo db.js
en la raíz del proyecto que tenga el siguiente contenido:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const Sequelize = require('sequelize')
const NoteModel = require('./models/Note')
const sequelize = new Sequelize(
process.env.DB_NAME,
process.env.DB_USER,
process.env.DB_PASSWORD,
{
dialect: 'mysql',
host: process.env.DB_HOST,
port: process.env.DB_PORT
}
)
const Note = NoteModel(sequelize, Sequelize)
const Models = { Note }
const connection = {}
module.exports = async () => {
if (connection.isConnected) {
console.log('=> Using existing connection.')
return Models
}
await sequelize.sync()
await sequelize.authenticate()
connection.isConnected = true
console.log('=> Created a new connection.')
return Models
}
Lo más importante de este archivo es cómo se administran las conexiones a la base de datos, si no existe alguna conexión se crear y en caso de que aún esté viva una conexión de reutiliza.
Modelo de datos
Para nuestro modelo creamos una carpeta llamada models
y dentro un archivo Note.js
este archivo lo llenamos con el siguiente código:
1
2
3
4
5
6
7
8
9
10
11
module.exports = (sequelize, type) => {
return sequelize.define('note', {
id: {
type: type.INTEGER,
primaryKey: true,
autoIncrement: true
},
title: type.STRING,
description: type.STRING
})
}
En este modelo estamos definiendo la table note y los campos que tendrá esta tabla en la base de datos, gracias a sequelize
es bastante sencillo.
Volvemos al handler.js
Ahora agregamos al comienzo del handler.js
la función para iniciar la conexión con la base de datos y una función para manejar los errores de HTTP de forma sencilla
const connectToDatabase = require('./db')
function HTTPError (statusCode, message) {
const error = new Error(message)
error.statusCode = statusCode
return error
}
¿Ya y estamos listos para probar? Aun no, nos falta un gran detalle, para poder probar nuestra aplicación offline tenemos que tener una base de datos local, recuerdas el archivo secrets.json
si ya tienes instalado mysql en tu computador puedes sustituir allí las variables de tu base de datos por otro lado si no te gusta tener bases de datos instaladas en tu sistema siempre te puedes ayudar con el amigo docker, para eso te dejo este otro articulo: No instales Mysql o Mariadb, utiliza docker.
Pruebas
Una vez que tengas las variables de tu base de datos local o en docker agregadas en el archivo secrets.json
ya puedes ejecutar la aplicación, con el comando:
sls offline start --skipCacheInvalidation
La propiedad --skipCacheInvalidation
nos ayuda a que se recarguen los módulos más rápidamente. Se lo podemos sacar en caso de tengamos algún error.
Si todo está correcto como debería ya podemos probar con nuestro Cliente de API favorita, Postman, Insomnia...
1
2
3
4
5
6
7
# Las rutas que tenemos definidas son:
GET - http://localhost:3000/ # Raíz - healthCheck
POST - http://localhost:3000/notes # Crear una nota - create
GET - http://localhost:3000/notes/{id} # Leer una nota - getOne
GET - http://localhost:3000/notes # Leer todas la snotas - getAll
PUT - http://localhost:3000/notes/{id} # Actualizar una nota - update
DELETE - http://localhost:3000/notes/{id} # Borrar una nota - destroy
Si revisamos la terminal podrás ver cuando se crear una nueva conexión a la base de datos using new database connection
o cuando se utiliza using existing database connection
. Hasta este punto todo funciona en local pero el objetivo de esta guía es terminar con una API desplegada en AWS y conectada con Aurora Serverless SQL.
Crear la base de datos Aurora Serverless
Accedemos a la consola de AWS buscamos el servicio RDS
y Create database
las opciones van casi por defecto, las elegimos en el siguiente orden:
Choose a database creation method
- Standard Create
Engine options
- Amazon Aurora
- Amazon Aurora with MySQL compatibilit
- Versión (la última disponible)
- Regional
Database features
- Serverless
Templates
- Dev/Test
Settings
- DB cluster identifier: El nombre de nuestro cluster o servidor de bases de datos.
- Credentials Settings: username y password de tu preferencia pero recuerda que las necesitarás más adelante.
DB instance size
- Memory Optimized classes
- db.r5.large
Availability & durability
- Create an Aurora Replica/Reader node in a different AZ (recommended for scaled availability).
Connectivity
- Virtual Private Cloud (VPC): Por defecto creará una nueva.
Database authentication
- Password authentication
Additional configuration
- Initial database name: importantes necesitamos agregar el nombre a tu base de datos ya que
sequelize
no es capaz de crear la base de datos.
... los campos no mencionados quedan con el valor por defecto.
Luego confirmamos que queremos crear la base de datos en Create database
y esto comienza el proceso que demora un par de minutos (15 ~ 20) luego de que esté creada nuestra base de datos en AWS, tendremos los valores que necesitamos sustituir en el archivo secrets.json
.
Deploy
Una vez sustituido las variables en el archivo secrets.json
podemos hacer el deploy de nuestra api, con el comando:
sls deploy
Luego de un par de minutos tendremos una url en la que podemos probar como funciona nuestra aplicación desplegada sustituyendo.
Recuerda que puedes ver el resultado de esta guía implementado aquí
Comandos útiles
sls config
nos ayuda a validar la configuración del archivoserverless.yml
.sls print
nos imprime el archivoserverless.yml
tal y como lo vería el intérprete.sls remove
si quieres eliminar todos los servicios creados por tu aplicación en AWS.
Gracias por llegar hasta acá, recuerda si tienes algún comentario o duda me lo puedes hacer llegar por twitter: @enBonnet o directamente por telegram @enBonnet.