engineering-handbook/lambda-template.md
Adam Moussa affa1a64f8
Some checks failed
ci / ci / ci (push) Has been cancelled
docs(cd): separate terraform infra from github app deploys (#46)
Make HCP Terraform plus GitHub Actions content CD the default for new
workloads, and keep SAM/CDK documented as the remaining path.
2026-09-15 22:05:33 +00:00

3.3 KiB

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 and aws-infrastructure.md.

Project Structure

my-stack/
├── template.yaml
├── samconfig.toml.example
├── src/
│   └── handler/
│       ├── app.py
│       └── requirements.txt
└── .gitignore

See sam-project-layout.md for the full directory convention.

template.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

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 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.