engineering-handbook/lambda-template.md
Adam Moussa 651dff5dd3
Add Bedrock, dev-env, and Lambda template pages (#7)
Adds three handbook pages covering conventions that were previously
scattered across feedback memories or rederived from scratch each
time:

- bedrock.md captures the cross-region inference profile requirement
  for Claude 4.x Bedrock Agents and the alias-version pinning gotcha,
  plus the IAM resource pattern and the KB Docker requirement.
- dev-environment.md documents the workstation directory layout,
  pyenv/Node conventions, the macOS launchd/TCC sandbox gotcha, and
  cleanup cadence.
- lambda-template.md provides a minimal SAM scaffold that follows the
  Lambda defaults already in aws-infrastructure.md (Python 3.12,
  arm64, explicit 60-day log retention, scoped Secrets Manager
  access, module-level secret cache).

Also extends two existing pages:

- sam-project-layout.md gains a Lambda Layers section with the
  BuildMethod nesting pattern that caused a ~22-hour production
  outage when violated.
- naming-conventions.md adds a Legacy Stacks note acknowledging that
  pre-convention PascalCase stacks (SeaHavenDoorUnlockStack,
  WorkorderIngestStack) stay as-is rather than risk stack
  replacement.
2026-05-14 19:50:37 -04:00

113 lines
3.2 KiB
Markdown

# Lambda Starter Template
Minimal SAM scaffold for a new Python Lambda. Drop into `template.yaml` and adjust names. Follows the defaults in [aws-infrastructure.md](aws-infrastructure.md#lambda-defaults).
## Project Structure
```
my-stack/
├── template.yaml
├── samconfig.toml.example
├── src/
│ └── handler/
│ ├── app.py
│ └── requirements.txt
└── .gitignore
```
See [sam-project-layout.md](sam-project-layout.md) for the full directory convention.
## template.yaml
```yaml
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: my-stack — one-line purpose
Globals:
Function:
Runtime: python3.12
Architecture: arm64
Timeout: 30
MemorySize: 256
LoggingConfig:
LogFormat: JSON
Resources:
HandlerFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: my-stack-handler
CodeUri: src/handler/
Handler: app.handler
Environment:
Variables:
CONFIG_SECRET: my-stack/config
Policies:
- AWSLambdaBasicExecutionRole
- Statement:
- Effect: Allow
Action: secretsmanager:GetSecretValue
Resource: !Sub arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:my-stack/*
HandlerLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub /aws/lambda/${HandlerFunction}
RetentionInDays: 60
Outputs:
HandlerArn:
Description: Handler Lambda ARN
Value: !GetAtt HandlerFunction.Arn
```
Key points:
- `Globals.Function` sets runtime, architecture, and JSON logging once for the whole template.
- The `LogGroup` is declared **explicitly** with `RetentionInDays: 60`. Omit it and CloudWatch creates the log group on first invocation with no retention — logs accumulate forever.
- IAM scopes `secretsmanager:GetSecretValue` to the stack's secret prefix only. Add specific permissions as needed; never use `AdministratorAccess`.
## src/handler/app.py
```python
import json
import os
import boto3
_secrets_client = boto3.client("secretsmanager")
_config = None
def _get_config():
global _config
if _config is None:
resp = _secrets_client.get_secret_value(SecretId=os.environ["CONFIG_SECRET"])
_config = json.loads(resp["SecretString"])
return _config
def handler(event, context):
config = _get_config()
# ... your logic ...
return {"statusCode": 200, "body": json.dumps({"ok": True})}
```
The module-level `_config` global caches the secret across warm invocations. The first call per cold start hits Secrets Manager; subsequent calls reuse the cached value. See [secrets-and-config.md](secrets-and-config.md) for the rationale.
## src/handler/requirements.txt
Keep this file in every function directory even when empty — SAM looks for it during `sam build`.
```
# Per-function dependencies. Leave empty if the function uses only boto3 and stdlib.
```
## Naming Reminders
- `FunctionName` must be kebab-case and start with the stack name (`my-stack-handler`).
- Secret IDs use `stack-name/secret-name`.
- Stack name itself is set in `samconfig.toml`, not the template — match the repo name.
See [naming-conventions.md](naming-conventions.md).