-
Notifications
You must be signed in to change notification settings - Fork 1
sprint3 documento tecnico
A partir del diagrama de despliegue de los servicios de negocio y de infraestructura implementados en el proyecto, se procede con la revisión de los elementos de arquitetectura definidos en la visión.

- Mantener una alta cohesión
- Mantener un bajo acoplamiento
- Reducir el tamaño de los módulos - dividir módulos.
Para favorecer el atributo de facilidad de modificación, se diseñaron e implementaron componentes tipo microservicios, aplicando tácticas de componentes de de bajo tamaño, con única responsabilidad, bajo acoplamiento y alta cohesión, permitiendo que a futuro los cambios solicitados a futuro impacten solo los servicios requeridos, sin genear impacto en el resto de servicios del sistema.
Para la implementción de cada micro, inicialmente se define el modelo del dominio de negocio, por ejemplo para la prueba, https://github.com/andergcp2/backend-proyecto-final/blob/main/pruebas_cmd/model.py:
class Test(db.Model):
__tablename__ = "tests"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50))
numQuestions = db.Column(db.Integer)
minLevel = db.Column(db.Integer)
profiles = db.relationship('Profile', cascade='all, delete, delete-orphan') #, back_populates="test"
techSkills = db.relationship("TechnicalSkill", cascade='all, delete, delete-orphan')
questions = db.relationship("Question", cascade='all, delete, delete-orphan')
createdAt = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
...
class Question(db.Model):
__tablename__ = "tests_questions"
id = db.Column(db.Integer, primary_key=True)
testId = db.Column(db.Integer, db.ForeignKey('tests.id'))
question = db.Column(db.String(512))
level = db.Column(db.Integer)
url = db.Column(db.String(256))
answers = db.relationship("Answer", cascade='all, delete, delete-orphan') # , back_populates="question"
# test = db.relationship("Test", back_populates="questions")
# def __repr__(self):
# return f'<Question "{self.testId, self.level, self.question, self.answers}">'
class Answer(db.Model):
__tablename__ = "tests_answers"
id = db.Column(db.Integer, primary_key=True)
questionId = db.Column(db.Integer, db.ForeignKey('tests_questions.id'))
answer = db.Column(db.String(240))
correct = db.Column(db.Boolean, default=False)
# question = db.relationship("Question", back_populates="answers")
# def __repr__(self):
# return f'<Answer "{self.questionId, self.answer, self.correct}">'
Luego se implementan los métodos de cada servicio a exponer, https://github.com/andergcp2/backend-proyecto-final/blob/main/pruebas_cmd/view.py
class CreateTest(Resource):
def post(self):
filename = None
if 'file' not in request.files:
return "Test was not created - file missing", 400
file = request.files['file']
if file.filename == '':
return "Test was not created - no selected file", 400
...
#data = request.get_json()
data = json.load(file)
name = type = numQuestions = minLEvel = profiles = techSkills = questions = None
...
for item in data["questions"]:
if "question" not in item or item["question"] is None:
return "Test was not created - question is required", 400
elif "level" not in item or item["level"] is None:
return "Test was not created - level's question is required", 400
elif "url" not in item or item["url"] is None:
return "Test was not created - url's question is required", 400
elif "answers" not in item or item["answers"] is None:
return "Test was not created - answers' question are required", 400
for item_answer in item["answers"]:
if "answer" not in item_answer or item_answer["answer"] is None:
return "Test was not created - answer is required", 400
elif "correct" not in item_answer or item_answer["correct"] is None:
return "Test was not created - correctness' answer is required", 400
...
for item in questions:
new_question = Question(question=item["question"], level=item["level"], url=item["url"], testId=new_test.id)
for item_answer in item["answers"]:
new_answer = Answer(answer=item_answer["answer"], correct=item_answer["correct"], questionId=new_question.id) #questionIdId=item_tech["id"]
new_question.answers.append(new_answer)
#print(" answer: ", new_answer)
new_test.questions.append(new_question)
#print(" question: ", new_question)
#print("")
#print("done: ", new_test)
db.session.add(new_test)
db.session.commit()
test_created = Test.query.filter(Test.name == name).filter(Test.minLevel==minLevel).order_by(Test.createdAt.desc()).first()
return test_schema.dump(test_created), 201
Posteriormente se definen las rutas de los endpoints y su respectivo controller, https://github.com/andergcp2/backend-proyecto-final/blob/main/pruebas_cmd/app.py
api = Api(app)
api.add_resource(CreateTest, '/tests' )
api.add_resource(HealthCheck,'/tests/ping' )
En este punto procedemos con las pruebas unitaras para cada flujo dentro de cada servicio, https://github.com/andergcp2/backend-proyecto-final/blob/main/pruebas_cmd/tests/test_pruebas_cmd.py, utilizando la libreria faker para generar la data de la prueba:
class PruebasCmd(TestCase):
def setUp(self):
self.client = app.test_client()
self.fake = Faker()
...
numQuestions = self.fake.random_element(elements=(5, 10, 15, 20))
testProfiles = [
{"profile": name1 + self.fake.job()},
{"profile": name1 + self.fake.job()}
]
testTechSkills = [
{"skill": self.fake.word(ext_word_list=skills1)},
{"skill": self.fake.word(ext_word_list=skills2)}
]
testQuestions = []
for x in range(minLevel, 6):
for y in range(numQuestions*3):
answers = []
for z in range(5):
answers.append({"answer": name1 + self.fake.bs(), "correct": False})
answers[self.fake.random_int(0, 4)] = {"answer": name1 + name3 + " "+ self.fake.bs(), "correct": True}
question = {
"question": name1 + name2 + self.fake.bs() +"?",
"level": x,
"url": self.fake.url(),
"answers": answers
}
testQuestions.append(question)
self.prueba = {
"name": "Test "+ name1 + name2 + name3,
"numQuestions": numQuestions,
"minLevel": minLevel,
"profiles": testProfiles,
"techSkills": testTechSkills,
"questions": testQuestions,
}
#self.prueba = copy.deepcopy(self.test)
self.data = {}
self.endpoint = '/tests'
self.endpoint_health = '/tests/ping'
...
def test_health_check(self):
req_health = self.client.get(self.endpoint_health, headers={'Content-Type': 'application/json'})
self.assertEqual(req_health.status_code, 200)
def test_create_test_400_file_missing(self):
resp_create = self.client.post(self.endpoint, headers=self.headers_token, data=json.dumps(self.prueba))
data = json.loads(resp_create.get_data())
self.assertEqual(resp_create.status_code, 400)
self.assertEqual(data, "Test was not created - file missing")
def test_create_test_400_no_selected_file(self):
input_json = json.dumps({}, indent=4).encode("utf-8")
self.data['file'] = FileStorage( stream=io.BytesIO(input_json), content_type="application/json", filename='')
resp_create = self.client.post(self.endpoint, headers=self.headers_token, content_type='multipart/form-data', data=self.data) # follow_redirects=True
data = json.loads(resp_create.get_data())
self.assertEqual(resp_create.status_code, 400)
self.assertEqual(data, "Test was not created - no selected file")
...
def test_create_test_412_num_questions(self):
self.prueba["numQuestions"] = self.fake.country_code()
input_json = json.dumps(self.prueba, indent=4).encode("utf-8")
self.data['file'] = FileStorage( stream=io.BytesIO(input_json), content_type="application/json", filename="test.json", )
resp_create = self.client.post(self.endpoint, headers=self.headers_token, content_type='multipart/form-data', data=self.data) # follow_redirects=True
data = json.loads(resp_create.get_data())
self.assertEqual(resp_create.status_code, 412)
self.assertEqual(data, "Test was not created - number of questions is not valid")
def test_create_test_412_num_questions_greater_than_50(self):
self.prueba["numQuestions"] = self.fake.random_int(51, 99)
input_json = json.dumps(self.prueba, indent=4).encode("utf-8")
self.data['file'] = FileStorage( stream=io.BytesIO(input_json), content_type="application/json", filename="test.json", )
resp_create = self.client.post(self.endpoint, headers=self.headers_token, content_type='multipart/form-data', data=self.data) # follow_redirects=True
data = json.loads(resp_create.get_data())
self.assertEqual(resp_create.status_code, 412)
self.assertEqual(data, "Test was not created - number of questions is not valid")
def test_create_test_201(self):
# print()
# print(json.dumps(self.prueba, indent=4))
input_json = json.dumps(self.prueba, indent=4).encode("utf-8") #sort_keys=True, indent=4
self.data['file'] = FileStorage( stream=io.BytesIO(input_json), content_type="application/json", filename="test.json", )
resp_create = self.client.post(self.endpoint, headers=self.headers_token, content_type='multipart/form-data', data=self.data) # follow_redirects=True
data = json.loads(resp_create.get_data())
self.assertEqual(resp_create.status_code, 201)
self.assertEqual(data["name"], self.prueba["name"])
self.assertEqual(data["minLevel"], self.prueba["minLevel"])
self.assertEqual(data["numQuestions"], self.prueba["numQuestions"])
Una vez pasadas con éxito las pruebas unitarias y además de cumplir un coverage superior al 80%:
cd ../pruebas_cmd
python -m unittest tests/test_pruebas_cmd.py -v
pipenv run pytest --cov=. -v -s --cov-fail-under=80
se genera el dockerfile que será utilizado por el pipeline para construir la imagen y cargarla al respectivo repositorio ECR del servicio:
https://github.com/andergcp2/backend-proyecto-final/blob/main/pruebas_cmd/Dockerfile
FROM python:3.11-alpine
LABEL author="m.agonf@uniandes.edu.co"
EXPOSE 80
WORKDIR /app
COPY requirements.txt /app
RUN apk add --update curl
RUN pip install -r requirements.txt
COPY . /app
CMD [ "flask", "run", "--host=0.0.0.0", "--port=80" ]
El pipeline CI/CD llama un workflow reutilizable por cada servicio que se requiera desplegar, previa creación de la respectiva infraestructura, utilizando terraform:
https://github.com/andergcp2/backend-proyecto-final/blob/main/.github/workflows/dev.yml
- Detectar fallas: healthcheck
- Enmascarar fallas: identificar y retirar componente defectuoso
- Recuperación de fallos: reinicio de componente
Para favorecer el atributo de disponibilidad se implementa el estilo de arquitectura Microservicios para aprovechar las funcionalidades de Elastic Container Service para la detección de fallas, el retiro de componente(s) defectuoso(s) y su respectivo reinicio.
En cada micro se implementa un endpoint para el health-check, por ejemplo:
https://github.com/andergcp2/backend-proyecto-final/blob/main/pruebas_taker/view.py
class HealthCheck(Resource):
def get(self):
return "ok"
https://github.com/andergcp2/backend-proyecto-final/blob/main/pruebas_taker/app.py
api = Api(app)
api.add_resource(HealthCheck, '/tests-taker/ping')
api.add_resource(PruebaInit, '/tests-taker/init/<candidatoId>/<pruebaId>')
api.add_resource(PruebaNext, '/tests-taker/next/<candidatoId>/<pruebaId>')
api.add_resource(PruebaDone, '/tests-taker/done/<candidatoId>/<pruebaId>')
Y luego, para cada micro, se configura la respectiva ruta de healt-check dentro del archivo de variables terraform, https://github.com/andergcp2/backend-proyecto-final/blob/main/aws-tf/terraform.tfvars:
"pruebas-taker" = {
name = "pruebas-taker"
...
alb_target_group = {
port = 80
protocol = "HTTP"
path_pattern = ["/tests-taker*"]
health_check_path = "/tests-taker/ping"
priority = 1
}
...
}
La cual será configurada dentro del target group del Application Load Balancer:
https://github.com/andergcp2/backend-proyecto-final/blob/main/aws-tf/modules/alb/main.tf
#Dynamically create the alb target groups for app services
resource "aws_alb_target_group" "alb_target_group" {
for_each = var.target_groups
name = "${lower(each.key)}-tg"
port = each.value.port
protocol = each.value.protocol
target_type = "ip"
vpc_id = var.vpc_id
health_check {
path = each.value.health_check_path
protocol = each.value.protocol
}
}
Application Load Balancer:

