Skip to main content

Compile Flask/FastAPI apps to AWS SAM serverless

Project description

deployless

deployless is a compiler that converts Flask applications (and in the future FastAPI) into AWS SAM templates ready to deploy as serverless Lambda functions. It does not require rewriting your app: simply add configuration annotations to your routes.py files and run deployless build.


Table of Contents

  1. What is deployless
  2. Installation
  3. deployless.yaml reference
  4. pc.configure() in routes.py
  5. AWS Resources
  6. @pc.cron() — Scheduled Lambdas
  7. @pc.route() — Split Lambdas per route
  8. @pc.lambda_function() — Standalone Lambdas
  9. pc.shared_resource() — Global resources
  10. .env file and secrets
  11. CLI commands (build, check, validate, deploy, clean, info, secrets)
  12. Project structure
  13. Full example

What is deployless

deployless takes your Flask project structured by features and generates:

  • A template.yaml for AWS SAM with one Lambda function per feature (and optionally one per specific route).
  • A .dist/ folder with the packaged code for each Lambda, including an auto-generated bootstrap.py and a merged requirements.txt.
  • CloudWatch Log Groups with configurable retention for each function.

Mental model

app/features/users/routes.py   →   UsersFunction (Lambda)
app/features/auth/routes.py    →   AuthFunction  (Lambda)
app/features/tenant/routes.py  →   TenantFunction (Lambda)

Each feature lives in its own Lambda. If a specific endpoint needs a different configuration (more memory, longer timeout), you can "split" it into its own Lambda with @pc.route().

Compilation flow

