Microservicio Api Rest para la Gestión de Máquinas Industriales implementando Serverless, Api Gateway, SSM, Bucket S3, NodeJs y Otras Tecnologías.
Ver
- 1.0) Descripción del Proyecto.
- 1.1) Ejecución del Proyecto.
- 1.2) Tecnologías.
- 1.3) Plugins.
- 1.4) Extensiones VSC.
- 2.0) Instalación y Configuración de Serverless Local
- 2.1) Configuración de Api Gateway
- 2.2) Ejecución de Serverless Local
- 2.3) SSM y IAM
- 3.0) Arquitectura Lambda Function, Api gateway y Bucket S3
- 3.1) Registro de Objectos Json Bucket S3
- 3.2) Instalación y Configuración de Amazon Bucket S3
- 3.3) Escritura de objetos
1.0) Descripción 🔝
Ver
- ipsum
1.1) Ejecución del Proyecto 🔝
Ver
- Crear un entorno de trabajo a través de algún IDE
- Clonar el Proyecto (
git clone https://github.com/andresWeitzel/Api_MaquinasIndustriales_ServerlessBucketS3_NodeJs
) - Dentro del directorio instalar todos los plugins implementados
npm install -g serverless
npm i serverless-offline
npm install serverless-offline serverless-offline-ssm --save-dev
npm install serverless-s3-local --save-dev
- Levantar Serverless en Local (
sls offline start
) - Comprobar respuestas de los endpoints generados a través de alguna herramienta Cliente Http (Ej:Postman)
1.2) Tecnologías 🔝
Ver
Tecnologías | Versión | Finalidad |
---|---|---|
SDK | 4.3.2 | Inyección Automática de Módulas para Lambdas |
Serverless Framework Core | 3.23.0 | Core Servicios AWS |
Serverless Plugin | 6.2.2 | Librerías para la Definición Modular |
Systems Manager Parameter Store (SSM) | 3.0 | Manejo de Variables de Entorno |
Amazon Api Gateway | 2.0 | Gestor, Autenticación, Control y Procesamiento de la Api |
Amazon S3 Bucket | 3.0 | Contenedor de Objetos |
NodeJS | 14.18.1 | Librería JS |
VSC | 1.72.2 | IDE |
Postman | 10.11 | Cliente Http |
CMD | 10 | Símbolo del Sistema para linea de comandos |
Git | 2.29.1 | Control de Versiones |
1.3) Plugins 🔝
1.4) Extensiones VSC 🔝
Ver
Extensión |
---|
Prettier - Code formatter |
YAML - Autoformatter .yml (alt+shift+f) |
YAML-fm Linter |
DotENV |
2.0) Instalación y Configuración de Serverless Local 🔝
Ver
-
Una vez abierto el proyecto instalamos serverless de forma Global
npm install -g serverless
-
Seguidamente creamos toda la config de serverless para nuestro proyecto(en mi caso el nombre del proyecto es
api-maquinas-industriales
)serverless create --template aws-nodejs --path api-maquinas-industriales && cd api-maquinas-industriales
-
Luego inicializamos el package.json en el proyecto
npm init -y
. -
Instalamos el plugin serverless-offline
npm i serverless-offline
-
Comprobamos versión
serverless --version
-
Salida Esperada ..
Framework Core: 3.23.0 Plugin: 6.2.2 SDK: 4.3.2
-
Agregamos el plugin instalado de serverless-offline al archivo
serverless.yml
-
Configuramos los diversos parámetros necesarios del provider
-
Vamos a trabajar todas las lambdas dentro del directorio
src/lambdas/
por asuntos de modularización de código -
Renombramos la lambda
handler.js
parahello.js
-
Configuramos tipo de método y path a través de httpApi.
-
Configuramos el puerto http
-
Archivo serveless.yml..
service: api-maquinas-industriales frameworkVersion: '3' provider: name: aws runtime: nodejs14.x stage: offline region : us-west-1 memorySize: 512 timeout : 20 plugins: - serverless-offline custom: serverless-offline: httpPort: 4000 functions: hello: handler: src/lambdas/hello.msg events: - httpApi: method: GET path: hello
-
La lambda
hello.js
la movemos al directorio ya mencionadosrc/lambdas/
-
Archivo Lambda hello.js..
'use strict'; module.exports.msg = async (event) => { return { statusCode: 200, body: JSON.stringify( { message: 'HELLO', input: event, }, null, 2 ), }; };
- Guía Oficial : https://www.serverless.com//blog/serverless-framework-v3-is-live
- Guía Recomendada : https://medium.com/@patricio.aranguiz/serverless-offline-aws-lambda-api-gateway-15a4dfdfbc16
- Config Parámetros Provider : https://www.tutorialspoint.com/serverless/serverless_regions_memory_size_timeouts.htm
2.1) Configuración de Api Gateway 🔝
Ver
-
API Gateway gestiona todas las tareas relacionadas con la aceptación y el procesamiento de centenares de miles de llamadas simultáneas a la API. Estas tareas incluyen la administración del tráfico, el control de la autorización y el acceso, el monitoreo y la administración de versiones de la API.
-
No es necesario la instalación de ningún paquete adicional, este servicio viene incluido en la instalación principal de serverless.
-
Para cada lambda es necesario adicionar el parametro
private: true
dentro de- httpApi
para que se aplique la restricción de acceso correctamente. -
Vamos a generar una sección de
resources
. Esta es la plantilla de CloudFormation (Servicio de recursos de AWS) para declarar los recursos de serverless a utilizar. -
En este caso vamos a extender los diversos manejos de recursos para nuestra Api Gateway. (Tipos, Templates y Códigos de Respuesta).
-
La configuración General de nuestro
serverless.yml
quedaría...service: api-maquinas-industriales frameworkVersion: '3' provider: name: aws runtime: nodejs14.x stage: offline region : us-west-1 memorySize: 512 timeout : 20 plugins: - serverless-offline custom: serverless-offline: httpPort: 4000 functions: hello: handler: src/lambdas/hello.msg events: - httpApi: method: GET path: hello private: true resources: Resources: ApiGatewayRestApi: Type: AWS::ApiGateway::RestApi Properties: Name: apiGatewayRestApi #### Gateway Response INIT GatewayResponseDefault400: Type: 'AWS::ApiGateway::GatewayResponse' Properties: RestApiId: Ref: 'ApiGatewayRestApi' ResponseType: DEFAULT_4XX ResponseTemplates: application/json: "{\"error\":{\"code\":\"custom-4XX-generic\",\"message\":$context.error.messageString},\"requestId\":\"$context.requestId\"}" GatewayResponseDefault500: Type: 'AWS::ApiGateway::GatewayResponse' Properties: RestApiId: Ref: 'ApiGatewayRestApi' ResponseType: DEFAULT_5XX ResponseTemplates: application/json: "{\"error\":{\"code\":\"custom-5XX-generic\",\"message\":$context.error.messageString},\"requestId\":\"$context.requestId\"}" GatewayResponseAccessDeied: Type: 'AWS::ApiGateway::GatewayResponse' Properties: RestApiId: Ref: 'ApiGatewayRestApi' ResponseType: ACCESS_DENIED ResponseTemplates: application/json: "{\"error\":{\"code\":\"custom-403-access-denied\",\"message\":$context.error.messageString},\"requestId\":\"$context.requestId\"}" GatewayResponseApiConfigurationError: Type: 'AWS::ApiGateway::GatewayResponse' Properties: RestApiId: Ref: 'ApiGatewayRestApi' ResponseType: API_CONFIGURATION_ERROR ResponseTemplates: application/json: "{\"error\":{\"code\":\"custom-500-api-configuration-error\",\"message\":$context.error.messageString},\"requestId\":\"$context.requestId\"}" GatewayResponseAuthorizerConfigurationError: Type: 'AWS::ApiGateway::GatewayResponse' Properties: RestApiId: Ref: 'ApiGatewayRestApi' ResponseType: AUTHORIZER_CONFIGURATION_ERROR ResponseTemplates: application/json: "{\"error\":{\"code\":\"custom-500-authorizer-configuration-error\",\"message\":$context.error.messageString},\"requestId\":\"$context.requestId\"}" GatewayResponseAuthorizerFailure: Type: 'AWS::ApiGateway::GatewayResponse' Properties: RestApiId: Ref: 'ApiGatewayRestApi' ResponseType: AUTHORIZER_FAILURE ResponseTemplates: application/json: "{\"error\":{\"code\":\"custom-500-authorizer-failure\",\"message\":$context.error.messageString},\"requestId\":\"$context.requestId\"}" GatewayResponseBadRequestBody: Type: 'AWS::ApiGateway::GatewayResponse' Properties: RestApiId: Ref: 'ApiGatewayRestApi' ResponseType: BAD_REQUEST_BODY ResponseTemplates: application/json: "{\"error\":{\"code\":\"custom-400-bad-request-body\",\"message\":$context.error.messageString},\"requestId\":\"$context.requestId\"}" GatewayResponseBadRequestParameters: Type: 'AWS::ApiGateway::GatewayResponse' Properties: RestApiId: Ref: 'ApiGatewayRestApi' ResponseType: BAD_REQUEST_PARAMETERS ResponseTemplates: application/json: "{\"error\":{\"code\":\"custom-400-bad-request-parameters\",\"message\":$context.error.messageString},\"requestId\":\"$context.requestId\"}" GatewayResponseExpiredToken: Type: 'AWS::ApiGateway::GatewayResponse' Properties: RestApiId: Ref: 'ApiGatewayRestApi' ResponseType: EXPIRED_TOKEN ResponseTemplates: application/json: "{\"error\":{\"code\":\"custom-403-expired-token\",\"message\":$context.error.messageString},\"requestId\":\"$context.requestId\"}" GatewayResponseIntegrationFailure: Type: 'AWS::ApiGateway::GatewayResponse' Properties: RestApiId: Ref: 'ApiGatewayRestApi' ResponseType: INTEGRATION_FAILURE ResponseTemplates: application/json: "{\"error\":{\"code\":\"custom-504-integration-failure\",\"message\":$context.error.messageString},\"requestId\":\"$context.requestId\"}" GatewayResponseIntegrationTimeout: Type: 'AWS::ApiGateway::GatewayResponse' Properties: RestApiId: Ref: 'ApiGatewayRestApi' ResponseType: INTEGRATION_TIMEOUT ResponseTemplates: application/json: "{\"error\":{\"code\":\"custom-504-integration-timeout\",\"message\":$context.error.messageString},\"requestId\":\"$context.requestId\"}" GatewayResponseInvalidApiKey: Type: 'AWS::ApiGateway::GatewayResponse' Properties: RestApiId: Ref: 'ApiGatewayRestApi' ResponseType: INVALID_API_KEY ResponseTemplates: application/json: "{\"error\":{\"code\":\"custom-403-invalid-api-key\",\"message\":$context.error.messageString},\"requestId\":\"$context.requestId\"}" GatewayResponseInvalidSignature: Type: 'AWS::ApiGateway::GatewayResponse' Properties: RestApiId: Ref: 'ApiGatewayRestApi' ResponseType: INVALID_SIGNATURE ResponseTemplates: application/json: "{\"error\":{\"code\":\"custom-403-invalid-signature\",\"message\":$context.error.messageString},\"requestId\":\"$context.requestId\"}" GatewayResponseMissingAuthenticationToken: Type: 'AWS::ApiGateway::GatewayResponse' Properties: RestApiId: Ref: 'ApiGatewayRestApi' ResponseType: MISSING_AUTHENTICATION_TOKEN ResponseTemplates: application/json: "{\"error\":{\"code\":\"custom-403-missing-authentication-token\",\"message\":$context.error.messageString},\"requestId\":\"$context.requestId\"}" GatewayResponseQuotaExceeded: Type: 'AWS::ApiGateway::GatewayResponse' Properties: RestApiId: Ref: 'ApiGatewayRestApi' ResponseType: QUOTA_EXCEEDED ResponseTemplates: application/json: "{\"error\":{\"code\":\"custom-429-quota-exceeded\",\"message\":$context.error.messageString},\"requestId\":\"$context.requestId\"}" GatewayResponseRequestTooLarge: Type: 'AWS::ApiGateway::GatewayResponse' Properties: RestApiId: Ref: 'ApiGatewayRestApi' ResponseType: REQUEST_TOO_LARGE ResponseTemplates: application/json: "{\"error\":{\"code\":\"custom-413-request-too-large\",\"message\":$context.error.messageString},\"requestId\":\"$context.requestId\"}" GatewayResponseResourceNotFound: Type: 'AWS::ApiGateway::GatewayResponse' Properties: RestApiId: Ref: 'ApiGatewayRestApi' ResponseType: RESOURCE_NOT_FOUND ResponseTemplates: application/json: "{\"error\":{\"code\":\"custom-404-resource-not-found\",\"message\":$context.error.messageString},\"requestId\":\"$context.requestId\"}" GatewayResponseThrottled: Type: 'AWS::ApiGateway::GatewayResponse' Properties: RestApiId: Ref: 'ApiGatewayRestApi' ResponseType: THROTTLED ResponseTemplates: application/json: "{\"error\":{\"code\":\"custom-429-throttled\",\"message\":$context.error.messageString},\"requestId\":\"$context.requestId\"}" GatewayResponseUnauthorized: Type: 'AWS::ApiGateway::GatewayResponse' Properties: RestApiId: Ref: 'ApiGatewayRestApi' ResponseType: UNAUTHORIZED ResponseTemplates: application/json: "{\"error\":{\"code\":\"custom-401-unauthorized\",\"message\":$context.error.messageString},\"requestId\":\"$context.requestId\"}" GatewayResponseUnauthorizedMediType: Type: 'AWS::ApiGateway::GatewayResponse' Properties: RestApiId: Ref: 'ApiGatewayRestApi' ResponseType: UNSUPPORTED_MEDIA_TYPE ResponseTemplates: application/json: "{\"error\":{\"code\":\"custom-415-unsupported-media-type\",\"message\":$context.error.messageString},\"requestId\":\"$context.requestId\"}" #### Gateway Response END
-
Código Base : https://gist.github.com/jonatassaraiva/4c33dd8225605c02318cd71a55b2335d
2.2) Ejecución de Serverless Local 🔝
Ver
-
Por defecto tenemos configurado una lambda llamada hello a través de su función .msg
-
Comprobamos la config generada.
-
Además tenemos configurada la seguridad y manejo de responses por parte de la Api Gateway, esta nos provera un token de acceso (x-api-key) para el acceso a cada lambda.
-
Levantamos serverless con el comando
sls offline start
oserverless offline start
-
Visualizamos el endpoint local que serverless nos genera..
Starting Offline at stage dev (us-east-1) Offline [http for lambda] listening on http://localhost:3002 Function names exposed for local invocation by aws-sdk: * hello: api-maquinas-industriales-offline-hello Remember to use 'x-api-key' on the request headers. Key with token: 'd41d8cd98f00b204e9800998ecf8427e' GET | http://localhost:4000/hello POST | http://localhost:4000/2015-03-31/functions/hello/invocations Server ready: http://localhost:4000
-
Abrimos alguna herramienta para generar peticiones http (ej: Postman) y generamos una request al endpoint generado junto con la api key de nuestro gateway
-
Seleccionamos método
GET
, luego escribimos el endpointhttp://localhost:4000/hello
, seguidamente pestañaHeaders
enKEY
colocamosx-api-key
y enVALUE
colocamos el token generadod41d8cd98f00b204e9800998ecf8427e
. -
Procedemos a ejecutar la request y podemos comprobar la metadata de la lambda en la consola de postman
-
También obtenemos la respuesta por consola..
GET /hello (λ: hello) (λ: hello) RequestId: 63fc1719-ae56-4d56-8296-87d45b44fc96 Duration: 124.64 ms Billed Duration: 125 ms
2.3) SSM e IAM 🔝
2.3.1) Instalación y Configuración de SSM Local 🔝
Ver
-
Instalamos el plugin
npm install serverless-offline serverless-offline-ssm --save-dev
-
Agregamos el complemento dentro del serverless.yml. Es importante mantener el orden siguiente (serverless-offline siempre último por temas de compatibilidad).
plugins: - serverless-offline-ssm - serverless-offline
-
Dentro del proyecto creamos el archivo
serverless.env.yml
que será el que contenga los valores de las variables de entorno de nuestro proyecto -
Dentro de dicho archivo colocamos una variable test para comprobar su funcionamiento..
HELLO_TEST : ${ssm:/hello_test}
-
Relacionamos el arhivo
serverless.env.yml
dentro delserverless.yml
agregando el parametroenviroment
junto a dicho archivo..provider: name: aws runtime: nodejs14.x stage: offline region : us-west-1 memorySize: 512 timeout : 10 environment: ${file(serverless.env.yml)}
-
Seguidamente vamos a configurar el ssm dentro del bloque
custom
para el archivoserverless.yml
-
Agregamos el stage (IMPORTANTE RESPETAR EL STAGE DEL PROVIDER Y DEL SSM) y la variable test, quedando..
custom: serverless-offline-ssm: stages: - offline ssm: '/hello_test': 'HELLO SSM' serverless-offline: httpPort: 4000
-
Por último utilizamos dicha variable en la lambda declarada, para invocar una variable de entorno utilizamos
process.env.variable
. -
Ejecutar serverless, es recomendable luego de cada instalación y agregado de plugin reiniciar el IDE para que los cambios se produzcan de forma correcta.
'use strict'; module.exports.hello = async (event) => { return { statusCode: 200, body: JSON.stringify( { message: process.env.HELLO_TEST, input: event, }, null, 2 ), }; };
-
Ejecutamos el programa, realizamos la petición get a través de postman a nuestra lambda y podemos visualizar la metadata de la misma junto con nuestra variable de entorno.
-
Salida Esperada..
{ "message": "HELLO SSM", "input": { "body": null, "cookies": [], "headers": { "x-api-key": "d41d8cd98f00b204e9800998ecf8427e", "content-type": "application/json", "user-agent": "PostmanRuntime/7.29.2", "accept": "*/*", "cache-control": "no-cache", "postman-token": "ea2e186f-732f-450e-abf2-4572b3e0206b", "host": "localhost:4000", "accept-encoding": "gzip, deflate, br", "connection": "keep-alive", "content-length": "163" }, "isBase64Encoded": false, "pathParameters": null, "queryStringParameters": null, "rawPath": "/hello", "rawQueryString": "", "requestContext": { "accountId": "offlineContext_accountId", "apiId": "offlineContext_apiId", "authorizer": { "jwt": {} }, "domainName": "offlineContext_domainName", "domainPrefix": "offlineContext_domainPrefix", "http": { "method": "GET", "path": "/hello", "protocol": "HTTP/1.1", "sourceIp": "127.0.0.1", "userAgent": "PostmanRuntime/7.29.2" }, "requestId": "offlineContext_resourceId", "routeKey": "GET hello", "stage": "$default", "time": "31/Oct/2022:23:49:02 -0300", "timeEpoch": 1667270942415 }, "routeKey": "GET hello", "stageVariables": null, "version": "2.0" } }
3.0) Arquitectura Lambda Function, Api gateway y Bucket S3 🔝
3.1) Registro de Objectos Json Bucket S3 🔝
3.2) Instalación y Configuración de Amazon Bucket S3 🔝
Ver
-
Amazon Simple Storage Service (Amazon S3) es un servicio de almacenamiento de objetos que ofrece escalabilidad, disponibilidad de datos, seguridad y rendimiento líderes en el sector.
-
Vamos a instalar el plugin en local
npm install serverless-s3-local --save-dev
-
Agregamos el mismo en la sección plugin del .yml, quedando...
plugins: - serverless-s3-local - serverless-offline-ssm - serverless-offline
-
Ejecutar serverless, es recomendable luego de cada instalación y agregado de plugin reiniciar el IDE para que los cambios se produzcan de forma correcta.
-
Seguidamente vamos a declarar una lambda de tipo bucket que nos gestione toda la metadata para las acciones que se realice en tiempo de ejecución.
-
Definimos el path de dicha lambda en el archivo
serverless.yml
functions: bucketS3: handler : src/lambdas/bucketS3.config events: - s3: local-bucket event: s3:*
-
Definimos la configuración y nombre de dicho bucket
resources: Resources: Bucket: Type: AWS::S3::Bucket Properties: BucketName: FILE_UPLOAD_BUCKET
-
Creamos dicha lambda
bucketS3.js
dentro desrc/lambdas/
//=================== METADATA BUCKET ================= module.exports.config = (event, context) => { console.log(JSON.stringify(event)); console.log(JSON.stringify(context)); console.log(JSON.stringify(process.env)); }; //=================== FIN METADATA BUCKET =================
-
Ejecutamos serverless y verificamos que no surjan errores.
3.3) Escritura de Objetos 🔝
Ver
-
Los objetos son las entidades de datos en sí, es decir, nuestros archivos. Un object almacena tanto los datos como los metadatos necesarios para S3.
-
Para este ejemplo vamos a modulizar código y abstraernos de las lambdas con respecto a las operaciones de escritura y lectura para el bucket, por ende crearemos funciones que realicen esto.
-
Creamos un nuevo directorio
src/utils/bucket
. -
Dentro del mismo el archivo
appendBucket.js
-
Crearemos una función que espere como parámetro el tipo de objeto a almacenar y toda la lógica del bucket dentro de esta.
-
Para utilizar la funcionalidad de los buckets es necesario importar el skd de aws, de esta forma podremos instanciar un objeto del tipo S3Client. Para este objeto se le podrán pasar las credenciales necesarias para trabajar en local (S3ERVER) y declarar otros params
-
Seguidamente utilizaremos el método send para indicar el nombre del bucket (
Bucket
) y su llave (key
). Además de indicarle el objeto a almacenar (Body
).//Imports "use strict"; const { S3Client, PutObjectCommand } = require("@aws-sdk/client-s3"); //======= APPEND BUCKET =========== async function put(appendData) { try { const client = new S3Client({ forcePathStyle: true, credentials: { accessKeyId: "S3RVER", // This specific key is required when working offline secretAccessKey: "S3RVER", }, endpoint: "http://localhost:4569", }); client.send( new PutObjectCommand({ Bucket: "local-bucket", Key: "bucketS3.json", Body: await appendData, }) ); } catch (error) { console.log(error); } } //======= END APPEND =========== module.exports = { put };
-
Seguidamente configuramos una lambda, es necesario su definición en el serverless.yml
functions: createMachine: handler: src/lambdas/createMachine.post description: STORING INDUSTRIAL MACHINES IN BUCKET S3. events: - httpApi: method: POST path: /dev/machine/ private: true
-
Crearemos la lambda configurada dentro de
src/lambdas/
para que ejecute dicho proceso de escritura del bucket.