El target group se evidencia el estado saludable del servicio:

Finalmente, a nivel de infraestructura, en el archivo de variables terraform https://github.com/andergcp2/backend-proyecto-final/blob/main/aws-tf/terraform.tfvars, se definen dos zonas de disponibilidad de la región us-east-1:
## Application configurations
account = 101526122836
region = "us-east-1"
profile = "default"
app_name = "abcjobs"
env = "dev"
app_services = ["interviews", "pruebas-taker", "pruebas-qry", "pruebas-cmd", "candidatos-tests", "candidatos-qry", "candidatos-cmd", "collaborators", "companies", "projects"]
#VPC configurations
cidr = "10.10.0.0/16"
availability_zones = ["us-east-1a", "us-east-1b"]
public_subnets = ["10.10.50.0/24", "10.10.51.0/24"]
private_subnets = ["10.10.0.0/24", "10.10.1.0/24"]
Para favorecer la escalabilidad, dentro de la táctica de manejo de recursos compartidos, se implementan múltiples copias de computación y múltiples copias de datos con el objetivo de crecer y decrecer horizontalmente de forma tal que permita atender la demanda de los diferentes servicios.
Para múltiples copias de computación, se definen para su respectivo aprovisionamiento en AWS, dentro del archivo de variables terraform https://github.com/andergcp2/backend-proyecto-final/blob/main/aws-tf/terraform.tfvars, los parámetros de auto-scaling (capacidad mínima y máxima, además de la política de uso de memoria y cpu) :
"pruebas-taker" = {
name = "pruebas-taker"
...
auto_scaling = {
max_capacity = 2
min_capacity = 1
cpu = {
target_value = 75
}
memory = {
target_value = 75
}
}
}

