Compile Flask/FastAPI apps to AWS SAM serverless
Project description
deployless
deployless es un compilador que convierte aplicaciones Flask (y en el futuro FastAPI) en templates de AWS SAM listos para desplegar como funciones Lambda serverless. No requiere reescribir tu app: simplemente añades anotaciones de configuración en tus routes.py y ejecutas deployless build.
Indice
- Qué es deployless
- Instalación
- Referencia de deployless.yaml
- pc.configure() en routes.py
- Recursos AWS
- @pc.cron() — Lambdas programadas
- @pc.route() — Split Lambdas por ruta
- @pc.lambda_function() — Lambdas standalone
- pc.shared_resource() — Recursos globales
- Fichero .env y secrets
- Comandos CLI
- Estructura de proyecto
- Ejemplo completo
Qué es deployless
deployless toma tu proyecto Flask estructurado por features y genera:
- Un
template.yamlde AWS SAM con una función Lambda por feature (y opcionalmente una por ruta específica). - Una carpeta
.dist/con el código empaquetado para cada Lambda, incluyendo unbootstrap.pygenerado automáticamente y unrequirements.txtfusionado. - Log Groups de CloudWatch con retención configurable para cada función.
Modelo mental
app/features/users/routes.py → UsersFunction (Lambda)
app/features/auth/routes.py → AuthFunction (Lambda)
app/features/tenant/routes.py → TenantFunction (Lambda)
Cada feature vive en su propia Lambda. Si un endpoint específico necesita configuración distinta (más memoria, timeout mayor), puedes "splittearlo" en su propia Lambda con @pc.route().
Flujo de compilación
deployless build
│
├── 1. Lee deployless.yaml
├── 2. Descubre app/features/*/routes.py
├── 3. Importa cada routes.py (extrae Blueprints y rutas)
├── 4. Lee metadata de pc.configure(), @pc.cron(), @pc.route()
├── 5. Valida (memoria, timeout, rutas duplicadas, schedules, etc.)
├── 6. Genera .dist/{Feature}Function/ para cada Lambda
└── 7. Escribe template.yaml
Instalación
Desde el repositorio (desarrollo local)
# Desde la raíz del proyecto
pip install -e ./deployless
# O con uv
uv add --editable ./deployless
Dependencias de deployless
pyyaml
click
flask # ya debes tenerlo instalado
Dependencia de runtime en cada Lambda
Cada Lambda generada necesita aws-wsgi para adaptar Flask al formato de eventos de API Gateway. deployless la añade automáticamente al requirements.txt de cada paquete .dist/.
pip install aws-wsgi
Referencia de deployless.yaml
Crea este fichero en la raíz del proyecto (al mismo nivel que requirements.txt). Todos los campos son opcionales; los valores por defecto están indicados.
# Nombre del proyecto
name: mi-app
# Proveedor cloud — solo "aws" soportado por ahora
provider: aws
# Stage de despliegue. Se puede sobreescribir con --stage en el CLI.
stage: dev
# Tags aplicados a todos los recursos de CloudFormation
tags:
Project: mi-app
Environment: production
# Rutas a los directorios clave del proyecto
paths:
features: app/features # Directorio donde viven las features
shared: app/shared # Código compartido (se copia en cada Lambda)
# Config global para todas las funciones Lambda
globals:
runtime: python3.13 # Runtime de Lambda
memory: 256 # MB (128–10240)
timeout: 30 # Segundos (1–900)
log_retention: 14 # Retención en CloudWatch (días)
# Valores válidos: 1,3,5,7,14,30,60,90,120,
# 150,180,365,400,545,731,1096,1827,3653
# Configuración del API Gateway
api:
endpoint_type: REGIONAL # REGIONAL | EDGE | PRIVATE
# CORS
cors:
allow_origin: "*" # O lista: ["https://mi-app.com"]
allow_methods: [GET, POST, PUT, DELETE, OPTIONS]
allow_headers: [Content-Type, Authorization, X-API-Key]
max_age: 3600 # Segundos que el browser cachea el preflight
# allow_credentials: true # No compatible con allow_origin: "*"
# Autenticación global del API Gateway
# (ver sección "Autenticación del API Gateway" para detalles)
auth:
type: cognito # cognito | lambda | iam
user_pool_arn: "arn:aws:cognito-idp:us-east-1:123456789:userpool/us-east-1_ABC"
name: CognitoAuthorizer # Opcional
scopes: [] # Opcional
# API Keys
api_keys: true # true = generar nueva key | "key-id" = usar existente
# Rate limiting (requiere api_keys)
usage_plan:
rate: 10000 # Requests/segundo
burst: 2000 # Pico máximo
quota: 1000000 # Opcional — total de requests
period: DAY # DAY | WEEK | MONTH (requerido si hay quota)
# Dominio personalizado
domain:
domain_name: api.mi-app.com
certificate_arn: "arn:aws:acm:us-east-1:123456789:certificate/abc-123"
base_path: /v1 # Opcional
route53: # Opcional — configura DNS automáticamente
hosted_zone_id: Z1234567890ABC
# Tipos MIME que API Gateway trata como binario (no UTF-8)
binary_media_types:
- image/png
- image/jpeg
- application/octet-stream
# Comprimir respuestas mayores a N bytes
minimum_compression_size: 1024
# Variables de entorno globales inyectadas en TODAS las funciones
env:
APP_ENV: production
LOG_LEVEL: INFO
# Fichero .env — variables de entorno y secrets
# Las variables normales se inyectan como env vars en todas las Lambdas.
# Las variables con prefijo SECRET_ se pushean a SSM Parameter Store como SecureString
# y se inyectan como dynamic references {{resolve:ssm-secure:...}}.
env_file: .env.production
# Clave KMS para cifrar los secrets en SSM (opcional).
# Si no se especifica, SSM usa la clave gestionada por AWS (aws/ssm).
# Acepta alias ("mi-app/secrets") o key ID / ARN.
secrets_kms: mi-app/secrets
Autenticación del API Gateway
Cognito User Pool
api:
auth:
type: cognito
user_pool_arn: "arn:aws:cognito-idp:us-east-1:123456789:userpool/us-east-1_ABC"
name: CognitoAuthorizer # Opcional, default: "CognitoAuthorizer"
scopes: # Opcional — scopes OAuth2 requeridos
- email
- profile
Lambda Authorizer (función personalizada)
api:
auth:
type: lambda
function_arn: "arn:aws:lambda:us-east-1:123456789:function:my-authorizer"
name: LambdaAuthorizer # Opcional, default: "LambdaAuthorizer"
ttl: 300 # Segundos antes de re-autorizar (0 = sin cache)
identity:
header: Authorization # Header donde está el token
IAM
api:
auth:
type: iam
Sobreescribir auth por feature
Desde routes.py, puedes sobreescribir la auth global para una feature completa:
import deployless as pc
# Todos los endpoints de esta feature son públicos (sin auth)
pc.configure(auth=None)
# Todos los endpoints de esta feature requieren API key
pc.configure(auth="api_key")
Sobreescribir auth por ruta individual (split Lambda)
@pc.route(memory=512, auth=None) # Este endpoint es público
@bp.route('/health', methods=['GET'])
def health_check():
return {"status": "ok"}
@pc.route(memory=1024, auth="api_key") # Este endpoint requiere API key
@bp.route('/export', methods=['POST'])
def export_data():
...
Jerarquía de auth (mayor prioridad primero)
@pc.route(auth=...) ← Ruta individual (solo split lambdas)
pc.configure(auth=...) ← Feature completa
api.auth en deployless.yaml ← Global
API Keys y Rate Limiting
api:
api_keys: true # Genera una nueva API key
usage_plan:
rate: 10000 # 10k requests/segundo
burst: 2000 # Pico de 2k simultáneos
quota: 1000000 # Máximo 1M requests por día
period: DAY
El ID de la API Key generada aparece en los Outputs del stack:
# Ver el valor de la key (no se muestra en Outputs por seguridad)
aws apigateway get-api-key --api-key <ApiKeyId> --include-value
Para usar una key existente en lugar de crear una nueva:
api:
api_keys: "abc123existingkeyid"
Reglas de validación
| Código | Regla |
|---|---|
| E00 | Validaciones de recursos: DynamoDB (key types, GSI, projection INCLUDE), S3 (bucket name DNS-compliant, 3–63 chars, sin underscores), SQS (queue name, visibility_timeout, message_retention, max_receive_count), KMS (alias format, key_usage/key_spec válidos, incompatibilidades ECC/SIGN_VERIFY), SSMParameter (name starts with /, chars válidos, type válido, value no vacío) |
| E01 | stage solo puede contener caracteres alfanuméricos |
| E02 | api.endpoint_type debe ser REGIONAL, EDGE o PRIVATE |
| E03 | globals.log_retention debe ser un valor válido de CloudWatch |
| E04 | allow_credentials: true no es compatible con allow_origin: "*" |
| E11 | api.auth.type debe ser cognito, lambda o iam |
| E12 | api.auth (cognito): user_pool_arn es obligatorio |
| E13 | api.auth (lambda): function_arn es obligatorio |
| E14 | api.usage_plan: rate y burst son obligatorios |
| E15 | api.usage_plan: period es obligatorio si hay quota |
| E16 | api.usage_plan.period debe ser DAY, WEEK o MONTH |
| E17 | api.domain: domain_name y certificate_arn son obligatorios |
| E18 | api.minimum_compression_size debe ser un entero >= 0 |
| E19 | ephemeral_storage fuera de rango (512–10240 MB) |
| E20 | reserved_concurrency debe ser >= 0 |
| E21 | provisioned_concurrency debe ser >= 1 |
| E22 | log_retention por feature debe ser un valor válido de CloudWatch |
| E23 | alarms.sns_topic_arn debe ser un ARN válido (empieza con arn:) |
| E24 | alarms.duration.threshold_pct debe estar entre 1 y 100 |
| E25 | lambda_function memory fuera de rango (128–10240 MB) |
| E26 | lambda_function timeout fuera de rango (1–900 s) |
| E27 | env_file especificado no existe |
| E28 | Variable SECRET_ con valor vacío |
| E29 | Formato inválido de secrets_kms |
pc.configure() en routes.py
pc.configure() se llama al nivel de módulo en routes.py para registrar la configuración Lambda de esa feature. Es un no-op en runtime: cuando tu app Flask arranca normalmente, esta llamada no hace nada visible. Solo el compilador de deployless la lee.
deployless detecta automáticamente en qué feature está siendo llamado inspeccionando el call stack.
Referencia completa de parámetros
import deployless as pc
pc.configure(
# ── Básico ──────────────────────────────────────────────────────────────
memory=512, # int — MB. Sobreescribe globals.memory (128–10240)
timeout=30, # int — Segundos. Sobreescribe globals.timeout (1–900)
description="Mi feature", # str — Descripción visible en CloudFormation
# ── Entorno ─────────────────────────────────────────────────────────────
env={"FLAG": "true"}, # dict — Env vars adicionales para esta Lambda
layers=["arn:aws:lambda:..."],# list — ARNs de Lambda Layers
# ── IAM ─────────────────────────────────────────────────────────────────
policies=[ # list — IAM policies inline (formato SAM)
"AmazonDynamoDBReadOnlyAccess", # Policy gestionada por nombre
{"DynamoDBCrudPolicy": {"TableName": pc.Ref(mi_tabla)}}, # SAM policy
{"Version": "2012-10-17", "Statement": [...]}, # Inline policy
],
# ── Recursos AWS ─────────────────────────────────────────────────────────
resources={ # dict — Recursos que esta feature usa
"users": pc.DynamoDB("users-table", pk="id"),
"files": pc.S3("uploads-bucket"),
"jobs": pc.SQS("jobs-queue", dlq=True),
},
# ── Arquitectura ─────────────────────────────────────────────────────────
architectures=["arm64"], # list — ["x86_64"] o ["arm64"] (Graviton, ~20% más barato)
tracing=True, # bool — Activa AWS X-Ray distributed tracing
# ── Concurrencia ─────────────────────────────────────────────────────────
reserved_concurrency=10, # int >= 0 — Límite máximo de ejecuciones simultáneas.
# 0 = throttle completo (útil para deshabilitar temporalmente)
provisioned_concurrency=3, # int >= 1 — Instancias pre-calentadas (elimina cold starts).
# Implica AutoPublishAlias: live en el template.
# ── Almacenamiento temporal ───────────────────────────────────────────────
ephemeral_storage=1024, # int — Tamaño de /tmp en MB (512–10240, default 512)
# ── Fiabilidad ────────────────────────────────────────────────────────────
dlq=True, # bool — Crea una SQS Dead Letter Queue para
# invocaciones fallidas asíncronas
# ── Observabilidad ────────────────────────────────────────────────────────
log_retention=30, # int — Días de retención en CloudWatch (sobreescribe global)
alarms=True, # Habilita CloudWatch Alarms con umbrales por defecto
# alarms=False, # Deshabilita alarms para esta feature
# alarms={...}, # Config personalizada (ver sección Alarms)
# ── Auth (API Gateway) ────────────────────────────────────────────────────
auth=None, # None = rutas públicas | "api_key" = requiere API key
# (no especificado = hereda auth global de deployless.yaml)
)
Ejemplo completo
# app/features/user/routes.py
from flask import Blueprint
import deployless as pc
users_table = pc.DynamoDB(
"users-table",
pk="tenant_id",
sk="user_id",
gsi=[{"name": "EmailIndex", "pk": "email"}],
ttl_attribute="expires_at",
deletion_policy="Retain",
)
pc.configure(
memory=512,
timeout=30,
description="User Management API",
resources={"users": users_table},
policies=[{"DynamoDBCrudPolicy": {"TableName": pc.Ref(users_table)}}],
architectures=["arm64"],
dlq=True,
alarms=True,
log_retention=30,
)
user_bp = Blueprint("user_bp", __name__, url_prefix="/users")
@user_bp.route("", methods=["GET"])
def list_users():
...
Recursos AWS
Los recursos se declaran dentro de pc.configure(resources={...}) en el routes.py de cada feature. deployless los añade al template.yaml y les asigna variables de entorno automáticamente.
DynamoDB
pc.DynamoDB(
table_name: str, # Nombre de la tabla en AWS
pk: str = "id", # Partition key (clave de partición)
pk_type: str = "S", # "S" (String) | "N" (Number) | "B" (Binary)
sk: str = None, # Sort key opcional. Si se define → AWS::DynamoDB::Table
sk_type: str = "S", # "S" | "N" | "B"
gsi: list = None, # Global Secondary Indexes (ver formato abajo)
billing_mode: str = "PAY_PER_REQUEST",# "PAY_PER_REQUEST" | "PROVISIONED"
read_capacity: int = None, # Solo para billing_mode="PROVISIONED" (default: 5)
write_capacity: int = None, # Solo para billing_mode="PROVISIONED" (default: 5)
ttl_attribute: str = None, # Atributo de Time-To-Live (DynamoDB lo expira automáticamente)
stream: str = None, # "NEW_IMAGE" | "OLD_IMAGE" | "NEW_AND_OLD_IMAGES" | "KEYS_ONLY"
point_in_time_recovery: bool = False, # Habilita PITR (restauración punto en el tiempo)
sse_enabled: bool = True, # Encriptación en reposo con KMS gestionado por AWS
deletion_policy: str = "Delete", # "Delete" | "Retain" | "Snapshot"
existing: bool = False, # True = tabla ya existe, no crear (solo inyecta env var)
)
Lógica de tipo CloudFormation
| Condición | Tipo generado | Notas |
|---|---|---|
Solo pk (sin sk ni gsi) |
AWS::Serverless::SimpleTable |
Más simple, sin SSE configurable |
Con sk o gsi |
AWS::DynamoDB::Table |
Control total, SSE habilitado por defecto |
Variable de entorno generada automáticamente
El sufijo -table / _table se elimina para evitar redundancia:
table_name |
Variable de entorno |
|---|---|
users-table |
USERS_TABLE |
orders_table |
ORDERS_TABLE |
sessions |
SESSIONS_TABLE |
Formato de GSI
Cada elemento de la lista gsi acepta:
{
"name": "StatusIndex", # Requerido — nombre del índice
"pk": "status", # Requerido — partition key del índice
"pk_type": "S", # Opcional, default "S"
"sk": "created_at", # Opcional — sort key del índice
"sk_type": "S", # Opcional, default "S"
"projection": "ALL", # "ALL" | "KEYS_ONLY" | "INCLUDE" (default "ALL")
"non_key_attributes": ["email"], # Requerido solo si projection="INCLUDE"
}
Ejemplos
Tabla simple (solo PK):
pc.DynamoDB("sessions-table", pk="session_id", ttl_attribute="expires_at")
# → AWS::Serverless::SimpleTable
# → Variable: SESSIONS_TABLE
Tabla con SK y múltiples GSI:
pc.DynamoDB(
"orders-table",
pk="tenant_id",
sk="order_id",
gsi=[
{
"name": "StatusIndex",
"pk": "status",
"sk": "created_at",
},
{
"name": "CustomerIndex",
"pk": "customer_id",
"projection": "INCLUDE",
"non_key_attributes": ["total", "status"],
},
],
ttl_attribute="expires_at",
point_in_time_recovery=True,
deletion_policy="Retain",
)
# → AWS::DynamoDB::Table con SSEEnabled=True
# → Variable: ORDERS_TABLE
Tabla con capacidad provisionada:
pc.DynamoDB(
"high-traffic-table",
pk="pk",
sk="sk",
billing_mode="PROVISIONED",
read_capacity=100,
write_capacity=50,
)
Tabla con DynamoDB Streams:
pc.DynamoDB(
"events-table",
pk="event_id",
stream="NEW_AND_OLD_IMAGES", # Dispara Lambda en cada cambio
)
Tabla existente (no crear, solo inyectar env var):
pc.DynamoDB("prod-users-table", existing=True)
# No genera recurso CloudFormation
# Inyecta: PROD_USERS_TABLE = "prod-users-table" (string literal)
S3
pc.S3(
bucket_name: str,
versioning: bool = False,
encryption: bool = True, # SSE-S3 (AES256) habilitado por defecto
cors: list = None, # Lista de reglas CORS (formato CloudFormation CorsRule)
lifecycle_rules: list = None, # Lista de reglas de lifecycle (formato CloudFormation)
public_access_block: bool = True, # Bloquea acceso público por defecto
deletion_policy: str = "Delete",
existing: bool = False,
)
Variable de entorno generada:
uploads-bucket→UPLOADS_BUCKETmy_files_bucket→MY_FILES_BUCKET(el sufijo-bucket/_bucketse elimina)
Validaciones en tiempo de compilación (E00):
bucket_nameno puede estar vacío- Longitud entre 3 y 63 caracteres
- No puede contener underscores (S3 es DNS-compliant)
- Solo minúsculas, dígitos, guiones y puntos — empieza y termina en alfanumérico
Ejemplo básico:
pc.S3("user-uploads")
# → SSE-S3 AES256 habilitado, acceso público bloqueado
# → Variable: UPLOADS_BUCKET
Ejemplo con todas las opciones:
pc.S3(
"user-uploads",
versioning=True,
encryption=True, # AES256 por defecto — pasar False solo si usas KMS externo
public_access_block=True,
deletion_policy="Retain",
cors=[
{
"AllowedOrigins": ["https://mi-app.com"],
"AllowedMethods": ["GET", "PUT"],
"AllowedHeaders": ["*"],
"MaxAge": 3600,
}
],
lifecycle_rules=[
{
"Id": "expire-tmp",
"Status": "Enabled",
"ExpirationInDays": 7,
"Prefix": "tmp/",
}
],
)
SQS
pc.SQS(
queue_name: str,
fifo: bool = False, # True = cola FIFO. Añade .fifo al nombre automáticamente.
dlq: bool = False, # True = crea también una Dead Letter Queue
visibility_timeout: int = 30, # segundos (0–43200)
message_retention: int = 345600, # segundos (60–1209600, por defecto 4 días)
max_receive_count: int = 3, # Intentos antes de mandar a DLQ (1–1000)
encryption: bool = True, # SqsManagedSseEnabled — SSE-SQS habilitado por defecto
deletion_policy: str = "Delete",
existing: bool = False,
)
Nota: SQS y KMS devuelven múltiples recursos de CloudFormation (la cola principal + la DLQ, o la clave + el alias). deployless los inserta todos correctamente en el template.
Variable de entorno generada:
notifications-queue→NOTIFICATIONS_QUEUE_URL
Validaciones en tiempo de compilación (E00):
queue_nameno puede estar vacío ni exceder 80 caracteres- Solo alfanumérico,
-y_(el sufijo.fifose excluye de la validación) visibility_timeoutdebe estar en rango[0, 43200]message_retentiondebe estar en rango[60, 1209600]max_receive_countdebe estar en rango[1, 1000]
Ejemplo básico:
pc.SQS("email-notifications")
# → SSE-SQS habilitado, 4 días retención, visibility 30s
# → Variable: EMAIL_NOTIFICATIONS_QUEUE_URL
Ejemplo con DLQ:
pc.SQS(
"email-notifications",
dlq=True,
visibility_timeout=60,
message_retention=86400, # 1 día
max_receive_count=5,
)
# → Cola principal + DLQ con 14 días de retención
# → Ambas con SSE-SQS habilitado
Ejemplo FIFO:
pc.SQS(
"orders",
fifo=True, # → queue_name se convierte en "orders.fifo" automáticamente
dlq=True, # → DLQ también será FIFO: "orders-dlq.fifo"
)
KMS
pc.KMS(
alias: str = None, # e.g. "alias/mi-app" o simplemente "mi-app"
description: str = None,
key_usage: str = "ENCRYPT_DECRYPT", # "ENCRYPT_DECRYPT" | "SIGN_VERIFY" | "GENERATE_VERIFY_MAC"
key_spec: str = "SYMMETRIC_DEFAULT", # "SYMMETRIC_DEFAULT" | "RSA_2048/3072/4096"
# | "ECC_NIST_P256/P384/P521" | "ECC_SECG_P256K1"
# | "HMAC_224/256/384/512"
enable_rotation: bool = None, # None → auto: True para SYMMETRIC_DEFAULT, False si no
deletion_policy: str = "Retain", # KMS usa Retain por defecto (seguridad)
existing_key_id: str = None, # ID o ARN de una clave existente (no crea recurso)
env_var: str = None, # Fuerza el nombre del env var generado
)
Variable de entorno generada:
env_var="MY_KEY"→MY_KEY(tiene prioridad sobre cualquier derivación automática)alias="myapp/encryption"→MYAPP_ENCRYPTION_KEY_ID- Sin alias ni env_var →
KMS_KEY_ID
Recursos CloudFormation generados:
AWS::KMS::Key— conEnabled: True,KeyUsage,KeySpecy key policy básica (root account)AWS::KMS::Alias— alias opcional para identificar la clave por nombreEnableKeyRotationsolo se añade cuandokey_spec="SYMMETRIC_DEFAULT"(claves asimétricas no soportan rotación automática)
Validaciones en tiempo de compilación (E00):
aliassolo puede contener alfanumérico,-,_,/key_usagedebe ser uno de los valores válidoskey_specdebe ser uno de los valores válidosenable_rotation=Trueno es válido para claves asimétricas (RSA, ECC, HMAC)key_specECC no es compatible conkey_usage="ENCRYPT_DECRYPT"key_spec="SYMMETRIC_DEFAULT"no es compatible conkey_usage="SIGN_VERIFY"
Nota: La Lambda NO tiene permisos para usar la clave por defecto. Debes añadir la policy IAM explícitamente con pc.configure(policies=[...]).
Ejemplo con permisos IAM
kms_key = pc.KMS(
alias="mi-app/datos",
description="Clave de cifrado para datos sensibles",
enable_rotation=True,
deletion_policy="Retain",
)
pc.configure(
resources={"datos_key": kms_key},
policies=[
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["kms:Encrypt", "kms:Decrypt", "kms:GenerateDataKey"],
"Resource": pc.Ref(kms_key),
}
],
}
],
)
Cómo usar la clave en el código de la app
La variable de entorno KMS_KEY_ID (o {ALIAS}_KEY_ID si usas alias) se inyecta automáticamente en la Lambda. Úsala en tus servicios de cifrado:
# app/features/tenant/services/kms_service.py
import boto3
import base64
import os
from botocore.exceptions import ClientError
kms_client = boto3.client('kms')
def encrypt_with_kms(plaintext: str) -> str:
"""Cifra un string y devuelve el ciphertext en base64."""
response = kms_client.encrypt(
KeyId=os.getenv('KMS_KEY_ID'),
Plaintext=plaintext.encode('utf-8'),
)
return base64.b64encode(response['CiphertextBlob']).decode('utf-8')
def decrypt_with_kms(ciphertext_b64: str) -> str:
"""Descifra un ciphertext en base64 y devuelve el plaintext."""
ciphertext_blob = base64.b64decode(ciphertext_b64)
response = kms_client.decrypt(CiphertextBlob=ciphertext_blob)
return response['Plaintext'].decode('utf-8')
kms:Decryptno necesita especificarKeyIdporque el ciphertext ya lleva embebido el ID de la clave que lo cifró.
Ejemplo completo — cifrado de claves RSA por tenant
Patrón real usado en la app de referencia: el tenant feature cifra la clave privada RSA al crear el tenant y el auth feature la descifra en cada login.
# app/features/tenant/routes.py
import deployless as pc
tenant_key = pc.KMS(
alias="ums/tenant-keys",
description="Cifrado de claves privadas RSA por tenant",
enable_rotation=True,
deletion_policy="Retain",
)
tenants_table = pc.DynamoDB("ums-tenants", pk="tenant_id", deletion_policy="Retain")
pc.configure(
resources={
"tenants": tenants_table,
"tenant_key": tenant_key,
},
policies=[
{"DynamoDBCrudPolicy": {"TableName": pc.Ref(tenants_table)}},
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["kms:Encrypt"], # tenant solo cifra
"Resource": pc.Ref(tenant_key),
}
],
},
],
)
# app/features/auth/routes.py
import deployless as pc
# Reutiliza la misma clave existente (no la vuelve a crear)
tenant_key = pc.KMS(existing_key_id=os.getenv("UMS_TENANT_KEYS_KEY_ID"))
pc.configure(
resources={"tenant_key": tenant_key},
policies=[
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["kms:Decrypt"], # auth solo descifra
"Resource": os.getenv("UMS_TENANT_KEYS_KEY_ID"),
}
],
}
],
)
Variables de entorno auto-inyectadas:
| Alias | Variable |
|---|---|
ums/tenant-keys |
UMS_TENANT_KEYS_KEY_ID |
mi-app |
MI_APP_KEY_ID |
| Sin alias | KMS_KEY_ID |
Clave asimétrica para firma digital (RSA)
signing_key = pc.KMS(
alias="mi-app/signing",
description="Clave RSA para firmar JWT o documentos",
key_usage="SIGN_VERIFY",
key_spec="RSA_2048",
# enable_rotation no aplica — se ignora automáticamente para claves asimétricas
)
pc.configure(
resources={"signing_key": signing_key},
policies=[
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["kms:Sign", "kms:Verify", "kms:GetPublicKey"],
"Resource": pc.Ref(signing_key),
}
],
}
],
)
Clave existente (no crear, solo inyectar env var)
pc.KMS(existing_key_id="arn:aws:kms:us-east-1:123456789:key/abc-123")
# No genera recurso CloudFormation
# KMS_KEY_ID = "arn:aws:kms:us-east-1:123456789:key/abc-123"
SSM Parameter Store
deployless ofrece dos herramientas para SSM: pc.SSMParameter para crear un parámetro como recurso CloudFormation, y pc.SSMParam para referenciar un parámetro existente como dynamic reference en env vars.
pc.SSMParameter — crear un parámetro
pc.SSMParameter(
name: str, # Path del parámetro, debe comenzar con "/"
value: str, # Valor del parámetro
type: str = "String", # "String" | "StringList" | "SecureString"
description: str = None,
existing: bool = False, # True = no crear, solo inyectar env var
)
Variable de entorno generada — último segmento del path:
/myapp/db/host→HOST/myapp/api/secret-key→SECRET_KEY
Validaciones en tiempo de compilación (E00):
namedebe comenzar con/- Solo alfanumérico,
.,-,_,/ typedebe serString,StringListoSecureStringvalueno puede estar vacío (salvoSecureString)
Ejemplo:
db_host = pc.SSMParameter(
"/myapp/db/host",
value="db.example.com",
description="RDS endpoint",
)
pc.configure(
resources={"db_host": db_host},
policies=["SSMParameterReadPolicy": {"ParameterName": "/myapp/db/host"}],
)
# → Variable: HOST = {"Ref": "MyappDbHostParameter"}
pc.SSMParam — referenciar un parámetro existente
No genera recurso CloudFormation. Produce una dynamic reference de CloudFormation directamente en el valor de la env var.
pc.SSMParam(
name: str, # Path del parámetro existente
secure: bool = False, # True → "{{resolve:ssm-secure:/path}}" (SecureString)
version: int = None, # Opcional — anclar a una versión específica
)
Uso en env vars:
pc.configure(
env={
"DB_HOST": pc.SSMParam("/prod/db/host"),
"API_KEY": pc.SSMParam("/prod/api/key", secure=True),
"DB_PASS": pc.SSMParam("/prod/db/password", secure=True, version=3),
}
)
Esto genera en el template:
Environment:
Variables:
DB_HOST: "{{resolve:ssm:/prod/db/host}}"
API_KEY: "{{resolve:ssm-secure:/prod/api/key}}"
DB_PASS: "{{resolve:ssm-secure:/prod/db/password:3}}"
{{resolve:ssm-secure:...}}solo funciona con parámetros de tipoSecureStringy requiere que la Lambda tenga permisossm:GetParameter+kms:Decryptsobre la clave KMS del parámetro.
CloudWatch Alarms
deployless puede generar automáticamente 3 alarmas por Lambda: errores, throttles y duración.
Activación
# En routes.py — habilita alarms con umbrales por defecto
pc.configure(alarms=True)
# Con umbrales personalizados
pc.configure(alarms={
"errors": {
"threshold": 1, # Disparar cuando Errors >= 1 en el periodo
"period": 300, # Periodo de evaluación en segundos
},
"throttles": {
"threshold": 1,
"period": 300,
},
"duration": {
"threshold_pct": 80, # Disparar cuando Duration > 80% del timeout configurado
"period": 300, # (si timeout=30s → alarma a los 24000ms)
},
"sns_topic_arn": "arn:aws:sns:us-east-1:123456789:my-alerts", # Opcional
})
# Deshabilitar alarms para esta feature aunque estén activos globalmente
pc.configure(alarms=False)
Alarms globales (para todas las features)
En deployless.yaml, puedes activar alarms para todo el proyecto:
alarms:
errors:
threshold: 1
period: 300
throttles:
threshold: 1
period: 300
duration:
threshold_pct: 80
period: 300
sns_topic_arn: "arn:aws:sns:us-east-1:123456789:my-alerts"
Recursos generados
Para cada feature con alarms activo, deployless genera en el template:
UserFunctionErrorsAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
MetricName: Errors
Namespace: AWS/Lambda
Statistic: Sum
Period: 300
Threshold: 1
ComparisonOperator: GreaterThanOrEqualToThreshold
TreatMissingData: notBreaching
UserFunctionThrottlesAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
MetricName: Throttles
# ...
UserFunctionDurationAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
MetricName: Duration
Statistic: Maximum
Threshold: 24000 # 80% de 30s = 24000ms
# ...
pc.Ref() y pc.GetAtt() — Referenciar recursos
Usa pc.Ref(resource) para obtener el ID lógico de un recurso (genera {"Ref": "LogicalId"}), y pc.GetAtt(resource, attr) para obtener un atributo específico (genera {"Fn::GetAtt": ["LogicalId", "Attr"]}).
tabla = pc.DynamoDB("users-table")
bucket = pc.S3("uploads")
pc.configure(
resources={"users": tabla, "uploads": bucket},
policies=[
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["dynamodb:GetItem", "dynamodb:PutItem"],
"Resource": pc.GetAtt(tabla, "Arn"),
},
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": pc.GetAtt(bucket, "Arn"),
},
]
}
],
)
pc.Ref() y pc.GetAtt() aceptan tanto un objeto recurso como un string con el logical ID de CloudFormation.
@pc.cron() — Lambdas programadas
Decora cualquier función con @pc.cron() para que deployless la despliegue como una Lambda separada disparada por EventBridge (CloudWatch Events) en el schedule indicado.
@pc.cron(
schedule: str, # Expresión de schedule (requerido)
memory: int = None, # MB. Si None, usa globals.memory
timeout: int = None, # Segundos. Si None, usa globals.timeout
env: dict = None, # Variables de entorno adicionales
description: str = None,
)
Formatos de schedule:
"rate(5 minutes)"— cada 5 minutos"rate(1 hour)"— cada hora"rate(24 hours)"— diario"cron(0 9 * * ? *)"— todos los días a las 9:00 UTC
La función debe tener la firma (event, context) de Lambda.
Ejemplo:
# app/features/user/routes.py
import deployless as pc
@pc.cron(
schedule="rate(24 hours)",
memory=128,
timeout=300,
description="Limpieza diaria de usuarios expirados",
)
def cleanup_expired_users(event, context):
# Tu lógica aquí
deleted = delete_expired_users()
return {"status": "ok", "deleted": deleted}
Esto genera en template.yaml:
CleanupExpiredUsersFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: .dist/CleanupExpiredUsersFunction/
Handler: bootstrap.handler
MemorySize: 128
Timeout: 300
Description: Limpieza diaria de usuarios expirados
Events:
Schedule:
Type: Schedule
Properties:
Schedule: rate(24 hours)
@pc.route() — Split Lambdas por ruta
Por defecto, todas las rutas de una feature comparten una sola Lambda. Con @pc.route() puedes aislar un endpoint específico en su propia Lambda (útil para endpoints que consumen muchos recursos o tienen timeouts distintos).
@pc.route(
memory: int = None,
timeout: int = None,
description: str = None,
auth = <no especificado>, # None = público | "api_key" = requiere API key
# (no especificado = hereda auth de la feature o global)
)
El decorador @pc.route() debe ir por encima del decorador de Flask.
# app/features/user/routes.py
import deployless as pc
from flask import Blueprint
user_bp = Blueprint("user_bp", __name__, url_prefix="/users")
@pc.route(memory=1024, timeout=120, description="Exportación pesada de datos")
@user_bp.route("/export", methods=["POST"])
def export_users():
# Este endpoint tendrá su propia Lambda con 1 GB y 2 minutos de timeout
...
@user_bp.route("", methods=["GET"])
def list_users():
# Este endpoint va en la Lambda compartida de la feature
...
Esto genera dos funciones Lambda separadas:
UserFunction— contieneGET /users(y todos los demás endpoints sin@pc.route())ExportUsersFunction— contiene soloPOST /users/export
@pc.lambda_function() — Lambdas standalone
Para funciones Lambda que no tienen rutas HTTP ni schedule — por ejemplo, consumers de SQS, handlers de eventos S3, o pasos de Step Functions — usa @pc.lambda_function().
@pc.lambda_function(
memory: int = None, # MB. Si None, usa globals.memory
timeout: int = None, # Segundos. Si None, usa globals.timeout
env: dict = None, # Variables de entorno adicionales
description: str = None,
)
La función debe tener la firma (event, context) de Lambda.
Ejemplo:
# app/features/orders/routes.py
import deployless as pc
@pc.lambda_function(memory=512, timeout=60, description="Procesa mensajes de la cola de pedidos")
def process_order_queue(event, context):
for record in event.get("Records", []):
body = record["body"]
print(f"Procesando pedido: {body}")
return {"processed": len(event.get("Records", []))}
Esto genera en template.yaml:
ProcessOrderQueueFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: .dist/ProcessOrderQueueFunction/
Handler: bootstrap.handler
MemorySize: 512
Timeout: 60
Description: Procesa mensajes de la cola de pedidos
Nota: A diferencia de las features HTTP, las lambdas standalone no tienen eventos de API Gateway. Puedes conectarlas a SQS, S3, DynamoDB Streams, etc. manualmente en el template o mediante event source mappings.
pc.shared_resource() — Recursos globales
Si un recurso (tabla DynamoDB, bucket S3, cola SQS, etc.) debe estar disponible para todas las features, usa pc.shared_resource() en lugar de declararlo dentro del resources de una feature individual.
pc.shared_resource(key: str, resource)
- El recurso se incluye una sola vez en el template de CloudFormation.
- Las variables de entorno del recurso se inyectan en todas las Lambdas del proyecto.
- Se puede referenciar con
pc.Ref()ypc.GetAtt()desde cualquier feature.
Ejemplo:
# app/features/events/routes.py (o cualquier routes.py)
import deployless as pc
# Tabla compartida por todas las features
pc.shared_resource("audit_log", pc.DynamoDB("audit-log", pk="event_id", sk="timestamp"))
# Bucket compartido
pc.shared_resource("shared_assets", pc.S3("app-shared-assets"))
Desde cualquier feature puedes usar las variables de entorno generadas:
import os
audit_table = os.getenv("AUDIT_LOG_TABLE") # Inyectado en TODAS las Lambdas
assets_bucket = os.getenv("APP_SHARED_ASSETS_BUCKET")
Fichero .env y secrets
deployless puede leer un fichero .env para inyectar variables de entorno y gestionar secrets automáticamente.
Configuración en deployless.yaml
env_file: .env.production # Ruta al fichero .env
# Opcional — clave KMS para cifrar los secrets en SSM
secrets_kms: mi-app/secrets # Alias, key ID, o ARN
Formato del fichero .env
# Variables normales — se inyectan directamente como env vars en todas las Lambdas
APP_ENV=production
LOG_FORMAT=json
# Secrets — prefijo SECRET_ indica que se pushean a SSM Parameter Store
SECRET_DB_PASSWORD=mysecretpassword
SECRET_API_KEY=sk_live_xxxx
Comportamiento
| Tipo | Ejemplo | Destino | Valor en Lambda |
|---|---|---|---|
| Normal | APP_ENV=production |
Env var directa | production |
| Secret | SECRET_DB_PASSWORD=xxx |
SSM Parameter Store | {{resolve:ssm:/mi-app/SECRET_DB_PASSWORD}} |
Para las variables SECRET_:
- El nombre se mantiene completo con el prefijo:
SECRET_DB_PASSWORD→/mi-app/SECRET_DB_PASSWORD - El valor se almacena como
Stringen SSM Parameter Store bajo el path/{app_name}/{VAR_NAME} - La Lambda recibe una dynamic reference
{{resolve:ssm:...}}que CloudFormation resuelve al crear/actualizar el stack - El env var en la Lambda también mantiene el nombre completo:
SECRET_DB_PASSWORD
Nota: Se usa
String(noSecureString) porque CloudFormation no soporta{{resolve:ssm-secure:...}}en Lambda environment variables. El valor sigue protegido por IAM — solo los roles con permisossm:GetParameterpueden leerlo.
Validaciones
| Código | Regla |
|---|---|
| E27 | El fichero env_file especificado no existe |
| E28 | Variable SECRET_ con valor vacío |
| E29 | Formato inválido de secrets_kms (alias solo puede contener alfanumérico, -, _, /) |
Comandos CLI
deployless build
Genera el template.yaml y construye los paquetes .dist/.
deployless build
# Opciones:
deployless build --stage prod # Sobreescribe el stage
deployless build -o infra/template.yaml # Ruta de salida del template
deployless build --dry-run # Valida sin escribir ficheros
deployless build --verbose # Output detallado
deployless validate
Valida el proyecto sin generar ningún fichero. Equivalente a build --dry-run pero con output más limpio.
deployless validate
deployless validate --stage prod
deployless validate --check-existing # Verifica que los recursos con existing=True existen en AWS
deployless validate --verbose
deployless deploy
Encadena deployless build + sam build + sam deploy. Requiere tener el AWS SAM CLI instalado.
deployless deploy
deployless deploy --stage prod
deployless deploy --guided # Lanza el wizard de sam deploy (primera vez)
deployless clean
Elimina los ficheros generados (.dist/ y template.yaml).
deployless clean
deployless clean -o infra/template.yaml # Si usaste una ruta de salida distinta
deployless info
Muestra el resumen del proyecto detectado.
deployless info
Salida de ejemplo:
Project : mi-ums-api
Provider : aws
Stage : dev
Runtime : python3.13
Features (3):
- auth (app/features/auth/routes.py)
- tenant (app/features/tenant/routes.py)
- user (app/features/user/routes.py)
deployless secrets push
Pushea las variables SECRET_* del fichero .env a AWS SSM Parameter Store.
deployless secrets push
deployless secrets push --stage prod
deployless secrets push --env-file .env.prod # Sobreescribe la ruta del env_file de deployless.yaml
deployless secrets push --verbose
Proceso:
- Lee el fichero
.env(dedeployless.yamlo--env-file) - Filtra las variables con prefijo
SECRET_ - Crea/actualiza parámetros SSM:
/{app_name}/{VAR_NAME}(tipoString)
Nota:
deployless buildtambién ejecuta este paso automáticamente cuandoenv_fileestá configurado endeployless.yaml.
Ejemplo:
# .env.prod
SECRET_DB_PASSWORD=mysecretpassword
SECRET_API_KEY=sk_live_xxx
deployless secrets push --env-file .env.prod
# Crea en SSM:
# /mi-app/SECRET_DB_PASSWORD (String)
# /mi-app/SECRET_API_KEY (String)
deployless secrets sync
Push + elimina parámetros huérfanos en SSM. Útil para mantener SSM en sync cuando se eliminan secrets del .env.
deployless secrets sync
deployless secrets sync --stage prod
deployless secrets sync --env-file .env.prod
deployless secrets sync --yes # Auto-confirma eliminación de huérfanos
deployless secrets sync --verbose
Comportamiento:
- Pushea todas las variables
SECRET_*(igual quesecrets push) - Lista los parámetros existentes bajo
/{app_name}/en SSM - Detecta parámetros que ya no están en el
.env - Pide confirmación antes de eliminarlos (salvo con
--yes)
Estructura de proyecto
deployless espera la siguiente estructura de directorios (configurable en deployless.yaml):
mi-proyecto/
├── deployless.yaml # Configuración de deployless
├── requirements.txt # Dependencias globales del proyecto
├── app/
│ ├── features/ # Una carpeta por feature
│ │ ├── auth/
│ │ │ ├── routes.py # REQUERIDO — Blueprint Flask + pc.configure()
│ │ │ ├── use_cases/
│ │ │ ├── repositories/
│ │ │ └── schemas/
│ │ ├── user/
│ │ │ ├── routes.py
│ │ │ ├── requirements.txt # OPCIONAL — dependencias extra para esta feature
│ │ │ └── ...
│ │ └── tenant/
│ │ └── routes.py
│ └── shared/ # Código compartido — se copia en TODAS las Lambdas
│ ├── decorators/
│ ├── errors/
│ └── config.py
└── .dist/ # Generado por deployless build (no subir a git)
├── AuthFunction/
│ ├── app/
│ │ ├── __init__.py
│ │ ├── features/
│ │ │ ├── __init__.py
│ │ │ └── auth/ # Solo el código de este feature
│ │ │ ├── routes.py
│ │ │ ├── use_cases/
│ │ │ └── ...
│ │ └── shared/ # Copia de app/shared/
│ ├── bootstrap.py # Generado automáticamente
│ ├── deployless.py # Stub runtime (no-ops)
│ └── requirements.txt # requirements.txt global + feature + aws-wsgi
├── UserFunction/
└── TenantFunction/
Reglas de descubrimiento
- deployless escanea
app/features/buscando subdirectorios que contengan un ficheroroutes.py. - Los directorios que empiezan por
_(e.g.__pycache__) se ignoran. - Se procesan en orden alfabético.
- Cada
routes.pydebe definir al menos un Blueprint Flask con al menos una ruta.
El bootstrap generado
Para cada Lambda se genera un bootstrap.py que:
- Registra todos los Blueprints Flask encontrados en
routes.py. - Crea una app Flask temporal.
- Envuelve la app con
aws_wsgi.response()para convertir eventos de API Gateway en requests WSGI.
# .dist/UserFunction/bootstrap.py — generado automáticamente, no editar
import sys, os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from flask import Flask
import app.features.user.routes as _routes_module # namespace app/ completo
import inspect
flask_app = Flask(__name__)
for _name, _obj in inspect.getmembers(_routes_module):
_klass = type(_obj)
if _klass.__name__ == "Blueprint" and "flask" in _klass.__module__:
flask_app.register_blueprint(_obj)
import awsgi
def handler(event, context):
return awsgi.response(flask_app, event, context, base64_content_types={"image/png", "image/jpeg"})
Cada Lambda incluye además un deployless.py con implementaciones no-op de todas las funciones de deployless (configure, KMS, DynamoDB, etc.), para que los import deployless as pc en routes.py no fallen en runtime sin necesidad de instalar el paquete completo.
Ejemplo completo
Este ejemplo usa la app real de este repositorio (app/features/auth, user, tenant).
1. deployless.yaml
name: ums-api
provider: aws
stage: dev
paths:
features: app/features
shared: app/shared
globals:
runtime: python3.13
memory: 256
timeout: 30
log_retention: 14
api:
endpoint_type: REGIONAL
cors:
allow_origin: "*"
allow_methods: [GET, POST, PUT, DELETE, OPTIONS]
allow_headers: [Content-Type, Authorization, X-Api-Key]
env:
LOG_LEVEL: INFO
2. app/features/user/routes.py
from flask import Blueprint, request, g, jsonify
import deployless as pc
from app.features.user.schemas import CreateUserRequest, UpdateUserRequest
from app.features.user.use_cases import create_user, list_users, get_user, update_user, delete_user
from app.shared.decorators import require_auth, require_scopes
# ---- Configuración Lambda para la feature "user" ----
pc.configure(
memory=512,
timeout=30,
description="User Management Service",
resources={
"users": pc.DynamoDB(
"ums-users",
pk="tenant_id",
pk_type="S",
sk="user_id",
sk_type="S",
gsi=[
{
"name": "EmailIndex",
"pk": "email",
"pk_type": "S",
}
],
ttl_attribute="expires_at",
deletion_policy="Retain",
),
"sessions": pc.DynamoDB(
"ums-sessions",
pk="session_id",
ttl_attribute="expires_at",
),
},
env={
"TOKEN_EXPIRY": "3600",
},
)
# ---- Cron: limpieza diaria de sesiones expiradas ----
@pc.cron(
schedule="rate(24 hours)",
memory=128,
timeout=60,
description="Limpieza de sesiones expiradas",
)
def cleanup_sessions(event, context):
# Lógica de limpieza
return {"status": "ok"}
# ---- Blueprint Flask ----
user_bp = Blueprint("user_bp", __name__, url_prefix="/users")
@user_bp.route("", methods=["POST"])
@require_auth
@require_scopes(["ums:users:create"])
def create_user_route():
data = request.get_json()
req = CreateUserRequest(
email=data.get("email"),
password=data.get("password"),
scopes=data.get("scopes", []),
)
response = create_user(req, g.user["tenant_id"])
return jsonify(response.to_dict()), 201
@user_bp.route("", methods=["GET"])
@require_auth
@require_scopes(["ums:users:read"])
def list_users_route():
response = list_users(g.user["tenant_id"])
return jsonify(response.to_dict()), 200
@user_bp.route("/<user_id>", methods=["GET"])
@require_auth
@require_scopes(["ums:users:read"])
def get_user_route(user_id):
response = get_user(g.user["tenant_id"], user_id)
return jsonify(response.to_dict()), 200
@user_bp.route("/<user_id>", methods=["PUT"])
@require_auth
@require_scopes(["ums:users:update"])
def update_user_route(user_id):
data = request.get_json()
req = UpdateUserRequest(
email=data.get("email"),
password=data.get("password"),
scopes=data.get("scopes"),
)
response = update_user(g.user["tenant_id"], user_id, req)
return jsonify(response.to_dict()), 200
@user_bp.route("/<user_id>", methods=["DELETE"])
@require_auth
@require_scopes(["ums:users:delete"])
def delete_user_route(user_id):
delete_user(g.user["tenant_id"], user_id)
return "", 204
# ---- Split Lambda: exportación pesada ----
@pc.route(memory=1024, timeout=120, description="Exportación masiva de usuarios")
@user_bp.route("/export", methods=["POST"])
@require_auth
@require_scopes(["ums:users:export"])
def export_users_route():
# Este endpoint tendrá su propia Lambda
...
return jsonify({"url": "https://..."}), 200
3. Ejecutar el build
deployless build --verbose
Salida esperada:
[deployless] Project: ums-api | Stage: dev | Provider: aws
[deployless] Features found: ['auth', 'tenant', 'user']
[deployless] auth: 3 routes, 0 split
[deployless] tenant: 2 routes, 0 split
[deployless] user: 5 routes, 1 split
[deployless] Crons: ['cleanup_sessions']
[deployless] Validation passed.
[deployless] Built: .dist/AuthFunction
[deployless] Built: .dist/TenantFunction
[deployless] Built: .dist/UserFunction
[deployless] Built split route: .dist/ExportUsersRouteFunction
[deployless] Built cron: .dist/CleanupSessionsFunction
[deployless] Template generated: /path/to/project/template.yaml
4. Desplegar
# Primera vez (wizard interactivo de SAM)
deployless deploy --guided --stage prod
# Despliegues posteriores
deployless deploy --stage prod
5. template.yaml generado (resumen)
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: ums-api — Generated by deployless
Globals:
Function:
Runtime: python3.13
MemorySize: 256
Timeout: 30
Environment:
Variables:
LOG_LEVEL: INFO
APP_STAGE: dev
Resources:
Api:
Type: AWS::Serverless::Api
Properties:
StageName: dev
EndpointConfiguration: REGIONAL
Cors:
AllowOrigin: "'*'"
AllowMethods: "'GET,POST,PUT,DELETE,OPTIONS'"
AllowHeaders: "'Content-Type,Authorization,X-Api-Key'"
UserFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: .dist/UserFunction/
Handler: bootstrap.handler
MemorySize: 512
Timeout: 30
Description: User Management Service
Environment:
Variables:
UMS_USERS_TABLE:
Ref: UmsUsersTable
UMS_SESSIONS_TABLE:
Ref: UmsSessionsTable
TOKEN_EXPIRY: '3600'
Events:
UserPostGet:
Type: Api
Properties:
RestApiId:
Ref: Api
Path: /users
Method: get
# ... más eventos
UmsUsersTable:
Type: AWS::DynamoDB::Table
DeletionPolicy: Retain
Properties:
TableName: ums-users
BillingMode: PAY_PER_REQUEST
# ... atributos, GSI, TTL
CleanupSessionsFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: .dist/CleanupSessionsFunction/
Handler: bootstrap.handler
MemorySize: 128
Timeout: 60
Events:
Schedule:
Type: Schedule
Properties:
Schedule: rate(24 hours)
Outputs:
ApiUrl:
Description: API Gateway endpoint URL
Value:
Fn::Sub: https://${Api}.execute-api.${AWS::Region}.amazonaws.com/dev
UserFunctionArn:
Value:
Fn::GetAtt: [UserFunction, Arn]
# ...
Notas y limitaciones conocidas
- Solo Flask está soportado por ahora. El soporte para FastAPI está previsto (adaptador en
deployless/adapters/fastapi.py). - El código de la feature se copia flat: solo los ficheros
.pydel directorio raíz de la feature se incluyen. Los subdirectorios (use_cases, repositories, etc.) no se copian. Si turoutes.pyimporta de subdirectorios propios, deberás adaptar la estructura o ampliar el packager. app/shared/se copia completo en cada Lambda bajo el nombreshared/. Las importaciones del tipofrom app.shared.x import ydeberán cambiarse afrom shared.x import yen el código de producción Lambda.- No se instalan las dependencias durante
deployless build.sam build(ejecutado pordeployless deploy) es quien instala elrequirements.txtde cada paquete. - Los recursos SQS y KMS devuelven múltiples entradas de CloudFormation (cola + DLQ, clave + alias). deployless los inserta correctamente todos en el template.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file deployless-0.1.0.tar.gz.
File metadata
- Download URL: deployless-0.1.0.tar.gz
- Upload date:
- Size: 79.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
684187265b65ea24cb748959a42798fdbd324f250e44b4db23123854873e16d9
|
|
| MD5 |
1fb1a74d89b0e4b145439a04a63d4693
|
|
| BLAKE2b-256 |
56af192433c3ae20367f260f1724f0a4d7320e76c66b46c9d12a3917b1ee08cd
|
File details
Details for the file deployless-0.1.0-py3-none-any.whl.
File metadata
- Download URL: deployless-0.1.0-py3-none-any.whl
- Upload date:
- Size: 53.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bbf157128731b975b4ff98234f18a3043b74d69e775effb0f963108f6cf435f5
|
|
| MD5 |
83a18dd2232291397b3e64873fd47421
|
|
| BLAKE2b-256 |
b50656eea80c2a8f06140000e9f807528fefad2b960379b6f5e948cc12e44bf4
|