Panel web ligero para listar y controlar servicios ECS y RDS (start/stop) a través de una AWS Lambda segura, con autenticación y token firmado (tipo JWT).
Este README explica todo el flujo para que QA pueda revisarlo sin desmontarte el tinglado:
- Arquitectura general
- Permisos necesarios de la Lambda
- Autenticación y seguridad (login + token)
- Endpoints expuestos (ECS + RDS)
- Funcionamiento del HTML / dashboard (pestañas ECS y RDS)
- Casos de prueba recomendados
Este proyecto se distribuye bajo la licencia Apache License 2.0.
Consulta el archivo LICENSE para más detalles.
-
AWS Lambda (
app.py)- Expone una API minimalista con endpoints para:
POST /login→ genera token firmado.GET /services→ lista servicios ECS.POST /action→ start/stop de servicios ECS.GET /rds/instances→ lista instancias RDS.POST /rds/action→ start/stop de instancias RDS.
- Se puede exponer mediante:
- Lambda Function URL, o
- API Gateway (HTTP API / REST API).
- Expone una API minimalista con endpoints para:
-
Dashboard HTML (
index.html)- Aplicación web estática (no requiere servidor).
- Incluye:
- Pantalla de login.
- Pestañas:
- ECS Services: gestión de servicios ECS.
- RDS Instances: gestión de instancias RDS.
- Para cada pestaña:
- Tabla con información relevante.
- Filtros (texto, estado, cluster/engine).
- Acciones masivas:
- Start de elementos seleccionados.
- Stop de elementos seleccionados.
-
Amazon ECS
- La Lambda se conecta vía
boto3para:- Listar clusters.
- Listar servicios.
- Describir servicios.
- Modificar
desiredCount(start/stop).
- La Lambda se conecta vía
-
Amazon RDS
- La Lambda se conecta vía
boto3para:- Listar instancias RDS.
- Ejecutar
StartDBInstance/StopDBInstancesobre instancias soportadas.
- La Lambda se conecta vía
La Lambda necesita un rol de ejecución con permisos sobre ECS y RDS.
Política mínima recomendada:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ecs:ListClusters",
"ecs:ListServices",
"ecs:DescribeServices",
"ecs:UpdateService"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"rds:DescribeDBInstances",
"rds:StartDBInstance",
"rds:StopDBInstance"
],
"Resource": "*"
}
]
}Nota:
En producción es recomendable restringirResourcea los ARN de clusters ECS e instancias RDS concretas en las cuentas/regiones donde va a operar.
- Runtime:
Python 3.11oPython 3.12. - Handler:
app.lambda_handler
(asumiendo que el archivo se llamaapp.py).
En la consola de Lambda → Configuration → Environment variables:
DASH_USER
Usuario autorizado para el panel (ej:ops_admin).DASH_PASS
Contraseña del usuario anterior.JWT_SECRET
Secreto largo para firmar el token (mínimo 32 caracteres, aleatorio).- Ejemplo:
a_very_long_random_secret_key_987654321
- Ejemplo:
JWT_TTL_SECONDS(opcional)
Tiempo de vida del token en segundos (por defecto1800= 30 min).
Si DASH_USER, DASH_PASS o JWT_SECRET no están configurados, la Lambda devuelve error 500 cuando se intenta hacer login.
- Crear una Function URL para la Lambda (HTTPS).
- Configurar:
- Auth type:
NONE(la protección se hace en la Lambda con token JWT). - CORS: permitir el origen desde el que servirás el HTML.
- Auth type:
- El dashboard usará una URL tipo:
https://<id>.lambda-url.<region>.on.aws
- Crear un HTTP API o REST API.
- Integrarlo con la Lambda como proxy.
- Configurar rutas:
POST /loginGET /servicesPOST /actionGET /rds/instancesPOST /rds/action
- El dashboard usará una URL tipo:
https://<id>.execute-api.<region>.amazonaws.com/prod
En el HTML, esa URL se define en la constante
API_BASE_URL.
La Lambda implementa un enrutado simple en lambda_handler según:
method→GET,POST,OPTIONSpath→/,/login,/services,/action,/rds/instances,/rds/action
- Sin token todavía (es el único endpoint público).
- Recibe JSON:
{
"user": "ops_admin",
"pass": "contraseña"
}-
Respuestas:
-
200 OK (credenciales válidas):
{ "token": "<JWT_firmado>", "user": "ops_admin", "role": "admin", "expiresIn": 1800 } -
401 Unauthorized (credenciales incorrectas):
{ "error": "Credenciales inválidas" } -
500 Internal Server Error (config mal hecha):
{ "error": "DASH_USER/DASH_PASS no configurados" }
-
Se genera un token HS256 tipo JWT con:
-
Cabecera:
{ "alg": "HS256", "typ": "JWT" } -
Payload:
{ "sub": "<usuario>", "role": "admin", "iat": <timestamp_ahora>, "exp": <timestamp_expiración>, "iss": "ecs-rds-dashboard", "aud": "ecs-rds-dashboard-ui" } -
Firma:
HMAC-SHA256conJWT_SECRET.
La Lambda verifica en cada petición protegida:
- Que la firma es válida.
- Que no está expirado (
exp). - Que
issyaudson correctos. - Que, para operaciones sensibles (
/action,/rds/action), elrolesea"admin".
Para /services, /action, /rds/instances y /rds/action, el cliente debe enviar:
Authorization: Bearer <token>-
Token ausente / inválido / expirado → 401 Unauthorized:
{ "error": "Token ausente" }{ "error": "Token expirado" } -
Rol insuficiente (si se aplicaran roles distintos a admin) → 401 Unauthorized:
{ "error": "Permisos insuficientes" }
-
Protegido → requiere
Authorization: Bearer <token>. -
Parámetros (query string):
cluster(opcional) → filtra por nombre de cluster exacto.
-
Ejemplo:
curl -X GET "https://<API_BASE_URL>/services?cluster=my-ecs-cluster" -H "Authorization: Bearer <TOKEN>"- Respuesta (200 OK):
{
"services": [
{
"clusterArn": "arn:aws:ecs:us-east-1:123456789012:cluster/my-ecs-cluster",
"clusterName": "my-ecs-cluster",
"serviceArn": "arn:aws:ecs:us-east-1:123456789012:service/my-ecs-cluster/api-backend",
"serviceName": "api-backend",
"status": "ACTIVE",
"desiredCount": 1,
"runningCount": 1,
"launchType": "FARGATE",
"createdAt": "2025-11-15T10:25:01.123Z",
"schedulingStrategy": "REPLICA"
}
]
}-
Protegido → requiere
Authorization: Bearer <token>con rol"admin". -
Cuerpo JSON:
{
"clusterArn": "arn:aws:ecs:us-east-1:123456789012:cluster/my-ecs-cluster",
"serviceName": "api-backend",
"action": "stop",
"desiredCount": 1
}-
action:"stop"→ fuerzadesiredCount = 0."start"→ usa:desiredCountdel cuerpo (si se envía), o1por defecto.
-
Ejemplo
stop:
curl -X POST "https://<API_BASE_URL>/action" -H "Authorization: Bearer <TOKEN>" -H "Content-Type: application/json" -d '{
"clusterArn": "arn:aws:ecs:us-east-1:123456789012:cluster/my-ecs-cluster",
"serviceName": "api-backend",
"action": "stop"
}'- Respuesta (200 OK):
{
"result": {
"clusterArn": "arn:aws:ecs:us-east-1:123456789012:cluster/my-ecs-cluster",
"serviceName": "api-backend",
"newDesiredCount": 0,
"runningCount": 0,
"status": "ACTIVE"
}
}-
Protegido → requiere
Authorization: Bearer <token>. -
Parámetros (query string, opcionales):
engine→ filtra por engine exacto (ej:mysql,postgres,aurora-mysql).status→ filtra por status exacto (ej:available,stopped).
-
Ejemplo:
curl -X GET "https://<API_BASE_URL>/rds/instances?engine=postgres&status=available" -H "Authorization: Bearer <TOKEN>"- Respuesta (200 OK):
{
"instances": [
{
"dbInstanceIdentifier": "mi-db-prod",
"dbInstanceArn": "arn:aws:rds:us-east-1:123456789012:db:mi-db-prod",
"engine": "postgres",
"engineVersion": "15.3",
"dbInstanceClass": "db.t3.medium",
"multiAZ": false,
"availabilityZone": "us-east-1a",
"status": "available",
"endpoint": "mi-db-prod.xxxxxxxx.us-east-1.rds.amazonaws.com",
"allocatedStorage": 100,
"storageType": "gp3",
"clusterIdentifier": null,
"publiclyAccessible": false
}
]
}- Protegido → requiere
Authorization: Bearer <token>con rol"admin". - Cuerpo JSON:
{
"dbInstanceIdentifier": "mi-db-prod",
"action": "stop"
}action:"stop"→ llama internamente ards.stop_db_instance."start"→ llama internamente ards.start_db_instance.
Nota:
No todas las instancias RDS soportanstop/start(ej: ciertas configuraciones Multi-AZ o Aurora).
Si la operación no es válida, RDS devolverá un error que la Lambda propagará como 500 con mensaje.
- Respuesta (200 OK):
{
"result": {
"dbInstanceIdentifier": "mi-db-prod",
"status": "stopping",
"engine": "postgres",
"dbInstanceClass": "db.t3.medium",
"availabilityZone": "us-east-1a",
"multiAZ": false
}
}- Toda la comunicación se hace vía HTTPS (Function URL o API Gateway).
- El header
Authorization: Bearerviaja cifrado por TLS → no se envían credenciales en texto plano.
- No se pasa
user/passen query string ni en cada request. - El login se hace una única vez (
POST /login), y después solo se usa el token.
- El token tiene un
expdefinido porJWT_TTL_SECONDS(por defecto, 30 minutos). - Una vez expirado → cualquier endpoint protegido responde 401 y el front fuerza re-login.
- Verificación de:
- firma del token,
- expiración,
issyaud,rolepara operaciones sensibles.
- IAM de la Lambda limitado solo a las acciones estrictamente necesarias (ECS y RDS).
- CORS controlado (en prod se recomienda sustituir
Access-Control-Allow-Origin: *por el dominio real del dashboard).
En el HTML hay una constante:
const API_BASE_URL = "https://<tu-url-de-lambda-o-api-gw>";Debe apuntar a la URL base donde está expuesta la Lambda.
-
Pantalla de login
- Pide
usuario+contraseña. - Hace
POST /logina la API. - Si el login es correcto:
- Guarda
token,user,roleen memoria. - Oculta la tarjeta de login.
- Muestra el dashboard.
- Carga por defecto la pestaña ECS (
GET /services).
- Guarda
- Pide
-
Pestañas
- ECS Services:
- Lista y controla servicios ECS (start/stop).
- RDS Instances:
- Lista y controla instancias RDS (start/stop).
- Cambiar de pestaña no requiere re-login; reutiliza el mismo token.
- ECS Services:
- Tabla con columnas:
- Selección (checkbox),
- Servicio,
- Cluster,
- Desired / Running,
- Launch type,
- Estado (badge "Activo"/"Parado").
- Filtros:
- Texto (servicio/cluster).
- Estado (
Todos,Con desiredCount > 0,Con desiredCount = 0). - Nombre de cluster (select dinámico).
- Acciones:
Start seleccionados→POST /actionconaction="start".Stop seleccionados→POST /actionconaction="stop".
- Otros:
- Checkbox global para seleccionar todos los servicios filtrados.
- Indicador de nº de servicios visibles y seleccionados.
- Tabla con columnas:
- Selección (checkbox),
- DB Identifier,
- Engine,
- Clase,
- AZ / Multi-AZ,
- Status (badge).
- Filtros:
- Texto (db identifier / engine).
- Engine (select dinámico).
- Estado:
Todos,Sólo available,Sólo stopped.
- Acciones:
Start seleccionados→POST /rds/actionconaction="start".Stop seleccionados→POST /rds/actionconaction="stop".
- Otros:
- Checkbox global para seleccionar todas las instancias filtradas.
- Indicador de nº de instancias visibles y seleccionadas.
- Si la API responde con 401 (token ausente/expirado):
- El front elimina el token en memoria.
- Oculta el dashboard.
- Muestra de nuevo el login con mensaje de “Sesión expirada o no autorizada”.
- Usuario abre
index.htmlen el navegador. - Ve la pantalla de login.
- Introduce
usuario+contraseña. - El front hace
POST /login:- Si ok → recibe
tokeny lo guarda. - Si ko → muestra error.
- Si ok → recibe
- Tras login:
- Se muestra el dashboard con pestañas ECS y RDS.
- Se carga la lista de servicios ECS (
GET /services).
- Al cambiar a la pestaña RDS:
- Se llama a
GET /rds/instances.
- Se llama a
- En cada pestaña:
- El usuario puede filtrar, seleccionar y lanzar start/stop.
- Cada acción genera llamadas
POST /action(ECS) oPOST /rds/action(RDS). - Tras las acciones, se recarga la lista correspondiente.
GET /servicessinAuthorization:- 401 Unauthorized.
POST /logincon credenciales incorrectas:- 401 con
{"error": "Credenciales inválidas"}.
- 401 con
POST /logincon credenciales correctas:- 200 con
token,user,role,expiresIn.
- 200 con
- Usar un token correcto en
GET /servicesyGET /rds/instances:- 200 y datos correspondientes.
- Modificar 1 carácter del token:
- 401 (firma inválida).
- Esperar más del tiempo
JWT_TTL_SECONDSy reutilizar el token:- 401 (token expirado).
- Sin parámetro
cluster:- Lista servicios de todos los clusters.
- Con
cluster=<nombre>:- Lista sólo servicios de ese cluster.
POST /actionconaction="stop"sobre servicio condesiredCount > 0:newDesiredCountdebe ser0.
POST /actionconaction="start"sobre servicio parado:newDesiredCountdebe reflejar el valor enviado (por defecto1).
POST /actionsin token:- 401.
POST /actioncon cuerpo incompleto:- 400 con mensaje de campos obligatorios.
GET /rds/instancessin filtros:- Lista todas las instancias accesibles.
GET /rds/instances?engine=postgres&status=available:- Lista sólo instancias que cumplan ambos criterios.
POST /rds/actionconaction="stop"sobre una instancia que admite stop:- Status devuelto debe pasar a
stopping/stoppedsegún el flujo de RDS.
- Status devuelto debe pasar a
POST /rds/actionconaction="start"sobre instanciastopped:- Status devuelto debe ser
starting/availablesegún el flujo.
- Status devuelto debe ser
POST /rds/actionsobre instancia que no admite stop:- RDS devolverá error; la Lambda lo propagará como 500 con mensaje en JSON.
POST /rds/actionsin token:- 401.
- Antes de login:
- No se deben mostrar datos de ECS ni RDS.
- Después de login:
- Pestaña ECS:
- Lista servicios,
- Filtros y selección global funcionan.
- Pestaña RDS:
- Lista instancias,
- Filtros y selección global funcionan.
- Pestaña ECS:
- Al expirar el token:
- El dashboard deja de funcionar y el usuario es redirigido al login.
- Reemplazar
Access-Control-Allow-Origin: "*"por el dominio concreto del panel (ej:https://ops.miempresa.com). - Colocar la API detrás de:
- API Gateway + WAF,
- Throttling / rate limiting,
- Logs centralizados.
- Integrar con IdP corporativo (Cognito, Azure AD, Okta) para SSO y MFA.
- Añadir auditoría:
- Logging de
sub(usuario) + acción + target (ECS servicio o RDS instancia) en CloudWatch o SIEM.
- Logging de
Este sistema implementa un panel interno para gestión de:
- Servicios ECS (start/stop vía
desiredCount). - Instancias RDS (start/stop vía
StartDBInstance/StopDBInstance).
Con:
- Autenticación basada en HTTPS + token firmado de vida corta.
- Permisos IAM reducidos a lo estrictamente necesario.
- UI con login, pestañas, filtros, acciones masivas y feedback visual.
Con este README, QA tiene trazado todo el comportamiento esperado, tanto de backend (Lambda) como de frontend (HTML), incluyendo las consideraciones de seguridad y los casos de prueba recomendados para ECS y RDS.