Para múltiples copias de datos, se define para su respectivo aprovisionamiento en AWS, dentro del archivo principal de terraform https://github.com/andergcp2/backend-proyecto-final/blob/main/aws-tf/main.tf, un Cluster ElastiCache for Redis:
resource "aws_elasticache_cluster" "redis" {
cluster_id = "abcjobs-redis-cluster"
engine = "redis"
node_type = "cache.t3.micro"
num_cache_nodes = 1
port = 6379
subnet_group_name = aws_elasticache_subnet_group.default.name
security_group_ids = [aws_security_group.redis.id]
#security_group_ids = ["${aws_security_group.redis.id}"]
#parameter_group_name = "default.redis3.2"
tags = {
Name = "redis-cluster"
}
}

El almacén de datos en memoria ElastiCache for Redis brinda latencias inferior a un milisegundo para aplicaciones en tiempo real, útil para favorecer los ASRs de disponibilidad y latencia relacionados con la presentación de las pruebas técnicas, implementadas en el micro pruebas-taker, el cual recibe los parámetros de la cache como variable de ambiente, https://github.com/andergcp2/backend-proyecto-final/blob/main/pruebas_taker/app.py,
app.config['CACHE_HOST'] = str(os.environ.get("CACHE_PATH"))
app.config['CACHE_PORT'] = str(os.environ.get("CACHE_PORT"))
app.config['CANDIDATOS_QUERY'] = str(os.environ.get("CANDIDATOS_PATH"))
app.config['PRUEBAS_QUERY'] = str(os.environ.get("PRUEBAS_PATH"))
app.config['CANDIDATOS_PRUEBAS'] = str(os.environ.get("CANDIDATOS_PRUEBAS_PATH"))
Nota: el paso de las variables de ambiente se realiza en el archivo principal del módulo ECS de los scripts terraform del proyecto, las cuales se configuran previamente en AWS Secrets Manager:
aws secretsmanager create-secret --name redis_host --secret-string abcjobs-redis-cluster.kneypx.0001.use1.cache.amazonaws.com
aws secretsmanager create-secret --name redis_port --secret-string 6379

