---
title: A serverless API in one afternoon with SAM — and the SecureString wall
date: 2026-07-31
tags: [aws-sam, aws-lambda, serverless, ssm-parameter, securestring, cloudformation]
type: success
reproducible: true
---

# A serverless API in one afternoon with SAM — and the SecureString wall

I ship mobile apps. AWS backends are not my home turf, and I expected standing up
an API — stories in a database, publish with auth, public reads — to eat a week.

It ate an afternoon, and the reason is that the whole backend is two files. One
YAML template (~60 lines) describes everything: the database table, the function,
the HTTP front door. One Python file is the entire API. `sam deploy` reads the
description and conjures the real infrastructure; change the file, deploy again,
done. Nothing to patch, nothing idling at 3am, and at my scale the bill is
literally pennies — pay-per-request everywhere.

The only wall of the day was, in retrospect, the best part. I stored an auth token
in SSM Parameter Store and naturally marked it SecureString — encrypted, the
responsible choice. Deploy failed: "Non-secure ssm prefix was used for secure
parameter." CloudFormation refuses to inject encrypted parameters into Lambda
environment variables — because env vars sit in plaintext in the console, and
injecting a decrypted secret there would quietly defeat the encryption. The wall
was a security feature protecting me from myself.

Two honest exits, and we ended up using both: a plain String parameter for the
low-stakes token, and for the real secret (an OAuth client secret), reading it at
runtime from inside the function with a narrowly-scoped IAM permission. Annoying
in the moment. Correct forever after.

## Reproduce this

Prerequisites: an AWS account, SAM CLI installed, Python 3.12.

Your agent will need from you: the resource names you want, your region, and which
values are secrets (they decide String vs runtime-fetched SecureString).

Steps: write template.yaml with AWS::Serverless::Function +
AWS::Serverless::HttpApi + AWS::DynamoDB::Table (PAY_PER_REQUEST); one handler
file routing on rawPath; deploy with sam deploy --resolve-s3 --capabilities
CAPABILITY_IAM. Plain config via '{{resolve:ssm:...}}' in env vars; secrets via
boto3 get_parameter(WithDecryption=True) at runtime plus SSMParameterReadPolicy.

Verify: curl every route; then break it on purpose — mark a param SecureString and
watch the deploy fail with the exact error above, so you recognize it forever.
