Skip to content

AWS SAM

AWS SAM is a CloudFormation transform: a template with Transform: AWS::Serverless-2016-10-31 may use AWS::Serverless::Function, Api, HttpApi, SimpleTable, StateMachine and a few more, which CloudFormation expands into plain resources when you deploy. The types are generated like the rest, from SAM's own schema at a pinned release, and live in cfn_typed_sumo.types.serverless.

import cfn_typed as cfn
from cfn_typed_sumo import sam
from cfn_typed_sumo.types import serverless, sqs


def build() -> cfn.Template:
    orders = sqs.queue()
    worker = serverless.function(
        Handler="app.handler",
        CodeUri="src/",
        Events={
            "Orders": sam.sqs_event(cfn.get_att("Orders", "Arn"), max_batching_window=5),
            "Web": sam.http_api_event("/orders", "POST"),
        },
    )
    return sam.template(
        {"Orders": orders, "Worker": worker},
        function_globals={"Runtime": "python3.12", "Timeout": 30, "MemorySize": 512},
    )

sam.template writes the Transform header and Globals; SAM merges Globals into every function, so the runtime and timeout are stated once. Property names are checked as for any generated type: serverless.function(Handlr=...) is an error, and so is a wrong type for Handler.

Functions, tables and connectors

serverless.function takes SAM's property names. sam.function takes the arguments the other patterns use, and leaves Runtime, MemorySize and Timeout to Globals unless you pass them:

worker = sam.function(
    handler="app.handler",
    code_uri="src/",
    events={"Orders": sam.sqs_event(cfn.get_att("Orders", "Arn"))},
    environment={"TABLE": cfn.ref("Table")},
    statements=[cfn.allow("dynamodb:GetItem", cfn.get_att("Table", "Arn"))],   # one inline policy, as in grants
    managed_policies=["AWSLambdaBasicExecutionRole"],
)
Pattern Resource Notes
table(key, key_type="String") StateMachine (Definition, Policies, Schedule and EventBridgeRule events) StepFunctions::StateMachine, its {Name}Role, and for each event the rule and a role that may start the execution
SimpleTable one partition key
connect(name, source, destination, permissions=("Read",)) Connector, or Connectors inside a function SAM writes the least-privilege policy for the pair; the ID is {name}Connector

connect replaces a hand-written role statement when both ends are in the template. For anything it cannot say (conditions, a resource outside the template), use statements= instead.

What the event builders do

An event is a small typed dict. The builders exist because of two things the checker cannot do for an inline dict:

