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
- What is deployless
- Installation
- deployless.yaml reference
- pc.configure() in routes.py
- AWS Resources
- @pc.cron() — Scheduled Lambdas
- @pc.route() — Split Lambdas per route
- @pc.lambda_function() — Standalone Lambdas
- pc.shared_resource() — Global resources
- .env file and secrets
- CLI commands (
build,check,validate,deploy,clean,info,secrets) - Project structure
- Full example
What is deployless
deployless takes your Flask project structured by features and generates:
- A
template.yamlfor 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-generatedbootstrap.pyand a mergedrequirements.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-bucket→UPLOADS_BUCKETmy_files_bucket→MY_FILES_BUCKET(the-bucket/_bucketsuffix is removed)
Compile-time validations (E00):
bucket_namecannot 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-queue→NOTIFICATIONS_QUEUE_URL
Compile-time validations (E00):
queue_namecannot be empty or exceed 80 characters- Alphanumeric only,
-and_(the.fifosuffix is excluded from validation) visibility_timeoutmust be in range[0, 43200]message_retentionmust be in range[60, 1209600]max_receive_countmust 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— withEnabled: True,KeyUsage,KeySpec, and a basic key policy (root account)AWS::KMS::Alias— optional alias to identify the key by nameEnableKeyRotationis only added whenkey_spec="SYMMETRIC_DEFAULT"(asymmetric keys do not support automatic rotation)
Compile-time validations (E00):
aliascan only contain alphanumeric characters,-,_,/key_usagemust be one of the valid valueskey_specmust be one of the valid valuesenable_rotation=Trueis not valid for asymmetric keys (RSA, ECC, HMAC)- ECC
key_specis not compatible withkey_usage="ENCRYPT_DECRYPT" key_spec="SYMMETRIC_DEFAULT"is not compatible withkey_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:Decryptdoes not need to specifyKeyIdbecause 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/host→HOST/myapp/api/secret-key→SECRET_KEY
Compile-time validations (E00):
namemust start with/- Alphanumeric only,
.,-,_,/ typemust beString,StringList, orSecureStringvaluecannot be empty (except forSecureString)
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 withSecureStringparameters and requires the Lambda to havessm:GetParameter+kms:Decryptpermission 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— containsGET /users(and all other endpoints without@pc.route())ExportUsersFunction— contains onlyPOST /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()andpc.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:
- The name is kept in full with the prefix:
SECRET_DB_PASSWORD→/mi-app/SECRET_DB_PASSWORD - The value is stored as
Stringin SSM Parameter Store under the path/{app_name}/{VAR_NAME} - The Lambda receives a dynamic reference
{{resolve:ssm:...}}that CloudFormation resolves when creating/updating the stack - The env var in the Lambda also keeps the full name:
SECRET_DB_PASSWORD
Note:
String(notSecureString) is used because CloudFormation does not support{{resolve:ssm-secure:...}}in Lambda environment variables. The value is still protected by IAM — only roles withssm:GetParameterpermission 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:
- Reads the
.envfile (fromdeployless.yamlor--env-file) - Filters variables with the
SECRET_prefix - Creates/updates SSM parameters:
/{app_name}/{VAR_NAME}(typeString)
Note:
deployless builddoes not push secrets automatically. Usedeployless secrets pushexplicitly, or pass--push-secretstodeployless 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:
- Pushes all
SECRET_*variables (same assecrets push) - Lists existing parameters under
/{app_name}/in SSM - Detects parameters that are no longer in the
.env - Asks for confirmation before deleting them (unless
--yesis 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 aroutes.pyfile. - Directories starting with
_(e.g.__pycache__) are ignored. - They are processed in alphabetical order.
- Each
routes.pymust define at least one Flask Blueprint with at least one route.
The generated bootstrap
For each Lambda a bootstrap.py is generated that:
- Registers all Flask Blueprints found in
routes.py. - Creates a temporary Flask app.
- 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
.pyfiles in the root directory of the feature are included. Subdirectories (use_cases, repositories, etc.) are not copied. If yourroutes.pyimports 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 nameshared/. Imports likefrom app.shared.x import ywill need to be changed tofrom shared.x import yin Lambda production code.- Dependencies are not installed during
deployless build.sam build(run bydeployless deploy) is what installs therequirements.txtof 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file deployless-0.1.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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c647942b162b83634d351ee1050bdc08bc55bfa086022abf0682ed12eee8c38e
|
|
| MD5 |
44b6768512b72b486b4dfc48dd85b37f
|
|
| BLAKE2b-256 |
53b2a3d111123b981c245721957c2536c602fdf2401a73571699d9bd12c50bef
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fdc123f6a0cedd27d9f38a66759368ad498b58fee9d4f2ff95adb9c8fffe5f57
|
|
| MD5 |
6f6323ac6d16150348de95d2e5c1beb6
|
|
| BLAKE2b-256 |
1a36fa8d8e65138b822cad0ca7086c84ecc2078005e4e3c996276b7d6983efab
|