Skip to content

sprint3 documento tecnico

Marlon Mauricio edited this page Nov 26, 2023 · 26 revisions

Documentación Técnica

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.

image

Tácticas de arquitectura para favorecer la Facilidad de modificació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 implementaros 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.

Tácticas de arquitectura para favorecer la Disponibilidad

  • 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
  }
}

image

image

Tácticas de arquitectura para favorecer la Escalabilidad

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.

Manejo de recursos compartidos - múltiples copias de computación

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, dos zonas de disponibilidad dentro 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"]

además de 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
      }
    }
  }

image

Manejo de recursos compartidos - múltiples copias de datos - ElastiCache for Redis

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"
  }
}

image

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

image

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, 
        }

Manejo de recursos compartidos - múltiples copias de datos - Amazon Aurora

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"
        },  
        ...

Implementación de Aurora en el cluster

image

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.

Tomado de: https://docs.aws.amazon.com/es_es/AmazonRDS/latest/AuroraUserGuide/AuroraPostgreSQL.Replication.html#AuroraPostgreSQL.Replication.Replicas.SRO

Clone this wiki locally