engineering-handbook/lambda-template.md

114 lines
3.3 KiB
Markdown
Raw Normal View History

# Lambda Starter Template
Remaining SAM scaffold for an existing Python Lambda stack. New functions belong in an HCP Terraform repo; see [terraform-project-layout.md](terraform-project-layout.md) and [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).