Builder Event What SAM creates
sqs_event(queue_arn, batch_size=10) SQS the event source mapping and read access to the queue
http_api_event(path, method) HttpApi a route on the implicit HTTP API (or api_id's)
schedule_event(expression) Schedule the rule and the permission to invoke the function
sns_event(topic) SNS the subscription and the permission for the topic to invoke the function
stream_event("Kinesis" or "DynamoDB", stream_arn) Kinesis, DynamoDB the event source mapping and read access to the stream
event_bridge_event(pattern) EventBridgeRule a rule on the default bus and the permission to invoke

For any other event type, build it as a typed value first (e: serverless.FunctionS3Event = {...}) and pass that in Events.

SAM or the patterns

SAM does the packaging side that patterns leave out (sam build, sam package upload the code and fill in CodeUri), and it expands to resources you do not see until deploy. Use SAM types when you want that workflow with checked properties. Use the plain function pattern when you want the exact resources in the template, as for the Serverless Framework migration, with logical IDs you choose. To move a SAM service that is already deployed, the steps in Ejecting from the Serverless Framework work on its processed template (the page says how to get it).

Using SAM from cfn-typed, or leaving it

You do not have to choose between SAM and cfn-typed all at once. There are two directions, and both start from the build() above.

Keep SAM, and write its template in Python. cfn-typed produces the SAM template; SAM still packages and deploys. Render it next to the code, because CodeUri is relative to the template file, and commit it like any other rendered template:

Path("template.yaml").write_text(cfn.to_yaml(build()))

then sam build and sam deploy as before (sam local too). You get checked property names, one Globals, and Transform written for you, and nothing changes for the people who run SAM. SAM's own translator accepts the result: it needs the local CodeUri replaced by an S3 location, which is what sam package does.

Leave SAM, and keep the Python. sam.expand(build()) turns the same template into plain CloudFormation, with no Transform and nothing of SAM in it (see expand below). Two things change. expand does not package code, so a local CodeUri such as "src/" is an error, and you upload the artifact yourself and give its S3 location (s3://bucket/key.zip, or a Bucket and Key). And the HTTP and REST APIs are replaced when a stack that SAM deployed moves over, because their logical IDs differ; the function, its role, the event mappings, rules and permissions keep SAM's IDs.

Already deployed with SAM, and not in Python yet? Do not start from SAM's types. Take the deployed stack's processed template and import it: you get plain resources under the IDs the stack already has, and then you tidy them by hand.

Keep SAM while you rely on sam build (it installs dependencies and builds the package) or sam local. Leave it when you want the exact resources in the template, or a pipeline where transforms are not allowed.

SAM without SAM: expand

sam.expand(template) does the transform in Python. It takes a template of serverless.* resources (and Globals) and returns plain CloudFormation: no Transform, no Globals, and no mention of SAM anywhere in it. The function, its role, the event source mappings, rules and routes are ordinary resources you can read, diff and check before you deploy, and the stack deploys in an account or pipeline where transforms are not allowed.

from cfn_typed_sumo import sam

template = sam.expand(build())   # build() returns the SAM template from above

It covers a fixed subset, the SAM release the types are pinned to, and says so when you go outside it:

SAM Becomes
Function (S3 CodeUri, Environment, Policies, Tags, ...) Lambda::Function and IAM::Role ({Name}, {Name}Role)
SQS, Kinesis, DynamoDB event Lambda::EventSourceMapping {Name}{Event}, and the matching execution policy on the role
SNS event SNS::Subscription {Name}{Event} and Lambda::Permission {Name}{Event}Permission
Schedule, EventBridgeRule event Events::Rule {Name}{Event} and Lambda::Permission {Name}{Event}Permission
Api event (REST) ApiGateway RestApi, a Resource per path segment, a proxy Method per route, Deployment (its ID carries a hash of the routes, so new routes mean a new deployment), Stage Prod and the invoke permission, one REST API per function
HttpApi event ApiGatewayV2 Api, Integration, Route, Stage and the invoke permission, one API per function, from the http_api pattern
StateMachine (Definition, Policies, Schedule and EventBridgeRule events) StepFunctions::StateMachine, its {Name}Role, and for each event the rule and a role that may start the execution
SimpleTable DynamoDB::Table, on-demand unless ProvisionedThroughput is given
Connector, or Connectors inside a function IAM::ManagedPolicy {Name}Policy on the source function's role ({Name}PolicyDestination{N} for a list), for Read and Write on an SQS queue, SNS topic, S3 bucket, DynamoDB table or SimpleTable, or a function
Globals.Function, IgnoreGlobals merged into each function (Environment.Variables and Tags are merged, other keys replaced)

Anything else (SAM policy templates such as LambdaInvokePolicy, other event types and resources, a local CodeUri that needs packaging, properties not listed above) raises NotImplementedError naming it, rather than being dropped. Resources that are not AWS::Serverless::* pass through unchanged.

The logical IDs of the function, role, mappings, rules, permissions, connector policies and state machine are the ones SAM generates, so a stack deployed with the transform can move to the expanded template without replacing those resources. The HTTP and REST APIs are not: SAM puts every route in one ServerlessHttpApi or ServerlessRestApi built from an OpenAPI document, and expand makes plain resources instead, so those are replaced. It also leaves out SAM's lambda:createdBy tag.

The test suite checks this against AWS's own translator (aws-sam-translator, a dev dependency pinned to the same release as the types): each resource expand makes, with the same logical ID, has to equal what SAM makes for it.