Skip to main content

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

  1. Qué es deployless
  2. Instalación
  3. Referencia de deployless.yaml
  4. pc.configure() en routes.py
  5. Recursos AWS
  6. @pc.cron() — Lambdas programadas
  7. @pc.route() — Split Lambdas por ruta
  8. @pc.lambda_function() — Lambdas standalone
  9. pc.shared_resource() — Recursos globales
  10. Fichero .env y secrets
  11. Comandos CLI
  12. Estructura de proyecto
  13. Ejemplo completo

Qué es deployless

deployless toma tu proyecto Flask estructurado por features y genera:

  • Un template.yaml de 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 un bootstrap.py generado automáticamente y un requirements.txt fusionado.
  • 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-bucketUPLOADS_BUCKET
  • my_files_bucketMY_FILES_BUCKET (el sufijo -bucket / _bucket se elimina)

Validaciones en tiempo de compilación (E00):

  • bucket_name no 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-queueNOTIFICATIONS_QUEUE_URL

Validaciones en tiempo de compilación (E00):

  • queue_name no puede estar vacío ni exceder 80 caracteres
  • Solo alfanumérico, - y _ (el sufijo .fifo se excluye de la validación)
  • visibility_timeout debe estar en rango [0, 43200]
  • message_retention debe estar en rango [60, 1209600]
  • max_receive_count debe 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 — con Enabled: True, KeyUsage, KeySpec y key policy básica (root account)
  • AWS::KMS::Alias — alias opcional para identificar la clave por nombre
  • EnableKeyRotation solo se añade cuando key_spec="SYMMETRIC_DEFAULT" (claves asimétricas no soportan rotación automática)

Validaciones en tiempo de compilación (E00):

  • alias solo puede contener alfanumérico, -, _, /
  • key_usage debe ser uno de los valores válidos
  • key_spec debe ser uno de los valores válidos
  • enable_rotation=True no es válido para claves asimétricas (RSA, ECC, HMAC)
  • key_spec ECC no es compatible con key_usage="ENCRYPT_DECRYPT"
  • key_spec="SYMMETRIC_DEFAULT" no es compatible con key_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:Decrypt no necesita especificar KeyId porque 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/hostHOST
  • /myapp/api/secret-keySECRET_KEY

Validaciones en tiempo de compilación (E00):

  • name debe comenzar con /
  • Solo alfanumérico, ., -, _, /
  • type debe ser String, StringList o SecureString
  • value no puede estar vacío (salvo SecureString)

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 tipo SecureString y requiere que la Lambda tenga permiso ssm:GetParameter + kms:Decrypt sobre 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 — contiene GET /users (y todos los demás endpoints sin @pc.route())
  • ExportUsersFunction — contiene solo POST /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() y pc.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_:

  1. El nombre se mantiene completo con el prefijo: SECRET_DB_PASSWORD/mi-app/SECRET_DB_PASSWORD
  2. El valor se almacena como String en SSM Parameter Store bajo el path /{app_name}/{VAR_NAME}
  3. La Lambda recibe una dynamic reference {{resolve:ssm:...}} que CloudFormation resuelve al crear/actualizar el stack
  4. El env var en la Lambda también mantiene el nombre completo: SECRET_DB_PASSWORD

Nota: Se usa String (no SecureString) porque CloudFormation no soporta {{resolve:ssm-secure:...}} en Lambda environment variables. El valor sigue protegido por IAM — solo los roles con permiso ssm:GetParameter pueden 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:

  1. Lee el fichero .env (de deployless.yaml o --env-file)
  2. Filtra las variables con prefijo SECRET_
  3. Crea/actualiza parámetros SSM: /{app_name}/{VAR_NAME} (tipo String)

Nota: deployless build también ejecuta este paso automáticamente cuando env_file está configurado en deployless.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:

  1. Pushea todas las variables SECRET_* (igual que secrets push)
  2. Lista los parámetros existentes bajo /{app_name}/ en SSM
  3. Detecta parámetros que ya no están en el .env
  4. 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 fichero routes.py.
  • Los directorios que empiezan por _ (e.g. __pycache__) se ignoran.
  • Se procesan en orden alfabético.
  • Cada routes.py debe definir al menos un Blueprint Flask con al menos una ruta.

El bootstrap generado

Para cada Lambda se genera un bootstrap.py que:

  1. Registra todos los Blueprints Flask encontrados en routes.py.
  2. Crea una app Flask temporal.
  3. 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 .py del directorio raíz de la feature se incluyen. Los subdirectorios (use_cases, repositories, etc.) no se copian. Si tu routes.py importa de subdirectorios propios, deberás adaptar la estructura o ampliar el packager.
  • app/shared/ se copia completo en cada Lambda bajo el nombre shared/. Las importaciones del tipo from app.shared.x import y deberán cambiarse a from shared.x import y en el código de producción Lambda.
  • No se instalan las dependencias durante deployless build. sam build (ejecutado por deployless deploy) es quien instala el requirements.txt de 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

deployless-0.1.0.tar.gz (79.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

deployless-0.1.0-py3-none-any.whl (53.7 kB view details)

Uploaded Python 3

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

Hashes for deployless-0.1.0.tar.gz
Algorithm Hash digest
SHA256 684187265b65ea24cb748959a42798fdbd324f250e44b4db23123854873e16d9
MD5 1fb1a74d89b0e4b145439a04a63d4693
BLAKE2b-256 56af192433c3ae20367f260f1724f0a4d7320e76c66b46c9d12a3917b1ee08cd

See more details on using hashes here.

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

Hashes for deployless-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 bbf157128731b975b4ff98234f18a3043b74d69e775effb0f963108f6cf435f5
MD5 83a18dd2232291397b3e64873fd47421
BLAKE2b-256 b50656eea80c2a8f06140000e9f807528fefad2b960379b6f5e948cc12e44bf4

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page