Repository: oielay/GTIO_Votacion
Pull request #95: feat (Api Gateway): Implementación de api gateway en Terraform #89
Author: login: alduncin139998
Labels: name: enhancement
description: New feature or request
name: cd
Created: 2025-05-18T10:37:46Z | Merged: 2025-05-18T11:06:25Z



Diff:
diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index 678d14a..b699ea6 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -12,7 +12,7 @@ on:
 
 env:
   AWS_REGION: us-east-1
-  ECR_REGISTRY: 024742722345.dkr.ecr.us-east-1.amazonaws.com
+  ECR_REGISTRY: 515599837804.dkr.ecr.us-east-1.amazonaws.com
   IMAGE_TAG: latest
 
 jobs:
@@ -104,7 +104,7 @@ jobs:
           aws-region: ${{ env.AWS_REGION }}
 
       - name: Obtener archivo env.tfvars
-        run: aws s3 cp s3://gtio-votacion-config/env.tfvars Terraform/env.tfvars
+        run: aws s3 cp s3://gtio-votacion-config2/env.tfvars Terraform/env.tfvars
 
       - name: Configurar Terraform
         uses: hashicorp/setup-terraform@v2
@@ -156,7 +156,7 @@ jobs:
           docker-compose version
 
       - name: Crear archivo .env
-        run: aws s3 cp s3://gtio-votacion-config/.env .env
+        run: aws s3 cp s3://gtio-votacion-config2/.env .env
 
       - name: Build imágenes con docker-compose
         run: docker-compose build
@@ -199,7 +199,7 @@ jobs:
           aws-region: ${{ env.AWS_REGION }}
 
       - name: Crear archivo env.tfvars
-        run: aws s3 cp s3://gtio-votacion-config/env.tfvars Terraform/env.tfvars
+        run: aws s3 cp s3://gtio-votacion-config2/env.tfvars Terraform/env.tfvars
 
       - name: Configurar Terraform
         uses: hashicorp/setup-terraform@v2
@@ -249,7 +249,7 @@ jobs:
       - name: Ejecutar script de poblamiento cuando esté disponible
         run: |
           echo "Esperando a que la API esté disponible para poblar la base de datos..."
-          until curl --silent --fail -X POST http://balanceador-candidatos-645434306.${{ env.AWS_REGION }}.elb.amazonaws.com:8080/api/Candidates/CrearBaseDeDatos; do
+          until curl --silent --fail -H "x-api-key: ${{ secrets.ADMIN_API_KEY }}" -X GET http://balanceador-candidatos-1364946992.${{ env.AWS_REGION }}.elb.amazonaws.com:8080/api/Candidates/CrearBaseDeDatos; do
             echo "La API aún no está disponible o falló el poblamiento. Reintentando en 10 segundos..."
             sleep 10
           done
