Projecte d'un servei web per determinar si es poden encabir diversos elements en un contenidor i si es així en quina disposició i ordre. El servei ha de poder gestionar peticions de diversos clients, a on cada client té uns contenidors preconfigurats diferents. El servei s'ha de poder desplegar en diferents configuracions:
El servei es desenvoluparà amb NodeJS, ExpressJS El loggin es farà amb Morgan i Winston. Les API REST es faran amb Open API i swagger. Les dades es guardaran en una BD MySql. Es farà servir una BD Redis com a cache de la MySql. Es testejarà amb mocha i artillery.
Posteriorment s'afegirà un altre servei per gestionar les claus de connexió dels clients, obtenir estadístiques de connexió, poder bloquejar claus, renovar-les i crear-ne de noves.
Com a útlim punt s'afegirà una base de dades Redis per tal de guardar en memòria els contenidors preconfigurats i les claus de connexió dels clients per agilitzar l'aplicació.
Hem de tenir en compte les diferents opcions de desplegament de les BD, ja que les podrem tenir https://cloud.google.com/blog/products/databases/to-run-or-not-to-run-a-database-on-kubernetes-what-to-consider:
Es vol comprobar si una aplicació serverless feta amb contenidors realment té els següents avantatges:
Per dissenyar les API REST s'ha de tenir en compte:
Exemple de crida amb HATEOAS:
{
“id”: 1,
“name”: “Blog”,
“links”: [
{
“href”: “1/posts”,
“rel”: “posts”,
“type” : “GET”
},
{
“href”: “1/comments”,
“rel”: “comments”,
“type” : “GET”
}
]
}
Exemple d'array d'errors:
{
“errors”: [
{
“code”: “10003”,
“message”: “invalid address field”
},
{
“code”: “10004”,
“message”: “birthday field is requried”
}
]
}
Es segueix https://semver.org/
Given a version number MAJOR.MINOR.PATCH, increment the:
MAJOR version when you make incompatible API changes
MINOR version when you add functionality in a backwards compatible manner
PATCH version when you make backwards compatible bug fixes
Important: crear una nova versió només quan sigui necessari i es trenqui compatibilitat amb la versió anterior.
Passos:
api-docs.js
que conté la documentació de la apiPer implementar la connexió amb MySql es consideren aquests mòduls per Nodejs i Expressjs:
Prioritzem rendiment sobre compatibilitat, per tant descartem les ORM i query builders ja que afegixen capes i lentitut Dels dos drivers MySql triem mysql2 ja que ofereix un rendiment superior i conté els elements bàsics per treballar amb MySql (pool de connexions i transaccions). Per tal de treballar de forma concurrent i sense bloqueigs a partir de múltiples serveis contra la mateixa base de dades, per agafar registres i fer-hi operacions de modificació o esborrat es pot fer de diferents maneres (no fer res i el que actualitza l'últim guanya, concurrència optimista en que es suposa que hi haurà pocs conflictes i l'usuari afectat no li farà res tornar-ho a intentar), concurrència pessimista on es suposen molts conflictes i que els usuaris afectats no voldran reintantar-ho.
Nosaltres farem servir concurrència optimista amb versionat.
Gotcha! Timezones in nodejs and mysql https://medium.com/@magnusjt/gotcha-timezones-in-nodejs-and-mysql-b39e418c9d3
MySql2 Timezone https://github.com/sidorares/node-mysql2/issues/642
MySQL no permet guardar el timezone en camps DATETIME o TIMESTAMP, per tal de poder guardar i recuperar aquestes dates correctament, es guardarà sempre com a UTC i serà en el client que es transformà al time zone que calgui.
A mysql, DATE i DATETIME s'emmagatzemen com a cadenes simples. També s'envien i reben com a cadenes, sense tenir en compte les zones horàries.
Interpretar DATETIME com a data nova ("AAAA-MM-DD HH:mm:ss"). Això pot ser correcte si el que ha emmagatzemat el DATETIME es troba a la mateixa zona horària que tu.
Per recuperar la data en format local es pot fer servir la funció CONVERT_TZ() en la SQL la consulta següent:
-- Convertir de UTC a la timezone de la connexió
SELECT id
, `code`
, CONVERT_TZ(date_start, '+00:00', @@session.time_zone)
, CONVERT_TZ(date_final, '+00:00', @@session.time_zone)
, `active`
, token
, notes
FROM cargo_loading.client;
Si es vol gestionar en Nodejs s'ha de tenir en compte:
let d1 = new Date(); // 28/4/2023, 18:09:40
console.log(d1); // 2023-04-28T16:09:40.561Z
console.log(d1.toISOString()); // '2023-04-28T16:09:40.561Z'
console.log(d1.toLocaleString()); // '28/4/2023, 18:09:40'
console.log(d1.toLocaleString('en-US')); // '4/28/2023, 6:09:40 PM'
console.log(d1.toLocaleString('es-ES')); // '28/4/2023, 18:09:40'
let d2 = new Date('2023-04-18T16:07:50');
console.log(d2); // 2023-04-18T14:07:50.000Z
console.log(d2.toISOString()); // '2023-04-18T14:07:50.000Z'
console.log(d2.toLocaleString()); // '18/4/2023, 16:07:50'
console.log(d2.toLocaleString('en-US')); // '4/18/2023, 4:07:50 PM'
console.log(d2.toLocaleString('es-ES')); // '18/4/2023, 16:07:50'
let d3 = new Date(2023, 04, 18, 16, 7, 50);
console.log(d3); // 2023-05-18T14:07:50.000Z
console.log(d3.toISOString()); // '2023-05-18T14:07:50.000Z'
console.log(d3.toLocaleString()); // '18/5/2023, 16:07:50'
console.log(d3.toLocaleString('en-US')); // '5/18/2023, 4:07:50 PM'
console.log(d3.toLocaleString('es-ES')); // '18/5/2023, 16:07:50'
let d4 = new Date('2023-04-18 16:07:50');
console.log(d4); // 2023-04-18T14:07:50.000Z
console.log(d4.toISOString()); // '2023-04-18T14:07:50.000Z'
console.log(d4.toLocaleString()); // '18/4/2023, 16:07:50'
console.log(d4.toLocaleString('en-US')); // '4/18/2023, 4:07:50 PM'
console.log(d4.toLocaleString('es-ES')); // '18/4/2023, 16:07:50'
https://lynn-kwong.medium.com/understand-the-basics-of-locks-and-deadlocks-in-mysql-part-i-92f229db0a https://lynn-kwong.medium.com/understand-the-basics-of-locks-and-deadlocks-in-mysql-part-ii-6beecd183345
Pojecte hostatjat al GitHub, per col·laborar treballar de la següent manera:
.env
amb la configuració del servei com a variables d'entornFitxer .env
d'exemple:
CLIENT_CODE="TEST"
SERVICE_CODE="onion-cargo-loading-service"
DB_HOST='localhost'
DB_PORT=3306
DB_DATABASE='cargo_loading'
DB_USER='user'
DB_PASSWORD='xxxxx'
DB_CONNECTION_LIMIT=10
Per veure totes les rutes del projecte executar-lo amb la camanda:
DEBUG=express:* node bin/www
Mes info a:
Per tal d'assegurar que abans de cada commit es passen tots els tests i que no es puja res a github que no funcioni s'afegeix un paquet que permet executar els tests en entron de desenvlupament amb els git hooks.
Per això afegim els paquets:
Hem de configurar el es-lint per assegurar que es respecta l'estil i no hi ha errors estàtics en el codi. Per defecte hem triat l'estandarjs. Les regles configurables es poden trobar aquí Hi fem algunes modificacions específiques de CodeBiting.
npm init @eslint/config
.
To check syntax, find problems, and enforce code style
CommonJS (require/exports)
None of these
No
Node
Standard: https://github.com/standard/standard
JavaScript
npm
.eslintrc.js
.eslintrc.js
en l'apartat rules
// Use 4 space identation to get the code more compact
// In switch-case ident case https://eslint.org/docs/latest/rules/indent#switchcase
indent: ['error', 4, { SwitchCase: 1 }],
// Use semicolons to make the code easier to read
semi: ['error', 'always']
Probem que funciona executant l'eslint d'un fitxer amb la comanda node ./node_modules/.bin/eslint yourfile.js
Instal·lem el paquet precommit: npm install pre-commit --save-dev
El paquet pre-commit executa els tests configurats al package.json
, per això hem d'afegir una secció indicant les comandes que s'executaran:
"scripts": {
"start": "node ./bin/www",
"test": "node ./node_modules/mocha/bin/mocha",
"test-apis": "node ./node_modules/mocha/bin/mocha ./test/api/",
"test-routes": "node ./bin/www & P1=$! && sleep 2 && node ./node_modules/mocha/bin/mocha ./test/routes/v1/ && kill $P1",
"test-eslint": "node ./node_modules/.bin/eslint app.js",
"nodemon": "nodemon ./bin/www"
},
"pre-commit": [
"test-apis",
"test-routes",
"test-eslint"
],
Podem veure que hem afegit els següents scripts:
node ./bin/www & P1=$!
aixequem el servei i ens guardem el PIDsleep 2
esperem 2 segons a que s'aixequi el serveinode ./node_modules/mocha/bin/mocha ./test/routes/v1/
executem els testskill $P1
tanquem el serveiEn la secció pre-commit
cridem els scripts que s'executaran abans de fer un commit. Hem de tenir en compte que:
commit -a -m "missatge"
Per tal de que quan es puja el projecte es torni a passar els testos, a dins de hithub s'afegeix l'action "Node.js" que crea le fitxer .github/workflows/node.js.yml
a on especifiquem els tests que s'han d'executar.
Ara cada vegada que fem commit, a part d'executar-se els tests pel git pre-commit es pujarà i s'executaran els tests al github. Aquesta execució de tests es farà per triplicat, una per cada versió de Node.js especificada en el fitxer de configuració del workflow .github/workflows/node.js.yml
.
Veure:
npm ci
bypasses a package’s package.json to install modules from a package’s lockfile. This ensures reproducible builds—you are getting exactly what you expect on every install. Mes infoPer veure els logs d'aquesta acció, a dins del projecte, a la pestanya "Actions" es poden veure les execucions dels workflows i el motiu d'error, si n'hi ha.
Info: https://docs.cypress.io/guides/getting-started/installing-cypress Per les dependències consulta: https://docs.cypress.io/guides/getting-started/installing-cypress#System-requirements
cd /your/project/path
npm install cypress --save-dev
Obre les eines client de Cypress: npx cypress open
Tria el tipus de test, en el nostre cas E2E
El primer cop s'haurà de configurar els tests, es a dir, crear les carpetes i els fitxers amb la configuració de cypress, només cal prémer continue i es crearà.
Engega l'aplicació o servei
Tria el navegador i Start E2E testing in XXXXXXX, apareixerà una nova finestra per començar a crear tests o executar els que estiguin configurats (a ./cypress/e2e).
Característiques:
Per tal de deplegar l'aplicació amb PM2 en un servidor farem servir un script que ens desplegui tots els components necessaris en un servidor Ubuntu de clouding.io
En el mateix servidor hi instalarem una BD MySQL per guardar les dades.
Opcions d'escalabilitat:
pm2 start app.js -i max
Exemple opció escalabilitat 1, amb fitxer ecosystem:
{
"apps" : [{
"name" : "worker-app",
"script" : "./worker.js",
"watch" : true,
"env": {
"NODE_ENV": "development"
},
"env_production" : {
"NODE_ENV": "production"
}
},{
"name" : "api-app",
"script" : "./api.js",
"instances" : 4,
"exec_mode" : "cluster"
}]
}
Per poder desplegar amb docker compose, swarm kubernetes o Cloud Run primer hem de crear l'aplicació en un contenidor.
# Mirem quines imatges tenim en local
$ docker images
# Creem una imatge en local a partir del Dockerfile
# docker build --tag <nom de la nova imatge> <ruta del fitxer dockerfile>
# Posem una nova versio cada vegada
$ docker build --tag codebiting/onion-cargo-loading:v1 .
# Verifiquem que la imatge s’ha creat correctament, per això mirem si la imatge existeix.
$ docker images
# Executem la imatge en mode interactiu
$ docker run -it -p 8080:8080 -d codebiting/onion-cargo-loading:v1
# Verifiquem que el contenidor està actiu, amb els ports correctes i mirem els logs que deixa:
$ docker ps
$ docker logs <container id>
$ docker inspcet <container id>
# Veure tots els contenidors (els finalitzats i els en execució)
$ docker ps -a
# Comprovem que l’aplicació funciona executant un navegador web l’adreça:
[http://<my.servlet.host>:8080/api-documentation](http://localhost:8080/api-documentation/)
# Entrem a dins del contenidor pel que sigui:
$ docker exec -it <container id> /bin/bash
# Si esta parat ho podem fer d aquesta manera
$ docker start -ai <container id>
# Parem l’execució del contenidor. Podem fer-ho de dues maneres diferents:
$ docker stop <container id>
$ docker kill <container id>
https://docs.docker.com/compose/ https://docs.docker.com/compose/production/ https://www.educative.io/blog/docker-compose-tutorial
Característiques:
Per tal de deplegar l'aplicació amb docker compose en un servidor farem servir un script que ens desplegui tots els components necessaris en un servidor Ubuntu de clouding.io
Al mateix servidor hi instalarem un docker amb MySQL per guardar-hi les dades.
https://docs.docker.com/engine/swarm/
Característiques:
Característiques:
https://www.youtube.com/watch?v=jvZXbJv6qJ4 https://towardsdatascience.com/how-to-connect-to-gcp-cloud-sql-instances-in-cloud-run-servies-1e60a908e8f2
Idea: API Gateway -> Cloud RUN -> VPC Network (Private IP) -> Cloud SQL (MySQL)
Step 1 - Set permissions:
Step 2 - Set up Cloud MySQL:
Step 2.1 - Set up a Redis (5GB => US$295.65/month)
Step 3 - Setup VPC:
Step 4 - Cloud RUN:
Container:
Exemple de com fer servir variables d'entorn enlloc de fitxer de configuració durant la crida d'inici de la app:
CLIENT_CODE="TEST" SERVICE_CODE="onion-cargo-loading-service" DB_HOST='localhost' DB_PORT=3306 DB_DATABASE='cargo_loading' DB_USER='cbwms' DB_PASSWORD='xxxxx' DB_CONNECTION_LIMIT=10 npm start
Step 5 - Configure an API Gateway using API Keys:
Docs:
Create an API Gateway public with an API Key:
https://g2l-integration-secured-8g7k8sqn.ew.gateway.dev
cloudrunapigatewaysecured-3fgqzb33trv87.apigateway.g2l-integration.cloud.goog
gcloud services enable cloudrunapigatewaysecured-3fgqzb33trv87.apigateway.g2l-integration.cloud.goog
https://g2l-integration-secured-8g7k8sqn.ew.gateway.dev/v1/products
https://g2l-integration-secured-8g7k8sqn.ew.gateway.dev/v1/products
Secure the APY key with restrictions https://cloud.google.com/docs/authentication/api-keys#creating_an_api_key:
Mode de desplegament | Màxima escalabilitat | Temps per canviar l'escalabilitat |
---|---|---|
PM2 | De 1 a N serveis per servidor | Segons a minuts: s'ha de modificar l'ecosystem i reiniciar PM2 |
Docker compose | ||
Docker swarm | ||
Kubernetes | ||
Cloud Run |
Escenaris:
Mode de desplegament | Escenari 1 | Escenari 2 | Escenari 3 | Escenari 4 |
---|---|---|---|---|
PM2 1 servei | ||||
PM2 mode cluster + Redis | ||||
Docker compose | ||||
Docker compose + Redis | ||||
Docker swarm + Redis | ||||
Kubernetes + Redis | ||||
Cloud Run + Redis |
V 0.0.0 - Característiques