infrastructure
The template I wrote and the one AWS ran

The flat has been deployed with SAM since January and I have never once looked at what SAM actually produces. That is a strange thing to be able to say about your own infrastructure, so on Saturday I printed it.
aws cloudformation get-template \
--stack-name home-automation \
--template-stage Processed \
--query TemplateBody
template.yaml is 231 lines. The processed template — the document
CloudFormation was actually given, after the transform ran — is 704. That
difference is the entire argument for and against the tool, and it is worth
being specific about where it lives.
One function, expanded
Here is a whole function as I wrote it. Not an excerpt; this is the block.
SensorIngestFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: src/sensors/
Handler: app.handler
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref DeviceRegistryTable
Events:
Readings:
Type: IoTRule
Properties:
Sql: "SELECT * FROM 'home/+/state'"
Fifteen lines. In the processed template it is four resources: the function,
an IAM role, the IoT topic rule itself, and a Lambda::Permission letting that
rule invoke the function. The role is the interesting one.
DynamoDBCrudPolicy is two lines. It arrives as this, give or take the
formatting:
{
"Action": [
"dynamodb:GetItem", "dynamodb:DeleteItem", "dynamodb:PutItem",
"dynamodb:Scan", "dynamodb:Query", "dynamodb:UpdateItem",
"dynamodb:BatchWriteItem", "dynamodb:BatchGetItem",
"dynamodb:DescribeTable", "dynamodb:ConditionCheckItem"
],
"Resource": [
{
"Fn::Sub": [
"arn:${AWS::Partition}:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${tableName}",
{ "tableName": { "Ref": "DeviceRegistryTable" } }
]
},
{
"Fn::Sub": [
"arn:${AWS::Partition}:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${tableName}/index/*",
{ "tableName": { "Ref": "DeviceRegistryTable" } }
]
}
],
"Effect": "Allow"
}
Plus the AWSLambdaBasicExecutionRole managed policy and an assume-role
document naming lambda.amazonaws.com, neither of which I typed either.
Five functions. Five roles I did not write, five trust policies I did not get wrong, and five policy documents that spell partition and region correctly because a transform did it rather than me at eleven at night. This is the largest single thing SAM does for this stack and it is not close.
The permission that exists nowhere I can read
The Alexa trigger is four lines of Events: and it becomes a
Lambda::Permission with alexa-connectedhome.amazon.com as the principal and
the skill id as the event source token. I did not know that principal existed.
I still would not be able to type it from memory.
That is the deal SAM offers, stated plainly: it will get right the things you do not know, in exchange for those things not appearing in your repository. For six months the answer to "why can Alexa call this function" was not in any file I owned. It was in a macro that AWS runs and I do not.
I am fine with that trade. I am less fine with having taken six months to notice I had made it.
Where the transform gives you nothing
Roughly two-thirds of template.yaml is not SAM at all.
AWS::Serverless::SimpleTable is a hash key and nothing else — no sort key, no
secondary index — so the device registry is a long-hand AWS::DynamoDB::Table.
There is no serverless alarm, so the four CloudWatch alarms are long-hand too,
about twenty-five lines each, and they are the single largest thing in the
file. Anything without a SAM equivalent is plain CloudFormation sitting in the
same document, which means the template is bilingual: a few dense serverless
resources next to a long tail of ordinary ones.
So "with SAM or without it" is not a choice between 231 lines and 704. It is a choice between 231 and something like 420 — the alarms and the table are the same either way. The saving is real and it is concentrated almost entirely in IAM and in event wiring.
The CLI is the other half, and it is the half I would miss
Without the transform I would still have aws cloudformation package and
deploy, which is most of what sam deploy is. What I would not have is
sam build, which reads CodeUri, resolves requirements.txt into
.aws-sam/build/<Function>/, and rewrites the template to point at the built
artefact. I would be doing that in a shell script, badly, and the shell script
would be the thing that broke.
And I would not have this:
sam local invoke SkillFunction --event events/discovery.json
which runs the handler in the Lambda image with the same handler contract and tells me in two seconds that my discovery response is malformed. Getting that answer from a deploy takes ninety.
If someone made me choose one half of SAM to keep, I would keep the CLI and hand-write the roles.
What it costs
Two things, both small, both real.
The diff you review is not the diff you deploy. A one-line change to a policy template is a fifty-line change to the document CloudFormation sees, and code review sees the one line. Most of the time that is the point. Occasionally it is how a permission gets wider than anyone intended without anything in the pull request looking different.
And stack events name resources you never typed. When a deploy fails on
SensorIngestFunctionRole, there is no SensorIngestFunctionRole to grep for.
You learn the naming convention — logical id plus Role, logical id plus event
name for rules — and after that it is fine, but it is a convention you have to
learn rather than a fact you can look up in your own repository.
Where I landed
Printing the processed template took ten minutes and I should have done it in
January. It is now a make target, and the output is committed under
infra/generated/ so that the expansion shows up in diffs even though nobody
edits it.
That is the change I would recommend to anyone using SAM: keep it, and stop letting the part it writes be invisible.
Written by
Deyan Peev
Founding Engineer · Sofia, Bulgaria