diff --git a/Backend/Servicio_1/Api.Candidatos/Api.Candidatos/04_Controllers/CandidatesController.cs b/Backend/Servicio_1/Api.Candidatos/Api.Candidatos/04_Controllers/CandidatesController.cs
index c9d691e..597360d 100644
--- a/Backend/Servicio_1/Api.Candidatos/Api.Candidatos/04_Controllers/CandidatesController.cs
+++ b/Backend/Servicio_1/Api.Candidatos/Api.Candidatos/04_Controllers/CandidatesController.cs
@@ -13,6 +13,8 @@ public class CandidatesController : ControllerBase
 {
     private readonly ICandidateService _service;
     private readonly IDatabaseScriptService _scriptService;
+    private static readonly string ExpectedApiKey = Environment.GetEnvironmentVariable("PUBLIC_API_KEY") ?? "";
+    private static readonly string AdminApiKey = Environment.GetEnvironmentVariable("ADMIN_API_KEY") ?? "";
 
     public CandidatesController(ICandidateService service, IDatabaseScriptService scriptService)
     {
@@ -24,18 +26,33 @@ public CandidatesController(ICandidateService service, IDatabaseScriptService sc
     [HttpGet("Test")]
     public ActionResult<string> GetTest()
     {
-        return "hola";
+        Console.WriteLine("== Headers recibidos ==");
+        string response = string.Empty;
+        foreach (var header in Request.Headers)
+        {
+            Console.WriteLine($"{header.Key}: {header.Value}");
+            response += $"{header.Key}: {header.Value}\n";
+        }
+        return "Test OK: " + response;
     }
 
     [HttpGet("ObtenerTodosCandidatos")]
     public async Task<ActionResult<IEnumerable<Candidate>>> GetCandidatos()
     {
+        if (!Request.Headers.TryGetValue("x-api-key", out var apiKey) || apiKey != ExpectedApiKey)
+        {
+            return Unauthorized(new { message = "API key invalid or missing" });
+        }
         return Ok(await _service.GetAllCandidatesAsync());
     }
 
     [HttpGet("ObtenerCandidatoPorId/{id}")]
     public async Task<ActionResult<Candidate>> GetCandidato(int id)
     {
+        if (!Request.Headers.TryGetValue("x-api-key", out var apiKey) || apiKey != ExpectedApiKey)
+        {
+            return Unauthorized(new { message = "API key invalid or missing" });
+        }
         var candidate = await _service.GetCandidateByIdAsync(id);
         if (candidate == null)
             return NotFound();
@@ -45,6 +62,10 @@ public async Task<ActionResult<Candidate>> GetCandidato(int id)
     [HttpPost("InsertarNuevoCandidato")]
     public async Task<ActionResult<Candidate>> CrearCandidato(CandidateRequest candidate)
     {
+        if (!Request.Headers.TryGetValue("x-api-key", out var apiKey) || apiKey != AdminApiKey)
+        {
+            return Unauthorized(new { message = "API key invalid or missing" });
+        }
         await _service.AddCandidateAsync(candidate);
         return CreatedAtAction(nameof(GetCandidato), candidate);
     }
@@ -52,6 +73,10 @@ public async Task<ActionResult<Candidate>> CrearCandidato(CandidateRequest candi
     [HttpPut("ActualizarVotosCandidato")]
     public async Task<IActionResult> ActualizarCandidato(VotosRequest votes)
     {
+        if (!Request.Headers.TryGetValue("x-api-key", out var apiKey) || apiKey != ExpectedApiKey)
+        {
+            return Unauthorized(new { message = "API key invalid or missing" });
+        }
         try
         {
             await _service.UpdateCandidateAsync(votes);
@@ -66,6 +91,10 @@ public async Task<IActionResult> ActualizarCandidato(VotosRequest votes)
     [HttpDelete("EliminarCandidato/{id}")]
     public async Task<IActionResult> EliminarCandidato(int id)
     {
+        if (!Request.Headers.TryGetValue("x-api-key", out var apiKey) || apiKey != AdminApiKey)
+        {
+            return Unauthorized(new { message = "API key invalid or missing" });
+        }
         await _service.DeleteCandidateAsync(id);
         return NoContent();
     }
@@ -73,13 +102,21 @@ public async Task<IActionResult> EliminarCandidato(int id)
     [HttpGet("ObtenerVotosCadidato/{id}")]
     public async Task<ActionResult<object>> ObtenerNumeroVotos(int id)
     {
+        if (!Request.Headers.TryGetValue("x-api-key", out var apiKey) || apiKey != ExpectedApiKey)
+        {
+            return Unauthorized(new { message = "API key invalid or missing" });
+        }
         var votes = await _service.GetCandidateVotesAsync(id);
         return Ok(new { id, votos = votes });
     }
 
-    [HttpPost("CrearBaseDeDatos")]
+    [HttpGet("CrearBaseDeDatos")]
     public async Task<IActionResult> InicializarBD()
     {
+        if (!Request.Headers.TryGetValue("x-api-key", out var apiKey) || apiKey != AdminApiKey)
+        {
+            return Unauthorized(new { message = "API key invalid or missing" });
+        }
         try
         {
             await _scriptService.EjecutarScriptAsync("init.sql");
diff --git a/Backend/Servicio_1/Api.Candidatos/Api.Candidatos/Program.cs b/Backend/Servicio_1/Api.Candidatos/Api.Candidatos/Program.cs
index 52eb766..1ada95a 100644
--- a/Backend/Servicio_1/Api.Candidatos/Api.Candidatos/Program.cs
+++ b/Backend/Servicio_1/Api.Candidatos/Api.Candidatos/Program.cs
@@ -58,6 +58,8 @@
 
 // Inyección de dependencias personalizada
 builder.Services.AddApplicationServices();
+builder.Logging.ClearProviders();
+builder.Logging.AddConsole();
 
 var app = builder.Build();
 
diff --git a/Frontend/Dockerfile b/Frontend/Dockerfile
index 5f8f461..18ce54f 100644
--- a/Frontend/Dockerfile
+++ b/Frontend/Dockerfile
@@ -18,6 +18,8 @@ COPY . .
 
 ARG PUBLIC_API_URL
 ENV PUBLIC_API_URL=${PUBLIC_API_URL}
+ARG PUBLIC_API_KEY
+ENV PUBLIC_API_KEY=${PUBLIC_API_KEY}
 
 # Ejecutar el build del proyecto
 RUN npm run build
diff --git a/Frontend/src/utils/getDataFromApi.ts b/Frontend/src/utils/getDataFromApi.ts
index 4e18b36..392f461 100644
--- a/Frontend/src/utils/getDataFromApi.ts
+++ b/Frontend/src/utils/getDataFromApi.ts
@@ -1,4 +1,5 @@
 const apiUrl = import.meta.env.PUBLIC_API_URL;
+const apiKey = "Z3Rpby12b3RhY2lvbi1hcGkta2V5IGhheSBxdWUgYWxhcmdhciBwYXJhIGxsZWdhciBhIDMwIGNhcmFjdGVyZXM"; //import.meta.env.PUBLIC_API_KEY;
 
 interface Participant {
   id: number;
@@ -17,7 +18,8 @@ export async function obtenerTodosCandidatos() {
     `${apiUrl}/api/Candidates/ObtenerTodosCandidatos`,
     {
       method: "GET",
-      headers: { "Content-Type": "application/json" },
+      headers: { "Content-Type": "application/json",
+                 "x-api-key": apiKey},
     }
   );
 
@@ -38,7 +40,8 @@ export async function actualizarVotosCandidato(
     `${apiUrl}/api/Candidates/ActualizarVotosCandidato`,
     {
       method: "PUT",
-      headers: { "Content-Type": "application/json" },
+      headers: { "Content-Type": "application/json",
+                 "x-api-key": apiKey},
       body: JSON.stringify({ votes: votos, id: participantId }),
     }
   );
@@ -56,7 +59,8 @@ export async function obtenerVotosCandidato(id: number) {
     `${apiUrl}/api/Candidates/ObtenerVotosCandidato/${id}`,
     {
       method: "GET",
-      headers: { "Content-Type": "application/json" },
+      headers: { "Content-Type": "application/json",
+                 "x-api-key": apiKey},
     }
   );
 
diff --git a/Terraform/main.tf b/Terraform/main.tf
index 0687d79..c704f29 100644
--- a/Terraform/main.tf
+++ b/Terraform/main.tf
@@ -13,7 +13,7 @@ terraform {
 
   # S3 bucket
   backend "s3" {
-    bucket       = "gtio-votacion-state"
+    bucket       = "gtio-votacion-state2"
     key          = "terraform/terraform.tfstate"
     region       = "us-east-1"
     use_lockfile = false
@@ -36,6 +36,8 @@ module "infrastructure" {
   api_image       = var.api_image
   sql_password    = var.sql_password
   frontend_image  = var.frontend_image
+  api_key         = var.api_key
+  desired_count   = var.desired_count
 }
 
 module "frontend" {
@@ -43,10 +45,12 @@ module "frontend" {
   frontend_image = var.frontend_image
 
   lb_dns_name               = module.infrastructure.lb_dns_name
-  rds_sg_id                 = module.infrastructure.rds_sg_id
   ecs_cluster_arn           = module.infrastructure.ecs_cluster_arn
   target_group_frontend_arn = module.infrastructure.target_group_frontend_arn
   listener_frontend_arn     = module.infrastructure.listener_frontend_arn
+  frontend_sg_id            = module.infrastructure.frontend_sg_id
+  desired_count             = var.desired_count
+  public_api_key            = var.api_key
 }
 
 module "api" {
@@ -54,12 +58,14 @@ module "api" {
   task_api_secret        = var.task_api_secret
   task_api_secret_master = var.task_api_secret_master
   api_image              = var.api_image
-
-  lb_dns_name          = module.infrastructure.lb_dns_name
-  rds_sg_id            = module.infrastructure.rds_sg_id
-  ecs_cluster_arn      = module.infrastructure.ecs_cluster_arn
-  target_group_api_arn = module.infrastructure.target_group_api_arn
-  listener_api_arn     = module.infrastructure.listener_api_arn
+  lb_dns_name            = module.infrastructure.lb_dns_name
+  ecs_cluster_arn        = module.infrastructure.ecs_cluster_arn
+  target_group_api_arn   = module.infrastructure.target_group_api_arn
+  listener_api_arn       = module.infrastructure.listener_api_arn
+  api_sg_id              = module.infrastructure.api_sg_id
+  desired_count          = var.desired_count
+  public_api_key         = var.api_key
+  admin_api_key          = var.admin_api_key
 }
 
 variable "region" {
@@ -101,6 +107,22 @@ variable "frontend_image" { # igual se puede dinamizar
   type        = string
 }
 
+variable "api_key" {
+  description = "API key"
+  type        = string
+}
+
+variable "admin_api_key" {
+  description = "API key"
+  type        = string
+}
+
+variable "desired_count" {
+  description = "Número deseado de instancias"
+  type        = number
+  default     = 2
+}
+
 ################################
 # Outputs
 ################################
@@ -115,4 +137,4 @@ output "ecs_service_name_frontend" {
 
 output "ecs_service_name_api" {
   value = module.api.ecs_service_name_api
-}
+}
\ No newline at end of file
diff --git a/Terraform/modules/api/main.tf b/Terraform/modules/api/main.tf
index a46d1cc..37d2032 100644
--- a/Terraform/modules/api/main.tf
+++ b/Terraform/modules/api/main.tf
@@ -27,6 +27,14 @@ resource "aws_ecs_task_definition" "task_api_candidatos" {
         {
           name  = "CORS_ORIGIN"
           value = "http://${var.lb_dns_name}:3000"
+        },
+        {
+          name  = "PUBLIC_API_KEY"
+          value = "${var.public_api_key}"
+        },
+        {
+          name  = "ADMIN_API_KEY"
+          value = "${var.admin_api_key}"
         }
       ]
       secrets = [
@@ -39,6 +47,15 @@ resource "aws_ecs_task_definition" "task_api_candidatos" {
           valueFrom = var.task_api_secret_master
         }
       ]
+      log_configuration = {
+        log_driver = "awslogs"
+
+        options = {
+            awslogs-group         = "/ecs/api"
+            awslogs-region        = "eu-west-1"
+            awslogs-stream-prefix = "ecs"
+        }
+      }
     }
   ])
 
@@ -53,7 +70,7 @@ resource "aws_ecs_service" "service_api_candidatos" {
   name            = "api-candidatos-service"
   cluster         = var.ecs_cluster_arn
   task_definition = aws_ecs_task_definition.task_api_candidatos.arn
-  desired_count   = 1
+  desired_count   = var.desired_count
 
   launch_type = "EC2"
 
@@ -65,7 +82,7 @@ resource "aws_ecs_service" "service_api_candidatos" {
 
   network_configuration {
     subnets         = data.aws_subnets.default.ids
-    security_groups = [data.aws_security_group.default.id, var.rds_sg_id]
+    security_groups = [data.aws_security_group.default.id, var.api_sg_id]
   }
 
   deployment_controller {
@@ -77,42 +94,16 @@ resource "aws_ecs_service" "service_api_candidatos" {
   health_check_grace_period_seconds = 30
   force_new_deployment              = true
 
-  lifecycle {
-    replace_triggered_by = [
-      aws_ecs_task_definition.task_api_candidatos,
-    ]
-  }
-
   depends_on = [
     aws_ecs_task_definition.task_api_candidatos,
     var.listener_api_arn
   ]
 }
 
-# Instance
-resource "aws_instance" "ecs_instance" {
-  ami                    = var.ami_id
-  instance_type          = var.instance_type
-  key_name               = var.key_name
-  availability_zone      = var.availability_zones[0]
-  subnet_id              = data.aws_subnets.default.ids[0]
-  vpc_security_group_ids = [data.aws_security_group.default.id, var.rds_sg_id]
-  user_data              = var.user_data
-  iam_instance_profile   = var.iam_instance_profile
-
-  tags = {
-    Name = "instancia-api-candidatos"
-  }
-}
-
 #################################
 # Local variables
 #################################
 
-variable "availability_zones" {
-  default = ["us-east-1b", "us-east-1b", "us-east-1c", "us-east-1a", "us-east-1e", "us-east-1f"]
-}
-
 variable "ami_id" {
   default = "ami-03b4de1e633ccdc0f"
 }
@@ -161,6 +152,10 @@ data "aws_subnets" "default" {
   }
 }
 
+data "aws_subnet" "zone" {
+  id = data.aws_subnets.default.ids[0]
+}
+
 #################################
 # Tfvars variables
 #################################
@@ -185,8 +180,8 @@ variable "lb_dns_name" {
   type        = string
 }
 
-variable "rds_sg_id" {
-  description = "ID del grupo de seguridad de RDS"
+variable "api_sg_id" {
+  description = "ID del grupo de seguridad de la API"
   type        = string
 }
 
@@ -205,6 +200,22 @@ variable "listener_api_arn" {
   type        = string
 }
 
+variable "desired_count" {
+  description = "Número deseado de instancias"
+  type        = number
+  default     = 1
+}
+
+variable "public_api_key" {
+  description = "API key pública"
+  type        = string
+}
+
+variable "admin_api_key" {
+  description = "API key de administrador"
+  type        = string
+}
+
 ###################################
 # Outputs
 ###################################
diff --git a/Terraform/modules/frontend/main.tf b/Terraform/modules/frontend/main.tf
index b16868c..52087a5 100644
--- a/Terraform/modules/frontend/main.tf
+++ b/Terraform/modules/frontend/main.tf
@@ -27,6 +27,10 @@ resource "aws_ecs_task_definition" "task_frontend" {
         {
           name  = "PUBLIC_API_URL"
           value = "http://${var.lb_dns_name}:8080"
+        },
+        {
+          name  = "PUBLIC_API_KEY"
+          value = "${var.public_api_key}"
         }
       ]
       logConfiguration = {
@@ -51,7 +55,7 @@ resource "aws_ecs_service" "service_frontend" {
   name            = "frontend-service"
   cluster         = var.ecs_cluster_arn
   task_definition = aws_ecs_task_definition.task_frontend.arn
-  desired_count   = 1
+  desired_count   = var.desired_count
 
   launch_type = "EC2"
 
@@ -63,7 +67,7 @@ resource "aws_ecs_service" "service_frontend" {
 
   network_configuration {
     subnets         = data.aws_subnets.default.ids
-    security_groups = [data.aws_security_group.default.id, var.rds_sg_id]
+    security_groups = [data.aws_security_group.default.id, var.frontend_sg_id]
   }
 
   deployment_controller {
@@ -75,42 +79,16 @@ resource "aws_ecs_service" "service_frontend" {
   health_check_grace_period_seconds = 30
   force_new_deployment              = true
 
-  lifecycle {
-    replace_triggered_by = [
-      aws_ecs_task_definition.task_frontend,
-    ]
-  }
-
   depends_on = [
     aws_ecs_task_definition.task_frontend,
     var.listener_frontend_arn
   ]
 }
 
-# Instance
-resource "aws_instance" "ecs_instance_frontend" {
-  ami                    = var.ami_id
-  instance_type          = var.instance_type
-  key_name               = var.key_name
-  availability_zone      = var.availability_zones[0]
-  subnet_id              = data.aws_subnets.default.ids[0]
-  vpc_security_group_ids = [data.aws_security_group.default.id, var.rds_sg_id]
-  user_data              = var.user_data
-  iam_instance_profile   = var.iam_instance_profile
-
-  tags = {
-    Name = "instancia-frontend"
-  }
-}
-
 #################################
 # Local variables
 #################################
 
-variable "availability_zones" {
-  default = ["us-east-1b", "us-east-1b", "us-east-1c", "us-east-1a", "us-east-1e", "us-east-1f"]
-}
-
 variable "ami_id" {
   default = "ami-03b4de1e633ccdc0f"
 }
@@ -159,6 +137,10 @@ data "aws_subnets" "default" {
   }
 }
 
+data "aws_subnet" "zone" {
+  id = data.aws_subnets.default.ids[0]
+}
+
 #################################
 # Tfvars variables
 #################################
@@ -173,8 +155,8 @@ variable "lb_dns_name" {
   type        = string
 }
 
-variable "rds_sg_id" {
-  description = "ID del grupo de seguridad de RDS"
+variable "frontend_sg_id" {
+  description = "ID del grupo de seguridad de frontend"
   type        = string
 }
 
@@ -193,6 +175,17 @@ variable "listener_frontend_arn" {
   type        = string
 }
 
+variable "desired_count" {
+  description = "Número deseado de instancias"
+  type        = number
+  default     = 1
+}
+
+variable "public_api_key" {
+  description = "API key pública"
+  type        = string  
+}
+
 ###################################
 # Outputs
 ###################################
diff --git a/Terraform/modules/infrastructure/main.tf b/Terraform/modules/infrastructure/main.tf
index 89273ce..29ab5c7 100644
--- a/Terraform/modules/infrastructure/main.tf
+++ b/Terraform/modules/infrastructure/main.tf
@@ -16,93 +16,10 @@ resource "aws_ecs_cluster" "ecs_cluster" {
 # Servicio: Api Candidatos
 #################################
 
-# Task Definition
-# resource "aws_ecs_task_definition" "task_api_candidatos" {
-#   family                   = "api"
-#   requires_compatibilities = ["EC2"]
-#   network_mode             = "awsvpc"
-#   cpu                      = "850"
-#   memory                   = "850"
-#   execution_role_arn       = data.aws_iam_role.lab_role.arn
-#   task_role_arn            = data.aws_iam_role.lab_role.arn
-
-#   container_definitions = jsonencode([
-#     {
-#       name  = var.api_container_name
-#       image = var.api_image
-#       portMappings = [
-#         {
-#           containerPort = 8080
-#           hostPort      = 8080
-#           protocol      = "tcp"
-#         }
-#       ]
-#       environment = [
-#         {
-#           name  = "CORS_ORIGIN"
-#           value = "http://${aws_lb.ElasticLoadBalancingV2LoadBalancer.dns_name}:3000"
-#         }
-#       ]
-#       secrets = [
-#         {
-#           name      = "ConnectionStrings__DefaultConnection"
-#           valueFrom = var.task_api_secret
-#         }
-#       ]
-#     }
-#   ])
-# }
-
-# Service
-# resource "aws_ecs_service" "service_api_candidatos" {
-#   name            = "api-candidatos-service"
-#   cluster         = aws_ecs_cluster.ecs_cluster.arn
-#   task_definition = aws_ecs_task_definition.task_api_candidatos.arn
-#   desired_count   = 1
-
-#   launch_type = "EC2"
-
-#   load_balancer {
-#     target_group_arn = aws_lb_target_group.ElasticLoadBalancingV2TargetGroup.arn
-#     container_name   = var.api_container_name
-#     container_port   = 8080
-#   }
-
-#   network_configuration {
-#     subnets         = data.aws_subnets.default.ids
-#     security_groups = [data.aws_security_group.default.id, aws_security_group.rds_sg.id]
-#   }
-
-#   deployment_controller {
-#     type = "ECS"
-#   }
-
-#   scheduling_strategy = "REPLICA"
-
-#   health_check_grace_period_seconds = 30
-#   force_new_deployment              = true
-
-#   depends_on = [
-#     aws_ecs_task_definition.task_api_candidatos,
-#     aws_lb_listener.ElasticLoadBalancingV2Listener
-#   ]
-# }
-
-# Instance
-# resource "aws_instance" "ecs_instance" {
-#   ami                    = var.ami_id
-#   instance_type          = var.instance_type
-#   key_name               = var.key_name
-#   availability_zone      = var.availability_zones[0]
-#   subnet_id              = data.aws_subnets.default.ids[0]
-#   vpc_security_group_ids = [data.aws_security_group.default.id, aws_security_group.rds_sg.id]
-#   user_data              = var.user_data
-#   iam_instance_profile   = var.iam_instance_profile
-
-#   tags = {
-#     Name = "instancia-api-candidatos"
-#   }
-# }
+resource "aws_cloudwatch_log_group" "api_logs" {
+  name              = "/ecs/api"
+  retention_in_days = 30
+}
 
 # Load Balancer
 resource "aws_lb" "ElasticLoadBalancingV2LoadBalancer" {
@@ -111,8 +28,8 @@ resource "aws_lb" "ElasticLoadBalancingV2LoadBalancer" {
   load_balancer_type = "application"
   subnets            = data.aws_subnets.default.ids
   security_groups = [
-    data.aws_security_group.default.id,
-    aws_security_group.rds_sg.id
+    aws_security_group.sg_frontend.id,
+    aws_security_group.sg_backend.id
   ]
   ip_address_type = "ipv4"
   access_logs {
@@ -144,9 +61,9 @@ resource "aws_autoscaling_group" "AutoScalingAutoScalingGroup" {
     id      = aws_launch_template.EC2LaunchTemplate.id
     version = "1"
   }
-  min_size                  = var.autoscaling_min_size
-  max_size                  = var.autoscaling_max_size
-  desired_capacity          = var.autoscaling_desired_capacity
+  min_size                  = var.desired_count
+  max_size                  = var.desired_count * 4
+  desired_capacity          = var.desired_count * 2
   default_cooldown          = 300
   health_check_type         = "EC2"
   health_check_grace_period = 0
@@ -180,8 +97,7 @@ resource "aws_launch_template" "EC2LaunchTemplate" {
     delete_on_termination       = true
     device_index                = 0
     security_groups = [
-      data.aws_security_group.default.id,
-      aws_security_group.rds_sg.id
+      data.aws_security_group.default.id
     ]
   }
   image_id      = var.ami_id
@@ -211,102 +127,12 @@ resource "aws_lb_target_group" "ElasticLoadBalancingV2TargetGroup" {
 # Servicio: Frontend
 #################################
 
-# Task Definition
-# resource "aws_ecs_task_definition" "task_frontend" {
-#   family                   = "frontend"
-#   requires_compatibilities = ["EC2"]
-#   network_mode             = "awsvpc"
-#   cpu                      = "850"
-#   memory                   = "850"
-#   execution_role_arn       = data.aws_iam_role.lab_role.arn
-#   task_role_arn            = data.aws_iam_role.lab_role.arn
-
-#   container_definitions = jsonencode([
-#     {
-#       name  = var.frontend_container_name
-#       image = var.frontend_image
-#       portMappings = [
-#         {
-#           containerPort = 3000
-#           hostPort      = 3000
-#           protocol      = "tcp"
-#         }
-#       ]
-#       environment = [
-#         {
-#           name  = "PUBLIC_API_URL"
-#           value = "http://${aws_lb.ElasticLoadBalancingV2LoadBalancer.dns_name}:8080"
-#         }
-#       ]
-#       logConfiguration = {
-#         logDriver = "awslogs",
-#         options = {
-#           awslogs-group         = "/ecs/frontend"
-#           awslogs-region        = "us-east-1"
-#           awslogs-stream-prefix = "ecs"
-#         }
-#       }
-#     }
-#   ])
-# }
-
 # Log group
 resource "aws_cloudwatch_log_group" "frontend_logs" {
   name              = "/ecs/frontend"
   retention_in_days = 7
 }
 
-# Service
-# resource "aws_ecs_service" "service_frontend" {
-#   name            = "frontend-service2"
-#   cluster         = aws_ecs_cluster.ecs_cluster.arn
-#   task_definition = aws_ecs_task_definition.task_frontend.arn
-#   desired_count   = 1
-
-#   launch_type = "EC2"
-
-#   load_balancer {
-#     target_group_arn = aws_lb_target_group.load_balancer_target_group_frontend.arn
-#     container_name   = var.frontend_container_name
-#     container_port   = 3000
-#   }
-
-#   network_configuration {
-#     subnets         = data.aws_subnets.default.ids
-#     security_groups = [data.aws_security_group.default.id, aws_security_group.rds_sg.id]
-#   }
-
-#   deployment_controller {
-#     type = "ECS"
-#   }
-
-#   scheduling_strategy = "REPLICA"
-
-#   health_check_grace_period_seconds = 30
-#   force_new_deployment              = true
-
-#   depends_on = [
-#     aws_ecs_task_definition.task_frontend,
-#     aws_lb_listener.load_balancer_listener_frontend
-#   ]
-# }
-
-# Instance
-# resource "aws_instance" "ecs_instance_frontend" {
-#   ami                    = var.ami_id
-#   instance_type          = var.instance_type
-#   key_name               = var.key_name
-#   availability_zone      = var.availability_zones[0]
-#   subnet_id              = data.aws_subnets.default.ids[0]
-#   vpc_security_group_ids = [data.aws_security_group.default.id, aws_security_group.rds_sg.id]
-#   user_data              = var.user_data
-#   iam_instance_profile   = var.iam_instance_profile
-
-#   tags = {
-#     Name = "instancia-frontend"
-#   }
-# }
-
 # Listener Frontend
 resource "aws_lb_listener" "load_balancer_listener_frontend" {
   load_balancer_arn = aws_lb.ElasticLoadBalancingV2LoadBalancer.arn
@@ -352,7 +178,7 @@ resource "aws_db_instance" "RDSDBInstance" {
   password                            = var.sql_password
   backup_window                       = "07:41-08:11"
   backup_retention_period             = 1
-  availability_zone                   = var.availability_zones[1]
+  availability_zone                   = data.aws_availability_zones.available.names[0]
   maintenance_window                  = "sat:04:41-sat:05:11"
   multi_az                            = false
   engine_version                      = "15.00.4430.1.v1"
@@ -371,7 +197,7 @@ resource "aws_db_instance" "RDSDBInstance" {
   db_subnet_group_name                = aws_db_subnet_group.db_subnet_gtio_votacion.name
   vpc_security_group_ids = [
     data.aws_security_group.default.id,
-    aws_security_group.rds_sg.id
+    aws_security_group.sg_db.id
   ]
   skip_final_snapshot = true
 }
@@ -386,83 +212,6 @@ resource "aws_db_subnet_group" "db_subnet_gtio_votacion" {
   }
 }
 
-# resource "aws_ecs_task_definition" "db_init_task" {
-#     family                   = "db-init-task"
-#     requires_compatibilities = ["FARGATE"]
-#     cpu                      = "256"
-#     memory                   = "512"
-#     network_mode             = "awsvpc"
-#     execution_role_arn       = data.aws_iam_role.lab_role.arn
-#     task_role_arn            = data.aws_iam_role.lab_role.arn
-
-#     container_definitions = jsonencode([
-#         {
-#             name      = "db-init"
-#             image     = var.db_init_image
-#             essential = true
-#             environment = [
-#                 {
-#                     name  = "DB_SERVER"
-#                     value = var.db_server
-#                 },
-#                 {
-#                     name  = "DB_USER"
-#                     value = "sa"
-#                 },
-#                 {
-#                     name  = "DB_PASSWORD"
-#                     value = var.db_password
-#                 }
-#             ]
-#             logConfiguration = {
-#                 logDriver = "awslogs",
-#                 options = {
-#                     awslogs-group         = "/ecs/db-init"
-#                     awslogs-region        = "us-east-1"
-#                     awslogs-stream-prefix = "ecs"
-#                 }
-#             }
-#         }
-#     ])
-
-#     depends_on = [
-#         aws_db_instance.RDSDBInstance
-#     ]
-# }
-
-# resource "aws_cloudwatch_log_group" "db_init_logs" {
-#     name              = "/ecs/db-init"
-#     retention_in_days = 7
-# }
-
-# resource "null_resource" "run_db_init_task" {
-#     depends_on = [aws_db_instance.RDSDBInstance]
-
-#     provisioner "local-exec" {
-#         command = "aws ecs run-task --cluster ${aws_ecs_cluster.ecs_cluster.name} --launch-type FARGATE --network-configuration awsvpcConfiguration={subnets=[${join(",", formatlist("\"%s\"", data.aws_subnets.default.ids))}],securityGroups=[\"${data.aws_security_group.default.id}\"],assignPublicIp=\"ENABLED\"} --task-definition ${aws_ecs_task_definition.db_init_task.arn}"
-#     }
-
-#     triggers = {
-#         always_run = timestamp()
-#     }
-# }
-
-# resource "aws_ecs_service" "run_db_init" {
-#     name = "db-init-service"
-#     task_definition = aws_ecs_task_definition.db_init_task.arn
-#     cluster         = aws_ecs_cluster.ecs_cluster.arn
-#     launch_type     = "FARGATE"
-
-#     network_configuration {
-#         subnets          = data.aws_subnets.default.ids
-#         security_groups  = [data.aws_security_group.default.id, aws_security_group.rds_sg.id]
-#         assign_public_ip = true
-#     }
-
-#     depends_on = [aws_db_instance.RDSDBInstance, aws_ecs_task_definition.db_init_task]
-#     desired_count   = 1
-# }
-
 resource "aws_security_group" "rds_sg" {
   name        = "rds-sql-access"
   description = "Allow SQL Server access"
@@ -504,6 +253,99 @@ resource "aws_security_group" "rds_sg" {
   }
 }
 
+resource "aws_security_group" "sg_frontend" {
+  name        = "gtio-sg-frontend"
+  description = "Allow HTTP/HTTPS traffic from the internet"
+  vpc_id      = data.aws_vpc.default.id
+
+  ingress {
+    description = "HTTP"
+    from_port   = 80
+    to_port     = 80
+    protocol    = "tcp"
+    cidr_blocks = ["0.0.0.0/0"]
+  }
+
+  ingress {
+    description = "HTTPS"
+    from_port   = 443
+    to_port     = 443
+    protocol    = "tcp"
+    cidr_blocks = ["0.0.0.0/0"]
+  }
+
+  ingress {
+    description = "Frontend (3000)"
+    from_port   = 3000
+    to_port     = 3000
+    protocol    = "tcp"
+    cidr_blocks = ["0.0.0.0/0"]
+  }
+
+  egress {
+    from_port   = 0
+    to_port     = 0
+    protocol    = "-1"
+    cidr_blocks = ["0.0.0.0/0"]
+  }
+
+  tags = {
+    Name = "Frontend Security Group"
+  }
+}
+
+resource "aws_security_group" "sg_backend" {
+  name        = "gtio-sg-backend"
+  description = "Allow traffic from frontend to API"
+  vpc_id      = data.aws_vpc.default.id
+
+  ingress {
+    description      = "API candidatos (8080)"
+    from_port        = 8080
+    to_port          = 8080
+    protocol         = "tcp"
+    # security_groups  = [data.aws_security_group.default.id, aws_security_group.sg_frontend.id]
+    cidr_blocks = ["0.0.0.0/0"]
+  }
+
+  egress {
+    from_port   = 0
+    to_port     = 0
+    protocol    = "-1"
+    cidr_blocks = ["0.0.0.0/0"]
+  }
+
+  tags = {
+    Name = "Backend Security Group"
+  }
+}
+
+resource "aws_security_group" "sg_db" {
+  name        = "gtio-sg-db"
+  description = "Allow SQL Server access from backend only"
+  vpc_id      = data.aws_vpc.default.id
+
+  ingress {
+    description      = "SQL Server"
+    from_port        = 1433
+    to_port          = 1433
+    protocol         = "tcp"
+    # security_groups  = [data.aws_security_group.default.id, aws_security_group.sg_backend.id]
+    cidr_blocks = ["0.0.0.0/0"]
+  }
+
+  egress {
+    from_port   = 0
+    to_port     = 0
+    protocol    = "-1"
+    cidr_blocks = ["0.0.0.0/0"]
+  }
+
+  tags = {
+    Name = "Database Security Group"
+  }
+}
+
 #################################
 # Api Gateway AWS
 #################################
@@ -526,14 +368,21 @@ resource "aws_api_gateway_rest_api" "rest_api" {
   })
 }
 
-# Resource: /test
-resource "aws_api_gateway_resource" "test_resource" {
+# Resource: /api
+resource "aws_api_gateway_resource" "api_base" {
   rest_api_id = aws_api_gateway_rest_api.rest_api.id
   parent_id   = aws_api_gateway_rest_api.rest_api.root_resource_id
+  path_part   = "api"
+}
+
+# Resource: /api/test
+resource "aws_api_gateway_resource" "test_resource" {
+  rest_api_id = aws_api_gateway_rest_api.rest_api.id
+  parent_id   = aws_api_gateway_resource.api_base.id
   path_part   = "test"
 }
 
-# Method: ANY on /test
+# Method: ANY on /api/test
 resource "aws_api_gateway_method" "test_method" {
   rest_api_id   = aws_api_gateway_rest_api.rest_api.id
   resource_id   = aws_api_gateway_resource.test_resource.id
@@ -551,17 +400,79 @@ resource "aws_api_gateway_integration" "test_integration" {
   uri                     = "http://${aws_lb.ElasticLoadBalancingV2LoadBalancer.dns_name}:8080/api/Candidates/Test"
 }
 
-# Resource: /obtenerTodosCandidatos
+# Method: ANY on /frontend
+resource "aws_api_gateway_method" "frontend_method" {
+  rest_api_id   = aws_api_gateway_rest_api.rest_api.id
+  resource_id   = aws_api_gateway_rest_api.rest_api.root_resource_id
+  http_method   = "ANY"
+  authorization = "NONE"
+  api_key_required = false
+}
+
+# Integration: Forward to ALB
+resource "aws_api_gateway_integration" "frontend_integration" {
+  rest_api_id             = aws_api_gateway_rest_api.rest_api.id
+  resource_id             = aws_api_gateway_rest_api.rest_api.root_resource_id
+  http_method             = aws_api_gateway_method.frontend_method.http_method
+  integration_http_method = "ANY"
+  type                    = "HTTP_PROXY"
+  uri                     = "http://${aws_lb.ElasticLoadBalancingV2LoadBalancer.dns_name}:3000"
+}
+
+# resource "aws_lb_listener_rule" "require_header" {
+#   listener_arn = aws_lb_listener.ElasticLoadBalancingV2Listener.arn
+#   priority     = 10
+
+#   condition {
+#     http_header {
+#       http_header_name = "x-amzn-apigateway-api-id"
+#       values           = ["*"] # acepta cualquier valor siempre que el header esté presente
+#     }
+#   }
+
+#   action {
+#     type             = "forward"
+#     target_group_arn = aws_lb_target_group.ElasticLoadBalancingV2TargetGroup.arn
+#   }
+# }
+
+# # Regla para bloquear si NO tienen el header 'prueba'
+# resource "aws_lb_listener_rule" "block_if_missing_header" {
+#   listener_arn = aws_lb_listener.ElasticLoadBalancingV2Listener.arn
+#   priority     = 20
+
+#   condition {
+#     # Ningún valor significa que no cumple con la anterior
+#     # y cae en esta si no tenía el header
+#     # (esto funciona como fallback en ausencia del anterior)
+#     # No conditions = catch-all
+#     # But to simulate missing header:
+#     # use not having the above condition
+#   }
+
+#   action {
+#     type = "fixed-response"
+
+#     fixed_response {
+#       content_type = "text/plain"
+#       message_body = "Request blocked"
+#       status_code  = "403"
+#     }
+#   }
+# }
+
+# Resource: /api/obtenerTodosCandidatos
 resource "aws_api_gateway_resource" "obtener_todos_resource" {
   rest_api_id = aws_api_gateway_rest_api.rest_api.id
-  parent_id   = aws_api_gateway_rest_api.rest_api.root_resource_id
+  parent_id   = aws_api_gateway_resource.api_base.id
   path_part   = "obtenerTodosCandidatos"
 }
 
-# Method: ANY on /obtenerTodosCandidatos
+# Method: ANY on /api/obtenerTodosCandidatos
 resource "aws_api_gateway_method" "obtener_todos_method" {
   rest_api_id   = aws_api_gateway_rest_api.rest_api.id
   resource_id   = aws_api_gateway_resource.obtener_todos_resource.id
+  api_key_required = true
   http_method   = "ANY"
   authorization = "NONE"
 }
@@ -579,12 +490,14 @@ resource "aws_api_gateway_integration" "obtener_todos_integration" {
 # Configuración del stage "prod" con CloudWatch Logs
 resource "aws_api_gateway_deployment" "prod_deployment" {
   rest_api_id = aws_api_gateway_rest_api.rest_api.id
-  stage_name  = "prod"
-  depends_on = [
+  depends_on  = [
     aws_api_gateway_method.test_method,
-    aws_api_gateway_method.obtener_todos_method, # Asegurando que el método se haya creado antes del deployment
+    aws_api_gateway_method.obtener_todos_method,
+    aws_api_gateway_resource.obtener_por_id_base,
+    aws_api_gateway_resource.obtener_por_id_param,
     aws_api_gateway_integration.test_integration,
-    aws_api_gateway_integration.obtener_todos_integration
+    aws_api_gateway_integration.obtener_todos_integration,
+    aws_api_gateway_integration.obtener_por_id_integration
   ]
 }
 
@@ -597,6 +510,30 @@ resource "aws_api_gateway_stage" "prod_stage" {
   rest_api_id   = aws_api_gateway_rest_api.rest_api.id
   stage_name    = "prod"
   deployment_id = aws_api_gateway_deployment.prod_deployment.id
+
+  access_log_settings {
+    destination_arn = aws_cloudwatch_log_group.api_gw_logs.arn
+    format = jsonencode({
+      requestId       = "$context.requestId"
+      ip              = "$context.identity.sourceIp"
+      caller          = "$context.identity.caller"
+      user            = "$context.identity.user"
+      requestTime     = "$context.requestTime"
+      httpMethod      = "$context.httpMethod"
+      resourcePath    = "$context.resourcePath"
+      status          = "$context.status"
+      protocol        = "$context.protocol"
+      responseLength  = "$context.responseLength"
+    })
+  }
+  
+  tags = {
+    Name = "gtio-votacion-prod-stage"
+  }
+
+  depends_on = [
+    aws_api_gateway_account.account_settings
+  ]
 }
 
 # Listener en ALB que redirige tráfico a la API Gateway
@@ -611,209 +548,90 @@ resource "aws_lb_listener" "http_listener_for_gateway" {
   }
 }
 
-# DynamoDB
-# resource "aws_dynamodb_table" "api_keys" {
-#   name         = "api-keys"
-#   billing_mode = "PAY_PER_REQUEST"
-#   hash_key     = "api_key"
-
-#   attribute {
-#     name = "api_key"
-#     type = "S"
-#   }
-# }
-
-# # Lambda Authorizer
-# resource "aws_iam_role" "lambda_auth_role" {
-#   name = "lambda-authorizer-role"
-#   assume_role_policy = jsonencode({
-#     Version = "2012-10-17"
-#     Statement = [{
-#       Action = "sts:AssumeRole"
-#       Principal = {
-#         Service = "lambda.amazonaws.com"
-#       }
-#       Effect = "Allow"
-#     }]
-#   })
-# }
-
-# resource "aws_iam_policy_attachment" "lambda_logs" {
-#   name       = "lambda-logs"
-#   roles      = [aws_iam_role.lambda_auth_role.name]
-#   policy_arn = data.aws_iam_role.lab_role.arn
-# }
-
-# resource "aws_lambda_function" "auth_lambda" {
-#   function_name = "api-key-authorizer"
-#   role          = data.aws_iam_role.lab_role.arn
-#   handler       = "index.handler"
-#   runtime       = "nodejs18.x"
-#   timeout       = 5
-#   filename      = "lambda.zip"
-
-#   environment {
-#     variables = {
-#       TABLE_NAME = aws_dynamodb_table.api_keys.name
-#     }
-#   }
-# }
-
-#################################
-# Cloudwatch: eventos para despliegue automatico
-#################################
-
-# resource "aws_cloudwatch_event_rule" "api_image_push_rule" {
-#     name        = "api-image-push-rule"
-#     description = "Regla para activar eventos solo cuando se suba una nueva imagen al repositorio de api"
-#     event_pattern = jsonencode({
-#         "source" = [
-#             "aws.ecr"
-#         ],
-#         "detail-type" = [
-#             "ECR Image Action"
-#         ],
-#         "detail" = {
-#             "action-type" = ["PUSH"]
-#             "repository-name" = [aws_ecr_repository.api_repo.name]
-#         }
-#     })
-# }
-
-# resource "aws_cloudwatch_event_rule" "frontend_image_push_rule" {
-#     name        = "frontend-image-push-rule"
-#     description = "Regla para activar eventos solo cuando se suba una nueva imagen al repositorio de frontend"
-#     event_pattern = jsonencode({
-#         "source" = [
-#             "aws.ecr"
-#         ],
-#         "detail-type" = [
-#             "ECR Image Action"
-#         ],
-#         "detail" = {
-#             "action-type" = ["PUSH"]
-#             "repository-name" = [aws_ecr_repository.frontend_repo.name]
-#         }
-#     })
-# }
+# Resource: /api/obtenerCandidatoPorId/{id}
+resource "aws_api_gateway_resource" "obtener_por_id_base" {
+  rest_api_id = aws_api_gateway_rest_api.rest_api.id
+  parent_id   = aws_api_gateway_resource.api_base.id
+  path_part   = "obtenerCandidatoPorId"
+}
 
-# resource "aws_cloudwatch_event_target" "api_ecs_service_target" {
-#     rule = aws_cloudwatch_event_rule.api_image_push_rule.name
-#     arn  = aws_ecs_service.service_api_candidatos.id
-#     role_arn = data.aws_iam_role.lab_role.arn
-
-#     input = jsonencode({
-#         "cluster"         = aws_ecs_cluster.ecs_cluster.name,
-#         "service"         = aws_ecs_service.service_api_candidatos.name,
-#         "desiredCount"    = 1,
-#         "forceNewDeployment" = true
-#     })
-# }
+resource "aws_api_gateway_resource" "obtener_por_id_param" {
+  rest_api_id = aws_api_gateway_rest_api.rest_api.id
+  parent_id   = aws_api_gateway_resource.obtener_por_id_base.id
+  path_part   = "{id}"
+}
 
-# resource "aws_cloudwatch_event_target" "frontend_ecs_service_target" {
-#     rule = aws_cloudwatch_event_rule.frontend_image_push_rule.name
-#     arn  = aws_ecs_service.service_frontend.id
-#     role_arn = data.aws_iam_role.lab_role.arn
-
-#     input = jsonencode({
-#         "cluster"         = aws_ecs_cluster.ecs_cluster.name,
-#         "service"         = aws_ecs_service.service_frontend.name,
-#         "desiredCount"    = 1,
-#         "forceNewDeployment" = true
-#     })
-# }
+# Method: ANY on /api/obtenerCandidatoPorId
+resource "aws_api_gateway_method" "obtener_por_id_method" {
+  rest_api_id   = aws_api_gateway_rest_api.rest_api.id
+  resource_id   = aws_api_gateway_resource.obtener_por_id_param.id
+  http_method   = "GET"
+  authorization = "NONE"
+  api_key_required = true
 
-# Ejecutar en local
-# resource "null_resource" "run_sql_script" {
-#     provisioner "local-exec" {
-#         command = <<-EOF
-#         sqlcmd -S ${aws_db_instance.RDSDBInstance.endpoint} -U sa -P ${var.sql_password} -d master -i "./scripts/init1.sql"
-#         EOF
-#     }
+  request_parameters = {
+    "method.request.path.id" = true
+  }
+}
 
-#     depends_on = [aws_db_instance.RDSDBInstance]
-# }
+# Integration: Forward to ALB
+resource "aws_api_gateway_integration" "obtener_por_id_integration" {
+  rest_api_id             = aws_api_gateway_rest_api.rest_api.id
+  resource_id             = aws_api_gateway_resource.obtener_por_id_param.id
+  http_method             = aws_api_gateway_method.obtener_por_id_method.http_method
+  integration_http_method = "GET"
+  type                    = "HTTP_PROXY"
+  uri                     = "http://${aws_lb.ElasticLoadBalancingV2LoadBalancer.dns_name}:8080/api/Candidates/ObtenerCandidatoPorId/{id}"
 
-# Lambda Function ---- NO FUNCIONA ADECUADAMENTE ----
-# resource "aws_lambda_function" "db_creation_lambda" {
-#     function_name = "db_creation_lambda"
-#     role          = data.aws_iam_role.lab_role.arn
-#     handler       = "lambda_function.lambda_handler"
-#     runtime       = "python3.8"
-
-#     filename      = "lambda_function.zip"
-
-#     environment {
-#         variables = {
-#             DB_SERVER = aws_db_instance.RDSDBInstance.address
-#             DB_USER   = "sa"
-#             DB_PASSWORD = var.sql_password
-#         }
-#     }
+  request_parameters = {
+    "integration.request.path.id" = "method.request.path.id"
+  }
+}
 
-#     vpc_config {
-#         subnet_ids         = data.aws_subnets.default.ids
-#         security_group_ids = [data.aws_security_group.default.id]
-#     }
+# Api key
+resource "aws_api_gateway_api_key" "cliente_api_key" {
+  name        = "key"
+  description = "API Key"
+  enabled     = true
+  value       = var.api_key
+}
 
-#     source_code_hash = filebase64sha256("lambda_function.zip")
-# }
+# Usage plan
+resource "aws_api_gateway_usage_plan" "cliente_usage_plan" {
+  name = "usage-plan"
 
-# resource "aws_cloudwatch_event_rule" "rds_event_rule" {
-#     name        = "rds-instance-available-rule"
-#     description = "Regla para ejecutar Lambda cuando RDS esté disponible"
-#     event_pattern = jsonencode({
-#         source = ["aws.rds"],
-#         detail_type = ["RDS DB Instance Event"],
-#         detail = {
-#             "EventCategories" = ["Availability"]
-#             "SourceType"      = ["DB_INSTANCE"]
-#             "SourceIdentifier" = [aws_db_instance.RDSDBInstance.id]
-#         }
-#     })
-# }
+  api_stages {
+    api_id = aws_api_gateway_rest_api.rest_api.id
+    stage  = aws_api_gateway_stage.prod_stage.stage_name
+  }
 
-# resource "aws_cloudwatch_event_target" "lambda_target" {
-#     rule      = aws_cloudwatch_event_rule.rds_event_rule.name
-#     target_id = "lambdaTarget"
-#     arn       = aws_lambda_function.db_creation_lambda.arn
-# }
+  throttle_settings {
+    burst_limit = 5
+    rate_limit  = 10
+  }
 
-# resource "aws_lambda_permission" "allow_eventbridge_to_invoke_lambda" {
-#     statement_id  = "AllowExecutionFromCloudWatch"
-#     action        = "lambda:InvokeFunction"
-#     function_name = aws_lambda_function.db_creation_lambda.function_name
-#     principal     = "events.amazonaws.com"
-#     source_arn    = aws_cloudwatch_event_rule.rds_event_rule.arn
-# }
+  quota_settings {
+    limit  = 1000
+    period = "MONTH"
+  }
+}
 
-# resource "aws_ebs_volume" "EC2Volume" {
-#     availability_zone = var.availability_zones[0]
-#     encrypted = false
-#     size = 30
-#     type = "gp3"
-#     tags = {
-#         Name = "Volume1"
-#     }
-# }
+# Usage plan key
+resource "aws_api_gateway_usage_plan_key" "cliente_key_link" {
+  key_id        = aws_api_gateway_api_key.cliente_api_key.id
+  key_type      = "API_KEY"
+  usage_plan_id = aws_api_gateway_usage_plan.cliente_usage_plan.id
+}
 
-# resource "aws_network_interface" "EC2NetworkInterface" {
-#     description = ""
-#     subnet_id = data.aws_subnets.default.ids[0]
-#     source_dest_check = true
-#     security_groups = [
-#         data.aws_security_group.default.id
-#     ]
-# }
+# Cloudwatch role for API Gateway
+resource "aws_api_gateway_account" "account_settings" {
+  cloudwatch_role_arn = data.aws_iam_role.lab_role.arn
+}
 
 #################################
 # Local variables
 #################################
 
-variable "availability_zones" {
-  default = ["us-east-1b", "us-east-1b", "us-east-1c", "us-east-1a", "us-east-1e", "us-east-1f"]
-}
-
 variable "ami_id" {
   default = "ami-03b4de1e633ccdc0f"
 }
@@ -856,18 +674,6 @@ variable "launch_template_name" {
   default = "ECSLaunchTemplate_T9qfjpHq5vIs"
 }
 
-variable "autoscaling_min_size" {
-  default = 3
-}
-
-variable "autoscaling_max_size" {
-  default = 4
-}
-
-variable "autoscaling_desired_capacity" {
-  default = 3
-}
-
 variable "api_container_name" {
   default = "api-candidatos-container"
 }
@@ -916,6 +722,10 @@ data "aws_iam_instance_profile" "lab_profile" {
   name = "LabInstanceProfile"
 }
 
+data "aws_availability_zones" "available" {
+  state = "available"
+}
+
 #################################
 # Tfvars variables
 #################################
@@ -946,10 +756,15 @@ variable "frontend_image" { # igual se puede dinamizar
   type        = string
 }
 
-# variable "db_init_image" {
-#   description = "Imagen para inicializar la base de datos"
-#   type        = string
-# }
+variable "api_key" {
+  description = "API Key"
+  type        = string
+}
+
+variable "desired_count" {
+  description = "Cantidad deseada de instancias"
+  type        = number  
+}
 
 #################################
 # Output variables
@@ -963,6 +778,14 @@ output "rds_sg_id" {
   value = aws_security_group.rds_sg.id
 }
 
+output "api_sg_id" {
+  value = aws_security_group.rds_sg.id
+}
+
+output "frontend_sg_id" {
+  value = aws_security_group.rds_sg.id
+}
+
 output "ecs_cluster_arn" {
   value = aws_ecs_cluster.ecs_cluster.arn
 }
@@ -985,4 +808,4 @@ output "listener_api_arn" {
 
 output "listener_frontend_arn" {
   value = aws_lb_listener.load_balancer_listener_frontend.arn
-}
+}
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
index 75267e1..7ba202e 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -187,6 +187,7 @@ services:
       dockerfile: Dockerfile
       args:
         PUBLIC_API_URL: "${PUBLIC_API_URL}"
+        PUBLIC_API_KEY: "${PUBLIC_API_KEY}"
     container_name: web_frontend
     ports:
       - "3000:3000"


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR integrates a public/admin API key flow across the frontend, API services, Terraform modules, and CI/CD pipeline by introducing key configuration, validation, and propagation.

- Frontend: passes PUBLIC_API_KEY into the build, sets ENV, and attaches `x-api-key` header on all API calls  
- API and Controllers: adds PUBLIC_API_KEY and ADMIN_API_KEY env vars in task definitions, enforces key checks in Controllers, and adjusts the `CrearBaseDeDatos` endpoint  
- Terraform & CI/CD: updates modules to accept key inputs and `desired_count`, updates S3 backend, and modifies the workflow to send the admin key header

### Reviewed Changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 8 comments.

<details>
<summary>Show a summary per file</summary>

| File                                                                 | Description                                                                                     |
| -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| docker-compose.yml                                                   | Added `PUBLIC_API_KEY` build-arg for the frontend service                                       |
| Terraform/modules/frontend/main.tf                                   | Introduced `public_api_key`, `desired_count`, `frontend_sg_id`; updated task/service settings  |
| Terraform/modules/api/main.tf                                        | Added `public_api_key`, `admin_api_key`, `desired_count`, `api_sg_id`; configured logging      |
| Terraform/main.tf                                                    | Passed `api_key`, `admin_api_key`, `desired_count` to modules; bumped S3 state bucket          |
| Frontend/src/utils/getDataFromApi.ts                                 | Hardcoded API key placeholder; added `x-api-key` header on fetch calls                          |
| Frontend/Dockerfile                                                  | Declared `ARG PUBLIC_API_KEY` and `ENV PUBLIC_API_KEY`                                          |
| Backend/Servicio_1/Api.Candidatos/Api.Candidatos/Program.cs          | Cleared default logging providers and added console logging                                    |
| Backend/Servicio_1/Api.Candidatos/Api.Candidatos/04_Controllers/...  | Enforced API key validation on endpoints; changed DB-init from POST to GET; exposed Test logic |
| .github/workflows/ci-cd.yml                                          | Updated `curl` until-loop to use GET and include `x-api-key` header                             |
</details>



<details>
<summary>Comments suppressed due to low confidence (5)</summary>

**Frontend/src/utils/getDataFromApi.ts:2**
* Avoid hardcoding API keys in source code. Replace this with import.meta.env.PUBLIC_API_KEY or a secure runtime configuration.
```
const apiKey = "Z3Rpby12b3RhY2lhcGkta2V5IGhheSBxdWUgYWxhcmdhciBwYXJhIGxsZWdhciBhIDMwIGNhcmFjdGVyZXM"; //import.meta.env.PUBLIC_API_KEY;
```
**Backend/Servicio_1/Api.Candidatos/Api.Candidatos/04_Controllers/CandidatesController.cs:102**
* Typo in route template: 'ObtenerVotosCadidato' should be spelled 'ObtenerVotosCandidato'.
```
[HttpGet("ObtenerVotosCadidato/{id}")]
```
**Backend/Servicio_1/Api.Candidatos/Api.Candidatos/04_Controllers/CandidatesController.cs:113**
* Using GET for an operation that initializes or mutates state violates REST principles. Consider restoring POST for 'CrearBaseDeDatos'.
```
[HttpGet("CrearBaseDeDatos")]
```
**Terraform/modules/frontend/main.tf:140**
* This `aws_subnet.zone` data block is defined but never used. Remove it to avoid orphan resources.
```
data "aws_subnet" "zone" {
```
**Terraform/modules/api/main.tf:155**
* This `aws_subnet.zone` data block is defined but never consumed. Consider deleting it to keep the module lean.
```
data "aws_subnet" "zone" {
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-05-18T10:41:08Z
user: login: alduncin139998
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-18T11:02:32Z
user: login: alduncin139998
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-18T11:02:48Z
user: login: alduncin139998
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-18T11:03:10Z
user: login: alduncin139998
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-18T11:04:09Z
user: login: alduncin139998
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-18T11:04:47Z
user: login: alduncin139998
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-18T11:05:01Z
user: login: alduncin139998
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-18T11:05:16Z
user: login: alduncin139998
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-18T11:05:42Z
user: login: alex17alex17
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-18T11:05:49Z

Review comments:
diff_hunk: @@ -17,7 +18,8 @@ export async function obtenerTodosCandidatos() {
     `${apiUrl}/api/Candidates/ObtenerTodosCandidatos`,
     {
       method: "GET",
-      headers: { "Content-Type": "application/json" },
+      headers: { "Content-Type": "application/json",
+                 "x-api-key": apiKey},
path: Frontend/src/utils/getDataFromApi.ts
user: login: Copilot
body: [nitpick] The header construction is duplicated across multiple API calls. Consider extracting header setup into a shared helper to reduce duplication.
```suggestion
      headers: getHeaders(),
```
created_at: 2025-05-18T10:41:07Z
author_association: NONE
diff_hunk: @@ -24,18 +26,33 @@ public CandidatesController(ICandidateService service, IDatabaseScriptService sc
     [HttpGet("Test")]
     public ActionResult<string> GetTest()
     {
-        return "hola";
+        Console.WriteLine("== Headers recibidos ==");
+        string response = string.Empty;
+        foreach (var header in Request.Headers)
+        {
+            Console.WriteLine($"{header.Key}: {header.Value}");
+            response += $"{header.Key}: {header.Value}\n";
+        }
+        return "Test OK: " + response;
path: Backend/Servicio_1/Api.Candidatos/Api.Candidatos/04_Controllers/CandidatesController.cs
user: login: Copilot
body: This Test endpoint returns all headers (including sensitive values) in its response. Remove or restrict this to prevent leaking secrets.
created_at: 2025-05-18T10:41:07Z
author_association: NONE
diff_hunk: @@ -193,6 +175,17 @@ variable "listener_frontend_arn" {
   type        = string
 }
 
+variable "desired_count" {
+  description = "Número deseado de instancias"
+  type        = number
+  default     = 1
+}
+
+variable "public_api_key" {
+  description = "API key pública"
+  type        = string  
path: Terraform/modules/frontend/main.tf
user: login: Copilot
body: API keys are sensitive. Mark `public_api_key` as `sensitive = true` to prevent exposure in logs and state files.
```suggestion
  type        = string
  sensitive   = true
```
created_at: 2025-05-18T10:41:07Z
author_association: NONE
diff_hunk: @@ -205,6 +200,22 @@ variable "listener_api_arn" {
   type        = string
 }
 
+variable "desired_count" {
+  description = "Número deseado de instancias"
+  type        = number
+  default     = 1
+}
+
+variable "public_api_key" {
+  description = "API key pública"
+  type        = string
+}
+
+variable "admin_api_key" {
+  description = "API key de administrador"
+  type        = string
path: Terraform/modules/api/main.tf
user: login: Copilot
body: API keys are sensitive. Mark `public_api_key` as `sensitive = true` to avoid leaking them in Terraform outputs or plans.
```suggestion
  type        = string
  sensitive   = true
}

variable "admin_api_key" {
  description = "API key de administrador"
  type        = string
  sensitive   = true
```
created_at: 2025-05-18T10:41:07Z
author_association: NONE
diff_hunk: @@ -205,6 +200,22 @@ variable "listener_api_arn" {
   type        = string
 }
 
+variable "desired_count" {
+  description = "Número deseado de instancias"
+  type        = number
+  default     = 1
+}
+
+variable "public_api_key" {
+  description = "API key pública"
+  type        = string
+}
+
+variable "admin_api_key" {
+  description = "API key de administrador"
+  type        = string
path: Terraform/modules/api/main.tf
user: login: Copilot
body: Admin API key is sensitive. Add `sensitive = true` to `admin_api_key` to protect it in state and logs.
```suggestion
  type        = string
  sensitive   = true
```
created_at: 2025-05-18T10:41:08Z
author_association: NONE
diff_hunk: @@ -27,6 +27,10 @@ resource "aws_ecs_task_definition" "task_frontend" {
         {
           name  = "PUBLIC_API_URL"
           value = "http://${var.lb_dns_name}:8080"
+        },
+        {
+          name  = "PUBLIC_API_KEY"
+          value = "${var.public_api_key}"
path: Terraform/modules/frontend/main.tf
user: login: Copilot
body: Unnecessary interpolation in Terraform 0.12+: you can simplify this to `value = var.public_api_key` directly.
```suggestion
          value = var.public_api_key
```
created_at: 2025-05-18T10:41:08Z
author_association: NONE
diff_hunk: @@ -27,6 +27,14 @@ resource "aws_ecs_task_definition" "task_api_candidatos" {
         {
           name  = "CORS_ORIGIN"
           value = "http://${var.lb_dns_name}:3000"
+        },
+        {
+          name  = "PUBLIC_API_KEY"
+          value = "${var.public_api_key}"
+        },
+        {
+          name  = "ADMIN_API_KEY"
+          value = "${var.admin_api_key}"
path: Terraform/modules/api/main.tf
user: login: Copilot
body: Unnecessary interpolation in Terraform 0.12+: replace with `value = var.public_api_key`.
```suggestion
          value = var.public_api_key
        },
        {
          name  = "ADMIN_API_KEY"
          value = var.admin_api_key
```
created_at: 2025-05-18T10:41:08Z
author_association: NONE
diff_hunk: @@ -27,6 +27,14 @@ resource "aws_ecs_task_definition" "task_api_candidatos" {
         {
           name  = "CORS_ORIGIN"
           value = "http://${var.lb_dns_name}:3000"
+        },
+        {
+          name  = "PUBLIC_API_KEY"
+          value = "${var.public_api_key}"
+        },
+        {
+          name  = "ADMIN_API_KEY"
+          value = "${var.admin_api_key}"
path: Terraform/modules/api/main.tf
user: login: Copilot
body: Unnecessary interpolation in Terraform 0.12+: use `value = var.admin_api_key` instead.
```suggestion
          value = var.admin_api_key
```
created_at: 2025-05-18T10:41:08Z
author_association: NONE
diff_hunk: @@ -17,7 +18,8 @@ export async function obtenerTodosCandidatos() {
     `${apiUrl}/api/Candidates/ObtenerTodosCandidatos`,
     {
       method: "GET",
-      headers: { "Content-Type": "application/json" },
+      headers: { "Content-Type": "application/json",
+                 "x-api-key": apiKey},
path: Frontend/src/utils/getDataFromApi.ts
user: login: alduncin139998
body: Lo dejamos para trabajo futuro. Sería hacer limpieza general y refactorización.
created_at: 2025-05-18T11:02:32Z
author_association: COLLABORATOR
diff_hunk: @@ -193,6 +175,17 @@ variable "listener_frontend_arn" {
   type        = string
 }
 
+variable "desired_count" {
+  description = "Número deseado de instancias"
+  type        = number
+  default     = 1
+}
+
+variable "public_api_key" {
+  description = "API key pública"
+  type        = string  
path: Terraform/modules/frontend/main.tf
user: login: alduncin139998
body: De momento no hace falta.
created_at: 2025-05-18T11:02:47Z
author_association: COLLABORATOR
diff_hunk: @@ -205,6 +200,22 @@ variable "listener_api_arn" {
   type        = string
 }
 
+variable "desired_count" {
+  description = "Número deseado de instancias"
+  type        = number
+  default     = 1
+}
+
+variable "public_api_key" {
+  description = "API key pública"
+  type        = string
+}
+
+variable "admin_api_key" {
+  description = "API key de administrador"
+  type        = string
path: Terraform/modules/api/main.tf
user: login: alduncin139998
body: Lo mismo, de momento no hace falta.
created_at: 2025-05-18T11:03:10Z
author_association: COLLABORATOR
diff_hunk: @@ -24,18 +26,33 @@ public CandidatesController(ICandidateService service, IDatabaseScriptService sc
     [HttpGet("Test")]
     public ActionResult<string> GetTest()
     {
-        return "hola";
+        Console.WriteLine("== Headers recibidos ==");
+        string response = string.Empty;
+        foreach (var header in Request.Headers)
+        {
+            Console.WriteLine($"{header.Key}: {header.Value}");
+            response += $"{header.Key}: {header.Value}\n";
+        }
+        return "Test OK: " + response;
path: Backend/Servicio_1/Api.Candidatos/Api.Candidatos/04_Controllers/CandidatesController.cs
user: login: alduncin139998
body: Es verdad, lo cambiaré en un commit más tarde para evitar empezar desde cero.
created_at: 2025-05-18T11:04:09Z
author_association: COLLABORATOR
diff_hunk: @@ -205,6 +200,22 @@ variable "listener_api_arn" {
   type        = string
 }
 
+variable "desired_count" {
+  description = "Número deseado de instancias"
+  type        = number
+  default     = 1
+}
+
+variable "public_api_key" {
+  description = "API key pública"
+  type        = string
+}
+
+variable "admin_api_key" {
+  description = "API key de administrador"
+  type        = string
path: Terraform/modules/api/main.tf
user: login: alduncin139998
body: Lo mismo de antes, para futuro.
created_at: 2025-05-18T11:04:47Z
author_association: COLLABORATOR
diff_hunk: @@ -27,6 +27,10 @@ resource "aws_ecs_task_definition" "task_frontend" {
         {
           name  = "PUBLIC_API_URL"
           value = "http://${var.lb_dns_name}:8080"
+        },
+        {
+          name  = "PUBLIC_API_KEY"
+          value = "${var.public_api_key}"
path: Terraform/modules/frontend/main.tf
user: login: alduncin139998
body: Para limpieza general
created_at: 2025-05-18T11:05:01Z
author_association: COLLABORATOR
diff_hunk: @@ -27,6 +27,14 @@ resource "aws_ecs_task_definition" "task_api_candidatos" {
         {
           name  = "CORS_ORIGIN"
           value = "http://${var.lb_dns_name}:3000"
+        },
+        {
+          name  = "PUBLIC_API_KEY"
+          value = "${var.public_api_key}"
+        },
+        {
+          name  = "ADMIN_API_KEY"
+          value = "${var.admin_api_key}"
path: Terraform/modules/api/main.tf
user: login: alduncin139998
body: Para limpieza general.
created_at: 2025-05-18T11:05:16Z
author_association: COLLABORATOR
diff_hunk: @@ -27,6 +27,14 @@ resource "aws_ecs_task_definition" "task_api_candidatos" {
         {
           name  = "CORS_ORIGIN"
           value = "http://${var.lb_dns_name}:3000"
+        },
+        {
+          name  = "PUBLIC_API_KEY"
+          value = "${var.public_api_key}"
+        },
+        {
+          name  = "ADMIN_API_KEY"
+          value = "${var.admin_api_key}"
path: Terraform/modules/api/main.tf
user: login: alduncin139998
body: Para limpieza general.
created_at: 2025-05-18T11:05:42Z
author_association: COLLABORATOR

PR comments:
user: login: infracost[bot]
created_at: 2025-05-18T10:37:57Z
body: [//]: <> (infracost-ghapp-comment, valid-at=2025-05-18T10:57:44Z)
<h3>💰 Infracost report</h3>

<p>Consider fixing these issues, they don't align with your company's FinOps policies & the Well-Architected Framework. <b>Add a PR comment with <code>@infracost help</code> to see how you can dismiss or snooze issues and unblock your PR.</b></p>

<table>
  <tr><td colspan="2" width="1000px">Tagging policies</td></tr>
<tr><td colspan="2" title="Failure">
  <details open>
    <summary>
    <b>🔴 FinOps tags</b>
    </summary><br/>

This example Tagging policy shows how you can enforce required FinOps tag keys/values in pull requests. This example checks for the tags 'Service' (can have any value) and 'Environment' (must be Dev/Stage/Prod) on all taggable resources being changed in the pull request. You can adjust it from https://dashboard.infracost.io > Governance > Tagging policies. You have a 14 day trial of this feature as it's part of Infracost Cloud.
  </details>
</td></tr>
<tr><td></td><td>

resource [module.api.aws_ecs_service.service_api_candidatos](https://github.com/oielay/gtio_votacion/blob/7abf8226d88b2acdd4661fad96633b3b2340626b/Terraform/main.tf#L56)
* Missing mandatory tag `Environment`. Must be one of ` Dev `, ` Stage `, ` Prod `. Consider using default tags to avoid adding tags to individual resources.
* Missing mandatory tag: ` Service `. Consider using default tags to avoid adding tags to individual resources.
* Dynamically created `task` resources will not have tag(s) `Service`, `Environment` because tag propagation is not configured. Tag propagation should be configured by setting `propagate_tags` to a valid value (`TASK_DEFINITION`, `SERVICE`)

in project ` Terraform-terraform `

</td></tr>
<tr><td></td><td>

resource [module.api.aws_ecs_task_definition.task_api_candidatos](https://github.com/oielay/gtio_votacion/blob/7abf8226d88b2acdd4661fad96633b3b2340626b/Terraform/main.tf#L56)
* Missing mandatory tag `Environment`. Must be one of ` Dev `, ` Stage `, ` Prod `. Consider using default tags to avoid adding tags to individual resources.
* Missing mandatory tag: ` Service `. Consider using default tags to avoid adding tags to individual resources.

in project ` Terraform-terraform `

</td></tr>
<tr><td></td><td>

resource [module.frontend.aws_ecs_service.service_frontend](https://github.com/oielay/gtio_votacion/blob/7abf8226d88b2acdd4661fad96633b3b2340626b/Terraform/main.tf#L43)
* Missing mandatory tag `Environment`. Must be one of ` Dev `, ` Stage `, ` Prod `. Consider using default tags to avoid adding tags to individual resources.
* Missing mandatory tag: ` Service `. Consider using default tags to avoid adding tags to individual resources.
* Dynamically created `task` resources will not have tag(s) `Service`, `Environment` because tag propagation is not configured. Tag propagation should be configured by setting `propagate_tags` to a valid value (`TASK_DEFINITION`, `SERVICE`)

in project ` Terraform-terraform `

</td></tr>
<tr><td></td><td>

resource [module.frontend.aws_ecs_task_definition.task_frontend](https://github.com/oielay/gtio_votacion/blob/7abf8226d88b2acdd4661fad96633b3b2340626b/Terraform/main.tf#L43)
* Missing mandatory tag `Environment`. Must be one of ` Dev `, ` Stage `, ` Prod `. Consider using default tags to avoid adding tags to individual resources.
* Missing mandatory tag: ` Service `. Consider using default tags to avoid adding tags to individual resources.

in project ` Terraform-terraform `

</td></tr>
<tr><td></td><td>

resource [module.infrastructure.aws_api_gateway_api_key.cliente_api_key](https://github.com/oielay/gtio_votacion/blob/7abf8226d88b2acdd4661fad96633b3b2340626b/Terraform/main.tf#L32)
* Missing mandatory tag `Environment`. Must be one of ` Dev `, ` Stage `, ` Prod `. Consider using default tags to avoid adding tags to individual resources.
* Missing mandatory tag: ` Service `. Consider using default tags to avoid adding tags to individual resources.

in project ` Terraform-terraform `

</td></tr>
<tr><td></td><td>
  ... and 10 more.
</td></tr>
  <tr><td colspan="2">... view <a href="https://dashboard.infracost.io/org/oierlayana/repos/2844d788-c330-40e0-88db-b5e1d55c099d/runs/f44d1dba-8544-41b8-91af-5796e1aeaf00">all issues</a> in Infracost Cloud</td></tr>
</table>

<details >
  <summary><b>Monthly estimate decreased by $10 📉</b></summary>
  <br/>

<table>
  <thead>
    <td>Changed project</td>
    <td><span title="Baseline costs are consistent charges for provisioned resources, like the hourly cost for a virtual machine, which stays constant no matter how much it is used. Infracost estimates these resources assuming they are used for the whole month (730 hours).">Baseline cost</span></td>
    <td><span title="Usage costs are charges based on actual usage, like the storage cost for an object storage bucket. Infracost estimates these resources using the monthly usage values in the usage-file.">Usage cost</span>*</td>
    <td>Total change</td>
    <td>New monthly cost</td>
  </thead>
  <tbody>
    <tr>
      <td>Terraform-terraform</td>
      <td align="right">-$10</td>
      <td align="right">-</td>
      <td align="right">-$10 (-13%)</td>
      <td align="right">$69</td>
    </tr>
  </tbody>
</table>

*Usage costs can be estimated by updating [Infracost Cloud settings](https://dashboard.infracost.io/org/oierlayana/settings/usage-cost-defaults), see [docs](https://www.infracost.io/docs/features/usage_based_resources/#infracost-usageyml) for other options.

  <details>
  <summary>Estimate details </summary>

```
Key: * usage cost, ~ changed, + added, - removed

──────────────────────────────────
Project: Terraform-terraform
Module path: Terraform
Workspace: terraform-terraform

~ module.infrastructure.aws_autoscaling_group.AutoScalingAutoScalingGroup
  +$8 ($25 → $34)

    ~ module.infrastructure.aws_launch_template.EC2LaunchTemplate

        ~ Instance usage (Linux/UNIX, on-demand, t2.micro)
          +$8 ($25 → $34)

+ module.infrastructure.aws_cloudwatch_log_group.api_logs
  Monthly cost depends on usage

    + Data ingested
      Monthly cost depends on usage
        +$0.50 per GB

    + Archival Storage
      Monthly cost depends on usage
        +$0.03 per GB

    + Insights queries data scanned
      Monthly cost depends on usage
        +$0.005 per GB

- module.api.aws_instance.ecs_instance
  -$9

    - Instance usage (Linux/UNIX, on-demand, t2.micro)
      -$8

    - root_block_device

        - Storage (general purpose SSD, gp2)
          -$0.80

- module.frontend.aws_instance.ecs_instance_frontend
  -$9

    - Instance usage (Linux/UNIX, on-demand, t2.micro)
      -$8

    - root_block_device

        - Storage (general purpose SSD, gp2)
          -$0.80

Monthly cost change for Terraform-terraform (Module path: Terraform, Workspace: terraform-terraform)
Amount:  -$10 ($79 → $69)
Percent: -13%

──────────────────────────────────
Key: * usage cost, ~ changed, + added, - removed

*Usage costs can be estimated by updating Infracost Cloud settings, see docs for other options.

44 cloud resources were detected:
∙ 9 were estimated
∙ 35 were free
```
  </details>
</details>

<hr/>

There are also [29 pre-existing issues](https://dashboard.infracost.io/org/oierlayana/repos/2844d788-c330-40e0-88db-b5e1d55c099d?utm_source=pr_comment&utm_content=preexisting_issues) in the `desarrollo` branch. Fix some to climb your [org’s leaderboard](https://dashboard.infracost.io/org/oierlayana?utm_source=pr_comment&utm_content=org_leaderboard#leaderboard) 🥇

<sub>
  This comment will be updated when code changes.
</sub>

author_association: NONE
user: login: alex17alex17
created_at: 2025-05-18T11:03:49Z
body: Todos los cambios referentes a la api gateway son correctos.
author_association: COLLABORATOR