https://github.com/andergcp2/backend-proyecto-final/blob/main/aws-tf/modules/ecs/main.tf
container_definitions = jsonencode([
{
name = each.value.name
#image = "${var.account}.dkr.ecr.${var.region}.amazonaws.com/${lower(var.app_name)}-${lower(each.value.name)}:latest"
image = "${var.account}.dkr.ecr.${var.region}.amazonaws.com/${lower(each.value.name)}:latest"
cpu = each.value.cpu
memory = each.value.memory
essential = true
secrets = [
{
name = "CACHE_PATH"
valueFrom = "arn:aws:secretsmanager:us-east-1:101526122836:secret:redis_host-bDx9aw"
},
{
name = "CACHE_PORT"
valueFrom = "arn:aws:secretsmanager:us-east-1:101526122836:secret:redis_port-xRwupn"
},
{
name = "CANDIDATOS_PATH"
valueFrom = "arn:aws:secretsmanager:us-east-1:101526122836:secret:candidatos_qry_path-UqZKuK"
},
{
name = "PRUEBAS_PATH"
valueFrom = "arn:aws:secretsmanager:us-east-1:101526122836:secret:pruebas_qry_path-Mcg4ke"
},
...
Durante la presentación de la prueba, se consulta inicialmente el banco de preguntas y respiuesta de la prueba para su almacenamiento y uso a partir del caché, https://github.com/andergcp2/backend-proyecto-final/blob/main/pruebas_taker/view.py, utilizando como llave el id de la prueba y el id del candidato:
def deleteCache(self, key):
self.redis.delete(key)
print("delete-cache")
def setCache(self, key, data):
self.redis.set(key, json.dumps(data))
def getCache(self, key):
return json.loads(self.redis.get(key))
def setupCache(self, fase):
print("setup-cache")
print(fase, current_app.config['CACHE_HOST'], current_app.config['CACHE_PORT'] )
#self.redis = redis.Redis(host=current_app.config['CACHE_HOST'], port=current_app.config['CACHE_PORT'], decode_responses=True, ssl=True) #encoding="utf-8"
pool = redis.ConnectionPool(host=current_app.config['CACHE_HOST'], port=current_app.config['CACHE_PORT'], db=0)
self.redis = redis.Redis(connection_pool=pool)
try:
cache_is_working = self.redis.ping()
print(fase, "connected to redis")
except Exception as ex:
print(fase, 'exception: host could not be accessed: {}'.format(ex))
...
data = {
'pruebaId': prueba['id'],
'candidatoId': candidato['id'],
"totalQuestions": prueba['numQuestions'],
"numQuestion": 1,
"answersOK": 0,
"prueba": prueba,
"candidato": candidato,
}
idcache = pruebaId+"-"+candidatoId
deleteCache(self, idcache)
setCache(self, idcache, data)
test = getCache(self, idcache)
...
idcache = pruebaId+"-"+candidatoId
test = getCache(self, idcache)
# 404 si no existen los parametros como llave en la cache
if(test is None):
return "this test was not started by candidate", 404
# 412 no debe ser la ultima pregunta
if (numQuestion == totalQuestions):
return "this question should not be the last question {}/{}".format(numQuestion, totalQuestions), 412
idx = numQuestion
numQuestion +=1
respuestas = test['prueba']['questions'][idx-1]['answers']
for x in range(len(respuestas)):
if(respuestas[x]['id'] == answerId and respuestas[x]['correct']):
test['answersOK'] = test['answersOK'] +1
setCache(self, idcache, test)
#print("next-question ok ", idx, test['answersOK'])
answers = []
respuestas = test['prueba']['questions'][idx]['answers']
for x in range(len(respuestas)):
answers.append({"id": respuestas[x]['id'], "answer": respuestas[x]['answer']})
resp_next = {
'pruebaId': test['prueba']['id'],
'candidatoId': test['candidato']['id'],
'question': {'id': test['prueba']['questions'][idx]['id'], 'question': test['prueba']['questions'][idx]['question']},
'answers': answers,
'totalQuestions': test['prueba']['numQuestions'],
'numQuestion': numQuestion,
}
Finalmente, para múltiples copias de datos, con el objetivo de implementar el patrón CQRS, se configura un cluster de instancias de bases de datos Amazon Aurora, incluyendo una réplica en modo Read Only. De esta manera, se optimizan las consultas de los micros candidatos-qry y pruebas-qry, utilizados en el proceso de presentación de la prueba, favoreciendo atributos relacionados al desempeño.
https://github.com/andergcp2/backend-proyecto-final/blob/main/pruebas_qry/app.py
app.config['SQLALCHEMY_DATABASE_URI'] = "postgresql://"+ str(os.environ.get("DB_USER")) +":"+ str(os.environ.get("DB_PASSWORD")) +"@"+ str(os.environ.get("DB_HOST_READ")) +":"+ str(os.environ.get("DB_PORT")) +"/"+ str(os.environ.get("DB_NAME"))
print("prod: ", app.config['SQLALCHEMY_DATABASE_URI'], app.config['USERS'])
Nuevamente, estas variables de ambiente se configuran desde la definición de la tarea en el módulo ECS de los scripts terraform:
https://github.com/andergcp2/backend-proyecto-final/blob/main/aws-tf/modules/ecs/main.tf
...
{
name = "DB_USER"
valueFrom = "arn:aws:secretsmanager:us-east-1:101526122836:secret:rds_usr-qt5O4R"
},
{
name = "DB_PASSWORD"
valueFrom = "arn:aws:secretsmanager:us-east-1:101526122836:secret:rds_pwd-i8ebsf"
},
{
name = "DB_HOST"
valueFrom = "arn:aws:secretsmanager:us-east-1:101526122836:secret:rds_host-EkVWVQ"
},
{
name = "DB_HOST_READ"
valueFrom = "arn:aws:secretsmanager:us-east-1:101526122836:secret:aurora_host_read-5osQT0"
},
{
name = "DB_PORT"
valueFrom = "arn:aws:secretsmanager:us-east-1:101526122836:secret:rds_port-xETcnO"
},
{
name = "DB_NAME"
valueFrom = "arn:aws:secretsmanager:us-east-1:101526122836:secret:rds_name-KBdDNB"
},
...

Como estrategia para mejorar la disponibilidad y el desempeño de la base de datos se escogió Aurora. A este se le agregó una instancia de escritura mejorando las capacidades del cluster para atender las peticiones y beneficiar varios atributos que rigen nuestro proyecto. Si se incrementa la cantidad de usuarios concurrentes de la aplicación se pueden configurar más instancias que soporten la nueva carga.
Los benerficios de usar varias instancias de base de datos son:
- Ajuste de escala para réplicas de lectura bajo demanda
- Replicas dedicadas y optimizadas para operaciones de lectura
- El volumen del clúster se comparte entre todas las instancias en su clúster de base de datos Aurora PostgreSQL. Por lo tanto, no se necesita trabajo adicional para replicar una copia de los datos de cada réplica Aurora
- Aurora PostgreSQL mejora la disponibilidad de lectura en el clúster de base de datos al atender continuamente las solicitudes de lectura cuando la instancia de base de datos del escritor se reinicia o cuando la réplica de Aurora no puede seguir el ritmo del tráfico de escritura.
- Adicionalmente las instancias que se usan son Serverless v2 en la cuales la capacidad se ajusta automáticamente en función de la demanda de la aplicación.
Cognito en conjunto con el Api Gateway se usaron para para proteger y administrar el acceso de los recursos de los clientes. La configuración se hizo de la siguiente manera, Creación de Cognito User Pool con terraform: https://github.com/andergcp2/backend-proyecto-final/blob/main/aws/terraform/1-cognito.tf
resource "aws_cognito_user_pool" "mi_user_pool" {
name = "user-pool"
password_policy {
minimum_length = 6
}
verification_message_template {
default_email_option = "CONFIRM_WITH_CODE"
email_subject = "Account Confirmation"
email_message = "Your confirmation code is {####}"
}
schema {
attribute_data_type = "String"
developer_only_attribute = false
mutable = true
name = "email"
required = true
string_attribute_constraints {
min_length = 1
max_length = 256
}
}
lambda_config {
pre_sign_up = aws_lambda_function.pre_sign_up.arn
}
}
resource "aws_cognito_user_pool_client" "mi_user_pool_client" {
name = "cognito-client"
user_pool_id = aws_cognito_user_pool.mi_user_pool.id
generate_secret = false
refresh_token_validity = 90
prevent_user_existence_errors = "ENABLED"
explicit_auth_flows = [
"ALLOW_REFRESH_TOKEN_AUTH",
"ALLOW_USER_PASSWORD_AUTH",
"ALLOW_ADMIN_USER_PASSWORD_AUTH"
]
}
Consola de administración de Cognito

Para la creación de usuarios se creo un Lambda y se expuso desde el Api Gateway. Esta se comunicaba directamente con Cognito y permitía agregar nuevos usuarios y definir su Rol. https://github.com/andergcp2/backend-proyecto-final/blob/main/aws/terraform/Lambda/login/index.js
const AWS = require('aws-sdk');
const cognito = new AWS.CognitoIdentityServiceProvider();
exports.handler = async (event, context) => {
const { username, password, email, groupName, idDb } = event;
if (!event || !event.username || !event.password || !event.email || !event.groupName || !event.idDb) {
console.error('Missing required input parameters');
throw new Error('Missing required input parameters');
}
const cognitoClientId = process.env.COGNITO_CLIENT_ID; // Retrieve Cognito Client ID from environment variable
// Sign up the user in Cognito
const signUpParams = {
ClientId: cognitoClientId,
Username: username,
Password: password,
UserAttributes: [
{
Name: 'email',
Value: email,
},
{
Name: 'custom:Role',
Value: groupName,
},
{
Name: 'custom:idDb',
Value: idDb,
}
],
};
try {
await cognito.signUp(signUpParams).promise();
console.log('User successfully signed up');
} catch (error) {
if (error.code === 'UsernameExistsException') {
console.error('Username already exists:', error);
throw new Error('User with this username already exists');
} else {
console.error('Error signing up:', error);
throw new Error('Sign-up failed');
}
}
return {
statusCode: 200,
body: JSON.stringify({ message: 'User signed up successfully' }),
};
};
Para la obtención del token se generó una Lambda que obtenía de Cognito el JWT, este se provisionaba en las peticiones y eran verificadas por el Api Gateway y su integración con Cognito https://github.com/andergcp2/backend-proyecto-final/blob/main/aws/terraform/Lambda/login/index.js
const AWS = require('aws-sdk');
const cognito = new AWS.CognitoIdentityServiceProvider();
exports.handler = async (event, context) => {
const params = {
AuthFlow: 'USER_PASSWORD_AUTH',
AuthParameters: {
USERNAME: event.username,
PASSWORD: event.password
},
ClientId: process.env.COGNITO_CLIENT_ID // Reemplaza con el ID de tu cliente de Cognito
};
try {
const response = await cognito.initiateAuth(params).promise();
return {
statusCode: 200,
body: JSON.stringify(response.AuthenticationResult)
};
} catch (error) {
return {
statusCode: 500,
body: JSON.stringify(error.message)
};
}
};
### Api Gateway integración
Se crea Api Gateway con el script de terraform: https://github.com/andergcp2/backend-proyecto-final/blob/main/aws/terraform/2-apigtw.tf
resource "aws_api_gateway_rest_api" "mi_api" {
name = "Simple ABC (Terraform)"
description = "Mi API Gateway"
}
# Recurso de Rol IAM para el autorizador de Cognito
resource "aws_iam_role" "mi_iam_role" {
name = "mi-iam-role"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Action = "sts:AssumeRole",
Effect = "Allow",
Principal = {
Federated = "cognito-identity.amazonaws.com"
}
}
]
})
}
resource "aws_api_gateway_authorizer" "mi_authorizer" {
name = "mi-authorizer"
rest_api_id = aws_api_gateway_rest_api.mi_api.id
type = "COGNITO_USER_POOLS"
identity_source = "method.request.header.Authorization"
provider_arns = [aws_cognito_user_pool.mi_user_pool.arn]
authorizer_credentials = aws_iam_role.mi_iam_role.arn
}

Se agregó la instancia de Cognito para administrar la autorización

En el Api Gateway se crearon los recursos que queríamos exponer al app en Amplify, indicando cual debía ser protegido con Cognito. Estor recursos eran direccionados como proxy a los servicios expuestos por el Load Balancer.

- Avance-1 - Charts
- Avance-1 - Devops
- Avance-2 - Charts
- Avance-2 - Devops
- Cierre - Charts
- Cierre - Devops
- Cierre - Retrospectiva
- Cierre - Instructivo Usuario
- Cierre - Documentación Técnica
- Objetivos, Restricciones y Requisitos
- Escenarios de calidad
- Visión de Arquitectura
- Estilos Tácticas Patrones
- Experimento1
- Experimento2
- Resultados Experimentación