deployless build
  │
  ├── 1. Reads deployless.yaml
  ├── 2. Discovers app/features/*/routes.py
  ├── 3. Imports each routes.py (extracts Blueprints and routes)
  ├── 4. Reads metadata from pc.configure(), @pc.cron(), @pc.route()
  ├── 5. Validates (memory, timeout, duplicate routes, schedules, etc.)
  ├── 6. Generates .dist/{Feature}Function/ for each Lambda
  └── 7. Writes template.yaml

Installation

From the repository (local development)

# From the project root
pip install -e ./deployless

# Or with uv
uv add --editable ./deployless

deployless dependencies

pyyaml
click
flask         # you should already have it installed

Runtime dependency in each Lambda

Each generated Lambda needs aws-wsgi to adapt Flask to the API Gateway event format. deployless adds it automatically to the requirements.txt of each .dist/ package.

pip install aws-wsgi

deployless.yaml reference

Create this file at the project root (at the same level as requirements.txt). All fields are optional; default values are indicated.

# Project name
name: mi-app

# Cloud provider — only "aws" is supported for now
provider: aws

# Deployment stage. Can be overridden with --stage in the CLI.
stage: dev

# Tags applied to all CloudFormation resources
tags:
  Project: mi-app
  Environment: production

# Paths to the key directories of the project
paths:
  features: app/features    # Directory where features live
  shared: app/shared         # Shared code (copied into each Lambda)

# Global config for all Lambda functions
globals:
  runtime: python3.13        # Lambda runtime
  memory: 256                # MB (128–10240)
  timeout: 30                # Seconds (1–900)
  log_retention: 14          # Retention in CloudWatch (days)
                             # Valid values: 1,3,5,7,14,30,60,90,120,
                             # 150,180,365,400,545,731,1096,1827,3653

# API Gateway configuration
api:
  endpoint_type: REGIONAL    # REGIONAL | EDGE | PRIVATE

  # CORS
  cors:
    allow_origin: "*"          # Or a list: ["https://mi-app.com"]
    allow_methods: [GET, POST, PUT, DELETE, OPTIONS]
    allow_headers: [Content-Type, Authorization, X-API-Key]
    max_age: 3600              # Seconds the browser caches the preflight
    # allow_credentials: true  # Not compatible with allow_origin: "*"

  # Global API Gateway authentication
  # (see "API Gateway Authentication" section for details)
  auth:
    type: cognito              # cognito | lambda | iam
    user_pool_arn: "arn:aws:cognito-idp:us-east-1:123456789:userpool/us-east-1_ABC"
    name: CognitoAuthorizer    # Optional
    scopes: []                 # Optional

  # API Keys
  api_keys: true               # true = generate a new key | "key-id" = use existing

  # Rate limiting (requires api_keys)
  usage_plan:
    rate: 10000                # Requests/second
    burst: 2000                # Maximum peak
    quota: 1000000             # Optional — total requests
    period: DAY                # DAY | WEEK | MONTH (required if quota is set)

  # Custom domain
  domain:
    domain_name: api.mi-app.com
    certificate_arn: "arn:aws:acm:us-east-1:123456789:certificate/abc-123"
    base_path: /v1             # Optional
    route53:                   # Optional — configures DNS automatically
      hosted_zone_id: Z1234567890ABC

  # MIME types that API Gateway treats as binary (non-UTF-8)
  binary_media_types:
    - image/png
    - image/jpeg
    - application/octet-stream

  # Compress responses larger than N bytes
  minimum_compression_size: 1024

# Global environment variables injected into ALL functions
env:
  APP_ENV: production
  LOG_LEVEL: INFO

# .env file — environment variables and secrets
# Normal variables are injected as env vars in all Lambdas.
# Variables with the SECRET_ prefix are pushed to SSM Parameter Store as SecureString
# and injected as dynamic references {{resolve:ssm-secure:...}}.
env_file: .env.production

# KMS key to encrypt secrets in SSM (optional).
# If not specified, SSM uses the AWS-managed key (aws/ssm).
# Accepts alias ("mi-app/secrets") or key ID / ARN.
secrets_kms: mi-app/secrets

API Gateway Authentication

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    # Optional, default: "CognitoAuthorizer"
    scopes:                    # Optional — required OAuth2 scopes
      - email
      - profile

Lambda Authorizer (custom function)

api:
  auth:
    type: lambda
    function_arn: "arn:aws:lambda:us-east-1:123456789:function:my-authorizer"
    name: LambdaAuthorizer     # Optional, default: "LambdaAuthorizer"
    ttl: 300                   # Seconds before re-authorizing (0 = no cache)
    identity:
      header: Authorization    # Header where the token is located

IAM

api:
  auth:
    type: iam

Override auth per feature

From routes.py, you can override the global auth for an entire feature:

import deployless as pc

# All endpoints in this feature are public (no auth)
pc.configure(auth=None)

# All endpoints in this feature require an API key
pc.configure(auth="api_key")

Override auth per individual route (split Lambda)

@pc.route(memory=512, auth=None)      # This endpoint is public
@bp.route('/health', methods=['GET'])
def health_check():
    return {"status": "ok"}

@pc.route(memory=1024, auth="api_key")  # This endpoint requires an API key
@bp.route('/export', methods=['POST'])
def export_data():
    ...

Auth hierarchy (highest priority first)

@pc.route(auth=...)        ← Individual route (split lambdas only)
pc.configure(auth=...)     ← Entire feature
api.auth in deployless.yaml   ← Global

API Keys and Rate Limiting

api:
  api_keys: true        # Generates a new API key
  usage_plan:
    rate: 10000         # 10k requests/second
    burst: 2000         # Peak of 2k simultaneous
    quota: 1000000      # Maximum 1M requests per day
    period: DAY

The generated API Key ID appears in the stack Outputs:

# View the key value (not shown in Outputs for security)
aws apigateway get-api-key --api-key <ApiKeyId> --include-value

To use an existing key instead of creating a new one:

api:
  api_keys: "abc123existingkeyid"

Validation rules

Code Rule
E00 Resource validations: DynamoDB (key types, GSI, projection INCLUDE), S3 (bucket name DNS-compliant, 3–63 chars, no underscores), SQS (queue name, visibility_timeout, message_retention, max_receive_count), KMS (alias format, valid key_usage/key_spec, ECC/SIGN_VERIFY incompatibilities), SSMParameter (name starts with /, valid chars, valid type, non-empty value)
E01 stage can only contain alphanumeric characters
E02 api.endpoint_type must be REGIONAL, EDGE, or PRIVATE
E03 globals.log_retention must be a valid CloudWatch value
E04 allow_credentials: true is not compatible with allow_origin: "*"
E11 api.auth.type must be cognito, lambda, or iam
E12 api.auth (cognito): user_pool_arn is required
E13 api.auth (lambda): function_arn is required
E14 api.usage_plan: rate and burst are required
E15 api.usage_plan: period is required if quota is set
E16 api.usage_plan.period must be DAY, WEEK, or MONTH
E17 api.domain: domain_name and certificate_arn are required
E18 api.minimum_compression_size must be an integer >= 0
E19 ephemeral_storage out of range (512–10240 MB)
E20 reserved_concurrency must be >= 0
E21 provisioned_concurrency must be >= 1
E22 log_retention per feature must be a valid CloudWatch value
E23 alarms.sns_topic_arn must be a valid ARN (starts with arn:)
E24 alarms.duration.threshold_pct must be between 1 and 100
E25 lambda_function memory out of range (128–10240 MB)
E26 lambda_function timeout out of range (1–900 s)
E27 Specified env_file does not exist
E28 SECRET_ variable with empty value
E29 Invalid secrets_kms format

pc.configure() in routes.py

pc.configure() is called at module level in routes.py to register the Lambda configuration for that feature. It is a no-op at runtime: when your Flask app starts normally, this call does nothing visible. Only the deployless compiler reads it.

deployless automatically detects which feature is being called by inspecting the call stack.

Full parameter reference

import deployless as pc

pc.configure(
    # ── Basic ───────────────────────────────────────────────────────────────
    memory=512,                  # int — MB. Overrides globals.memory (128–10240)
    timeout=30,                  # int — Seconds. Overrides globals.timeout (1–900)
    description="Mi feature",    # str — Description visible in CloudFormation

    # ── Environment ──────────────────────────────────────────────────────────
    env={"FLAG": "true"},        # dict — Additional env vars for this Lambda
    layers=["arn:aws:lambda:..."],# list — Lambda Layer ARNs

    # ── IAM ──────────────────────────────────────────────────────────────────
    policies=[                   # list — Inline IAM policies (SAM format)
        "AmazonDynamoDBReadOnlyAccess",          # Managed policy by name
        {"DynamoDBCrudPolicy": {"TableName": pc.Ref(mi_tabla)}},  # SAM policy
        {"Version": "2012-10-17", "Statement": [...]},            # Inline policy
    ],

    # ── AWS Resources ─────────────────────────────────────────────────────────
    resources={                  # dict — Resources this feature uses
        "users": pc.DynamoDB("users-table", pk="id"),
        "files": pc.S3("uploads-bucket"),
        "jobs":  pc.SQS("jobs-queue", dlq=True),
    },

    # ── Architecture ──────────────────────────────────────────────────────────
    architectures=["arm64"],     # list — ["x86_64"] or ["arm64"] (Graviton, ~20% cheaper)
    tracing=True,                # bool — Enables AWS X-Ray distributed tracing

    # ── Concurrency ───────────────────────────────────────────────────────────
    reserved_concurrency=10,     # int >= 0 — Maximum simultaneous execution limit.
                                 #   0 = full throttle (useful for temporarily disabling)
    provisioned_concurrency=3,   # int >= 1 — Pre-warmed instances (eliminates cold starts).
                                 #   Implies AutoPublishAlias: live in the template.

    # ── Temporary storage ─────────────────────────────────────────────────────
    ephemeral_storage=1024,      # int — Size of /tmp in MB (512–10240, default 512)

    # ── Reliability ───────────────────────────────────────────────────────────
    dlq=True,                    # bool — Creates an SQS Dead Letter Queue for
                                 #   failed asynchronous invocations

    # ── Observability ─────────────────────────────────────────────────────────
    log_retention=30,            # int — Retention days in CloudWatch (overrides global)

    alarms=True,                 # Enables CloudWatch Alarms with default thresholds
    # alarms=False,              # Disables alarms for this feature
    # alarms={...},              # Custom config (see Alarms section)

    # ── Auth (API Gateway) ────────────────────────────────────────────────────
    auth=None,                   # None = public routes | "api_key" = requires API key
                                 # (not specified = inherits global auth from deployless.yaml)
)

Full example

# 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():
    ...

AWS Resources

Resources are declared inside pc.configure(resources={...}) in the routes.py of each feature. deployless adds them to template.yaml and automatically assigns environment variables to them.

DynamoDB

pc.DynamoDB(
    table_name: str,                      # Table name in AWS
    pk: str = "id",                       # Partition key
    pk_type: str = "S",                   # "S" (String) | "N" (Number) | "B" (Binary)
    sk: str = None,                       # Optional sort key. If defined → AWS::DynamoDB::Table
    sk_type: str = "S",                   # "S" | "N" | "B"
    gsi: list = None,                     # Global Secondary Indexes (see format below)
    billing_mode: str = "PAY_PER_REQUEST",# "PAY_PER_REQUEST" | "PROVISIONED"
    read_capacity: int = None,            # Only for billing_mode="PROVISIONED" (default: 5)
    write_capacity: int = None,           # Only for billing_mode="PROVISIONED" (default: 5)
    ttl_attribute: str = None,            # Time-To-Live attribute (DynamoDB expires it automatically)
    stream: str = None,                   # "NEW_IMAGE" | "OLD_IMAGE" | "NEW_AND_OLD_IMAGES" | "KEYS_ONLY"
    point_in_time_recovery: bool = False, # Enables PITR (point-in-time recovery)
    sse_enabled: bool = True,             # Encryption at rest with AWS-managed KMS
    deletion_policy: str = "Delete",      # "Delete" | "Retain" | "Snapshot"
    existing: bool = False,               # True = table already exists, do not create (only injects env var)
)

CloudFormation type

deployless always generates AWS::DynamoDB::Table regardless of whether a sort key or GSI is defined. This avoids CloudFormation replacement (and data loss) when you later add a sort key or GSI to a table that started with only a partition key.

Auto-generated environment variable

The -table / _table suffix is removed to avoid redundancy:

table_name Environment variable
users-table USERS_TABLE
orders_table ORDERS_TABLE
sessions SESSIONS_TABLE

GSI format

Each element of the gsi list accepts:

{
    "name": "StatusIndex",           # Required — index name
    "pk": "status",                  # Required — index partition key
    "pk_type": "S",                  # Optional, default "S"
    "sk": "created_at",              # Optional — index sort key
    "sk_type": "S",                  # Optional, default "S"
    "projection": "ALL",             # "ALL" | "KEYS_ONLY" | "INCLUDE" (default "ALL")
    "non_key_attributes": ["email"], # Required only if projection="INCLUDE"
}

Examples

Simple table (PK only):

pc.DynamoDB("sessions-table", pk="session_id", ttl_attribute="expires_at")
# → AWS::DynamoDB::Table
# → Variable: SESSIONS_TABLE

Table with SK and multiple GSIs:

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 with SSEEnabled=True
# → Variable: ORDERS_TABLE

Table with provisioned capacity:

pc.DynamoDB(
    "high-traffic-table",
    pk="pk",
    sk="sk",
    billing_mode="PROVISIONED",
    read_capacity=100,
    write_capacity=50,
)

Table with DynamoDB Streams:

pc.DynamoDB(
    "events-table",
    pk="event_id",
    stream="NEW_AND_OLD_IMAGES",  # Triggers a Lambda on every change
)

Existing table (do not create, only inject env var):

pc.DynamoDB("prod-users-table", existing=True)
# Does not generate a CloudFormation resource
# Injects: PROD_USERS_TABLE = "prod-users-table" (literal string)

S3

pc.S3(
    bucket_name: str,
    versioning: bool = False,
    encryption: bool = True,        # SSE-S3 (AES256) enabled by default
    cors: list = None,              # List of CORS rules (CloudFormation CorsRule format)
    lifecycle_rules: list = None,   # List of lifecycle rules (CloudFormation format)
    public_access_block: bool = True,  # Blocks public access by default
    deletion_policy: str = "Delete",
    existing: bool = False,
)

Auto-generated environment variable:

  • uploads-bucketUPLOADS_BUCKET
  • my_files_bucketMY_FILES_BUCKET (the -bucket / _bucket suffix is removed)

Compile-time validations (E00):

  • bucket_name cannot be empty
  • Length between 3 and 63 characters
  • Cannot contain underscores (S3 is DNS-compliant)
  • Lowercase only, digits, hyphens, and dots — starts and ends with alphanumeric

Basic example:

pc.S3("user-uploads")
# → SSE-S3 AES256 enabled, public access blocked
# → Variable: UPLOADS_BUCKET

Example with all options:

pc.S3(
    "user-uploads",
    versioning=True,
    encryption=True,           # AES256 by default — pass False only if using external KMS
    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 = FIFO queue. Adds .fifo to the name automatically.
    dlq: bool = False,                # True = also creates a Dead Letter Queue
    visibility_timeout: int = 30,     # seconds (0–43200)
    message_retention: int = 345600,  # seconds (60–1209600, default 4 days)
    max_receive_count: int = 3,       # Attempts before sending to DLQ (1–1000)
    encryption: bool = True,          # SqsManagedSseEnabled — SSE-SQS enabled by default
    deletion_policy: str = "Delete",
    existing: bool = False,
)

Note: SQS and KMS return multiple CloudFormation resources (the main queue + DLQ, or the key + alias). deployless inserts them all correctly into the template.

Auto-generated environment variable:

  • notifications-queueNOTIFICATIONS_QUEUE_URL

Compile-time validations (E00):

  • queue_name cannot be empty or exceed 80 characters
  • Alphanumeric only, - and _ (the .fifo suffix is excluded from validation)
  • visibility_timeout must be in range [0, 43200]
  • message_retention must be in range [60, 1209600]
  • max_receive_count must be in range [1, 1000]

Basic example:

pc.SQS("email-notifications")
# → SSE-SQS enabled, 4-day retention, 30s visibility
# → Variable: EMAIL_NOTIFICATIONS_QUEUE_URL

Example with DLQ:

pc.SQS(
    "email-notifications",
    dlq=True,
    visibility_timeout=60,
    message_retention=86400,   # 1 day
    max_receive_count=5,
)
# → Main queue + DLQ with 14-day retention
# → Both with SSE-SQS enabled

FIFO example:

pc.SQS(
    "orders",
    fifo=True,       # → queue_name becomes "orders.fifo" automatically
    dlq=True,        # → DLQ will also be FIFO: "orders-dlq.fifo"
)

KMS

pc.KMS(
    alias: str = None,                      # e.g. "alias/mi-app" or simply "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 for SYMMETRIC_DEFAULT, False otherwise
    deletion_policy: str = "Retain",        # KMS uses Retain by default (security)
    existing_key_id: str = None,            # ID or ARN of an existing key (does not create resource)
    env_var: str = None,                    # Forces the name of the generated env var
)

Auto-generated environment variable:

  • env_var="MY_KEY"MY_KEY (takes priority over any automatic derivation)
  • alias="myapp/encryption"MYAPP_ENCRYPTION_KEY_ID
  • No alias or env_var → KMS_KEY_ID

Generated CloudFormation resources:

  • AWS::KMS::Key — with Enabled: True, KeyUsage, KeySpec, and a basic key policy (root account)
  • AWS::KMS::Alias — optional alias to identify the key by name
  • EnableKeyRotation is only added when key_spec="SYMMETRIC_DEFAULT" (asymmetric keys do not support automatic rotation)

Compile-time validations (E00):

  • alias can only contain alphanumeric characters, -, _, /
  • key_usage must be one of the valid values
  • key_spec must be one of the valid values
  • enable_rotation=True is not valid for asymmetric keys (RSA, ECC, HMAC)
  • ECC key_spec is not compatible with key_usage="ENCRYPT_DECRYPT"
  • key_spec="SYMMETRIC_DEFAULT" is not compatible with key_usage="SIGN_VERIFY"

Note: The Lambda does NOT have permissions to use the key by default. You must add the IAM policy explicitly with pc.configure(policies=[...]).

Example with IAM permissions

kms_key = pc.KMS(
    alias="mi-app/datos",
    description="Encryption key for sensitive data",
    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),
                }
            ],
        }
    ],
)

How to use the key in app code

The KMS_KEY_ID environment variable (or {ALIAS}_KEY_ID if using an alias) is automatically injected into the Lambda. Use it in your encryption services:

# 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:
    """Encrypts a string and returns the ciphertext in 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:
    """Decrypts a base64 ciphertext and returns the plaintext."""
    ciphertext_blob = base64.b64decode(ciphertext_b64)
    response = kms_client.decrypt(CiphertextBlob=ciphertext_blob)
    return response['Plaintext'].decode('utf-8')

kms:Decrypt does not need to specify KeyId because the ciphertext already embeds the ID of the key that encrypted it.

Full example — RSA key encryption per tenant

A real pattern used in the reference app: the tenant feature encrypts the RSA private key when creating the tenant, and the auth feature decrypts it on each login.

# app/features/tenant/routes.py
import deployless as pc

tenant_key = pc.KMS(
    alias="ums/tenant-keys",
    description="Encryption of RSA private keys per 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 only encrypts
                    "Resource": pc.Ref(tenant_key),
                }
            ],
        },
    ],
)
# app/features/auth/routes.py
import deployless as pc

# Reuses the same existing key (does not create it again)
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 only decrypts
                    "Resource": os.getenv("UMS_TENANT_KEYS_KEY_ID"),
                }
            ],
        }
    ],
)

Auto-injected environment variables:

Alias Variable
ums/tenant-keys UMS_TENANT_KEYS_KEY_ID
mi-app MI_APP_KEY_ID
No alias KMS_KEY_ID

Asymmetric key for digital signing (RSA)

signing_key = pc.KMS(
    alias="mi-app/signing",
    description="RSA key for signing JWTs or documents",
    key_usage="SIGN_VERIFY",
    key_spec="RSA_2048",
    # enable_rotation does not apply — automatically ignored for asymmetric keys
)

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

Existing key (do not create, only inject env var)

pc.KMS(existing_key_id="arn:aws:kms:us-east-1:123456789:key/abc-123")
# Does not generate a CloudFormation resource
# KMS_KEY_ID = "arn:aws:kms:us-east-1:123456789:key/abc-123"

SSM Parameter Store

deployless provides two tools for SSM: pc.SSMParameter to create a parameter as a CloudFormation resource, and pc.SSMParam to reference an existing parameter as a dynamic reference in env vars.

pc.SSMParameter — create a parameter

pc.SSMParameter(
    name: str,                  # Parameter path, must start with "/"
    value: str,                 # Parameter value
    type: str = "String",       # "String" | "StringList" | "SecureString"
    description: str = None,
    existing: bool = False,     # True = do not create, only inject env var
)

Auto-generated environment variable — last segment of the path:

  • /myapp/db/hostHOST
  • /myapp/api/secret-keySECRET_KEY

Compile-time validations (E00):

  • name must start with /
  • Alphanumeric only, ., -, _, /
  • type must be String, StringList, or SecureString
  • value cannot be empty (except for SecureString)

Example:

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 — reference an existing parameter

Does not generate a CloudFormation resource. Produces a CloudFormation dynamic reference directly in the env var value.

pc.SSMParam(
    name: str,              # Path of the existing parameter
    secure: bool = False,   # True → "{{resolve:ssm-secure:/path}}" (SecureString)
    version: int = None,    # Optional — pin to a specific version
)

Usage in 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),
    }
)

This generates in the 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:...}} only works with SecureString parameters and requires the Lambda to have ssm:GetParameter + kms:Decrypt permission on the parameter's KMS key.



CloudWatch Alarms

deployless can automatically generate 3 alarms per Lambda: errors, throttles, and duration.

Activation

# In routes.py — enables alarms with default thresholds
pc.configure(alarms=True)

# With custom thresholds
pc.configure(alarms={
    "errors": {
        "threshold": 1,      # Trigger when Errors >= 1 in the period
        "period": 300,        # Evaluation period in seconds
    },
    "throttles": {
        "threshold": 1,
        "period": 300,
    },
    "duration": {
        "threshold_pct": 80,  # Trigger when Duration > 80% of the configured timeout
        "period": 300,        # (if timeout=30s → alarm at 24000ms)
    },
    "sns_topic_arn": "arn:aws:sns:us-east-1:123456789:my-alerts",  # Optional
})

# Disable alarms for this feature even if globally active
pc.configure(alarms=False)

Global alarms (for all features)

In deployless.yaml, you can activate alarms for the entire project:

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"

Generated resources

For each feature with alarms active, deployless generates in the 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% of 30s = 24000ms
    # ...

pc.Ref() and pc.GetAtt() — Referencing resources

Use pc.Ref(resource) to get the logical ID of a resource (generates {"Ref": "LogicalId"}), and pc.GetAtt(resource, attr) to get a specific attribute (generates {"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() and pc.GetAtt() accept both a resource object and a string with the CloudFormation logical ID.


@pc.cron() — Scheduled Lambdas

Decorate any function with @pc.cron() to have deployless deploy it as a separate Lambda triggered by EventBridge (CloudWatch Events) on the indicated schedule.

@pc.cron(
    schedule: str,          # Schedule expression (required)
    memory: int = None,     # MB. If None, uses globals.memory
    timeout: int = None,    # Seconds. If None, uses globals.timeout
    env: dict = None,       # Additional environment variables
    description: str = None,
)

Schedule formats:

  • "rate(5 minutes)" — every 5 minutes
  • "rate(1 hour)" — every hour
  • "rate(24 hours)" — daily
  • "cron(0 9 * * ? *)" — every day at 9:00 UTC

The function must have the Lambda signature (event, context).

Example:

# app/features/user/routes.py
import deployless as pc

@pc.cron(
    schedule="rate(24 hours)",
    memory=128,
    timeout=300,
    description="Daily cleanup of expired users",
)
def cleanup_expired_users(event, context):
    # Your logic here
    deleted = delete_expired_users()
    return {"status": "ok", "deleted": deleted}

This generates in 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 per route

By default, all routes in a feature share a single Lambda. With @pc.route() you can isolate a specific endpoint into its own Lambda (useful for endpoints that consume many resources or have different timeouts).

@pc.route(
    memory: int = None,
    timeout: int = None,
    description: str = None,
    auth = <not specified>,   # None = public | "api_key" = requires API key
                              # (not specified = inherits auth from feature or global)
)

The @pc.route() decorator must go above the Flask decorator.

# 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="Heavy data export")
@user_bp.route("/export", methods=["POST"])
def export_users():
    # This endpoint will have its own Lambda with 1 GB and 2-minute timeout
    ...

@user_bp.route("", methods=["GET"])
def list_users():
    # This endpoint goes in the feature's shared Lambda
    ...

This generates two separate Lambda functions:

  • UserFunction — contains GET /users (and all other endpoints without @pc.route())
  • ExportUsersFunction — contains only POST /users/export

@pc.lambda_function() — Standalone Lambdas

For Lambda functions that have no HTTP routes or schedules — for example, SQS consumers, S3 event handlers, or Step Functions steps — use @pc.lambda_function().

@pc.lambda_function(
    memory: int = None,       # MB. If None, uses globals.memory
    timeout: int = None,      # Seconds. If None, uses globals.timeout
    env: dict = None,         # Additional environment variables
    description: str = None,
)

The function must have the Lambda signature (event, context).

Example:

# app/features/orders/routes.py
import deployless as pc

@pc.lambda_function(memory=512, timeout=60, description="Processes messages from the orders queue")
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", []))}

This generates in 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

Note: Unlike HTTP features, standalone lambdas have no API Gateway events. You can connect them to SQS, S3, DynamoDB Streams, etc. manually in the template or via event source mappings.


pc.shared_resource() — Global resources

If a resource (DynamoDB table, S3 bucket, SQS queue, etc.) must be available to all features, use pc.shared_resource() instead of declaring it inside the resources of an individual feature.

pc.shared_resource(key: str, resource)
  • The resource is included only once in the CloudFormation template.
  • The resource's environment variables are injected into all Lambdas in the project.
  • It can be referenced with pc.Ref() and pc.GetAtt() from any feature.

Example:

# app/features/events/routes.py (or any routes.py)
import deployless as pc

# Table shared by all features
pc.shared_resource("audit_log", pc.DynamoDB("audit-log", pk="event_id", sk="timestamp"))

# Shared bucket
pc.shared_resource("shared_assets", pc.S3("app-shared-assets"))

From any feature you can use the generated environment variables:

import os

audit_table = os.getenv("AUDIT_LOG_TABLE")       # Injected in ALL Lambdas
assets_bucket = os.getenv("APP_SHARED_ASSETS_BUCKET")

.env file and secrets

deployless can read a .env file to inject environment variables and manage secrets automatically.

Configuration in deployless.yaml

env_file: .env.production       # Path to the .env file

# Optional — KMS key to encrypt secrets in SSM
secrets_kms: mi-app/secrets     # Alias, key ID, or ARN

.env file format

# Normal variables — injected directly as env vars in all Lambdas
APP_ENV=production
LOG_FORMAT=json

# Secrets — the SECRET_ prefix indicates they are pushed to SSM Parameter Store
SECRET_DB_PASSWORD=mysecretpassword
SECRET_API_KEY=sk_live_xxxx

Behavior

Type Example Destination Value in Lambda
Normal APP_ENV=production Direct env var production
Secret SECRET_DB_PASSWORD=xxx SSM Parameter Store {{resolve:ssm:/mi-app/SECRET_DB_PASSWORD}}

For SECRET_ variables:

  1. The name is kept in full with the prefix: SECRET_DB_PASSWORD/mi-app/SECRET_DB_PASSWORD
  2. The value is stored as String in SSM Parameter Store under the path /{app_name}/{VAR_NAME}
  3. The Lambda receives a dynamic reference {{resolve:ssm:...}} that CloudFormation resolves when creating/updating the stack
  4. The env var in the Lambda also keeps the full name: SECRET_DB_PASSWORD

Note: String (not SecureString) is used because CloudFormation does not support {{resolve:ssm-secure:...}} in Lambda environment variables. The value is still protected by IAM — only roles with ssm:GetParameter permission can read it.

Validations

Code Rule
E27 The specified env_file does not exist
E28 SECRET_ variable with empty value
E29 Invalid secrets_kms format (alias can only contain alphanumeric characters, -, _, /)

CLI commands

deployless build

Generates template.yaml and builds the .dist/ packages.

deployless build

# Options:
deployless build --stage prod            # Overrides the stage
deployless build -o infra/template.yaml  # Template output path
deployless build --dry-run               # Validates without writing files
deployless build --push-secrets          # Also push SECRET_ vars to SSM
deployless build --verbose               # Detailed output

deployless validate

Validates the project without generating any files. Equivalent to build --dry-run but with cleaner output.

deployless validate
deployless validate --stage prod
deployless validate --check-existing   # Verifies that resources with existing=True exist in AWS
deployless validate --verbose

deployless check

Runs pre-flight checks before deploying: validates env vars, verifies the SAM CLI is installed, checks AWS credentials, and verifies that resources declared with existing=True actually exist in AWS.

deployless check
deployless check --stage prod
deployless check --verbose

deployless deploy

Chains deployless check + deployless build + sam build + sam deploy. Requires the AWS SAM CLI to be installed.

If samconfig.toml does not exist (first deployment), --guided is added automatically so SAM prompts for the initial configuration.

deployless deploy
deployless deploy --stage prod
deployless deploy --guided          # Force wizard mode
deployless deploy --push-secrets    # Push SECRET_ vars to SSM before deploying

deployless clean

Removes the generated files (.dist/ and template.yaml).

deployless clean
deployless clean -o infra/template.yaml  # If you used a different output path

deployless info

Shows a summary of the detected project.

deployless info

Example output:

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

Pushes the SECRET_* variables from the .env file to AWS SSM Parameter Store.

deployless secrets push
deployless secrets push --stage prod
deployless secrets push --env-file .env.prod   # Overrides the env_file path from deployless.yaml
deployless secrets push --verbose

Process:

  1. Reads the .env file (from deployless.yaml or --env-file)
  2. Filters variables with the SECRET_ prefix
  3. Creates/updates SSM parameters: /{app_name}/{VAR_NAME} (type String)

Note: deployless build does not push secrets automatically. Use deployless secrets push explicitly, or pass --push-secrets to deployless build / deployless deploy.

Example:

# .env.prod
SECRET_DB_PASSWORD=mysecretpassword
SECRET_API_KEY=sk_live_xxx
deployless secrets push --env-file .env.prod
# Creates in SSM:
#   /mi-app/SECRET_DB_PASSWORD  (String)
#   /mi-app/SECRET_API_KEY      (String)

deployless secrets sync

Push + removes orphaned parameters in SSM. Useful for keeping SSM in sync when secrets are removed from the .env.

deployless secrets sync
deployless secrets sync --stage prod
deployless secrets sync --env-file .env.prod
deployless secrets sync --yes              # Auto-confirms deletion of orphans
deployless secrets sync --verbose

Behavior:

  1. Pushes all SECRET_* variables (same as secrets push)
  2. Lists existing parameters under /{app_name}/ in SSM
  3. Detects parameters that are no longer in the .env
  4. Asks for confirmation before deleting them (unless --yes is used)

Project structure

deployless expects the following directory structure (configurable in deployless.yaml):

mi-proyecto/
├── deployless.yaml             # deployless configuration
├── requirements.txt         # Global project dependencies
├── app/
│   ├── features/            # One folder per feature
│   │   ├── auth/
│   │   │   ├── routes.py    # REQUIRED — Flask Blueprint + pc.configure()
│   │   │   ├── use_cases/
│   │   │   ├── repositories/
│   │   │   └── schemas/
│   │   ├── user/
│   │   │   ├── routes.py
│   │   │   ├── requirements.txt  # OPTIONAL — extra dependencies for this feature
│   │   │   └── ...
│   │   └── tenant/
│   │       └── routes.py
│   └── shared/              # Shared code — copied into ALL Lambdas
│       ├── decorators/
│       ├── errors/
│       └── config.py
└── .dist/                   # Generated by deployless build (do not commit to git)
    ├── AuthFunction/
    │   ├── app/
    │   │   ├── __init__.py
    │   │   ├── features/
    │   │   │   ├── __init__.py
    │   │   │   └── auth/        # Only this feature's code
    │   │   │       ├── routes.py
    │   │   │       ├── use_cases/
    │   │   │       └── ...
    │   │   └── shared/          # Copy of app/shared/
    │   ├── bootstrap.py         # Auto-generated
    │   ├── deployless.py           # Runtime stub (no-ops)
    │   └── requirements.txt     # Global + feature + aws-wsgi requirements.txt
    ├── UserFunction/
    └── TenantFunction/

Discovery rules

  • deployless scans app/features/ looking for subdirectories that contain a routes.py file.
  • Directories starting with _ (e.g. __pycache__) are ignored.
  • They are processed in alphabetical order.
  • Each routes.py must define at least one Flask Blueprint with at least one route.

The generated bootstrap

For each Lambda a bootstrap.py is generated that:

  1. Registers all Flask Blueprints found in routes.py.
  2. Creates a temporary Flask app.
  3. Wraps the app with aws_wsgi.response() to convert API Gateway events into WSGI requests.
# .dist/UserFunction/bootstrap.py — auto-generated, do not edit
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  # full app/ namespace
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"})

Each Lambda also includes a deployless.py with no-op implementations of all deployless functions (configure, KMS, DynamoDB, etc.), so that import deployless as pc statements in routes.py do not fail at runtime without needing to install the full package.


Full example

This example uses the real app in this repository (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

# ---- Lambda configuration for the "user" feature ----
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: daily cleanup of expired sessions ----
@pc.cron(
    schedule="rate(24 hours)",
    memory=128,
    timeout=60,
    description="Limpieza de sesiones expiradas",
)
def cleanup_sessions(event, context):
    # Cleanup logic
    return {"status": "ok"}

# ---- Flask Blueprint ----
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: heavy export ----
@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():
    # This endpoint will have its own Lambda
    ...
    return jsonify({"url": "https://..."}), 200

3. Run the build

deployless build --verbose

Expected output:

[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. Deploy

# First time (SAM interactive wizard)
deployless deploy --guided --stage prod

# Subsequent deployments
deployless deploy --stage prod

5. Generated template.yaml (summary)

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
        # ... more events

  UmsUsersTable:
    Type: AWS::DynamoDB::Table
    DeletionPolicy: Retain
    Properties:
      TableName: ums-users
      BillingMode: PAY_PER_REQUEST
      # ... attributes, 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]
  # ...

Known notes and limitations

  • Only Flask is supported for now. FastAPI support is planned (adapter in deployless/adapters/fastapi.py).
  • Feature code is copied flat: only the .py files in the root directory of the feature are included. Subdirectories (use_cases, repositories, etc.) are not copied. If your routes.py imports from its own subdirectories, you will need to adapt the structure or extend the packager.
  • app/shared/ is copied in full into each Lambda under the name shared/. Imports like from app.shared.x import y will need to be changed to from shared.x import y in Lambda production code.
  • Dependencies are not installed during deployless build. sam build (run by deployless deploy) is what installs the requirements.txt of each package.
  • SQS and KMS resources return multiple CloudFormation entries (queue + DLQ, key + alias). deployless inserts them all correctly into the 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.2.tar.gz (86.8 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.2-py3-none-any.whl (64.1 kB view details)

Uploaded Python 3

File details

Details for the file deployless-0.1.2.tar.gz.

File metadata

  • Download URL: deployless-0.1.2.tar.gz
  • Upload date:
  • Size: 86.8 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.2.tar.gz
Algorithm Hash digest
SHA256 c647942b162b83634d351ee1050bdc08bc55bfa086022abf0682ed12eee8c38e
MD5 44b6768512b72b486b4dfc48dd85b37f
BLAKE2b-256 53b2a3d111123b981c245721957c2536c602fdf2401a73571699d9bd12c50bef

See more details on using hashes here.

File details

Details for the file deployless-0.1.2-py3-none-any.whl.

File metadata

  • Download URL: deployless-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 64.1 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.2-py3-none-any.whl
Algorithm Hash digest
SHA256 fdc123f6a0cedd27d9f38a66759368ad498b58fee9d4f2ff95adb9c8fffe5f57
MD5 6f6323ac6d16150348de95d2e5c1beb6
BLAKE2b-256 1a36fa8d8e65138b822cad0ca7086c84ecc2078005e4e3c996276b7d6983efab

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