From 651dff5dd3a2fafd2f35c1e82a6558770934c56c Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Thu, 14 May 2026 19:50:37 -0400 Subject: [PATCH] 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. --- README.md | 3 ++ bedrock.md | 59 ++++++++++++++++++++++ dev-environment.md | 55 ++++++++++++++++++++ lambda-template.md | 113 ++++++++++++++++++++++++++++++++++++++++++ naming-conventions.md | 9 ++++ sam-project-layout.md | 52 ++++++++++++++++++- 6 files changed, 290 insertions(+), 1 deletion(-) create mode 100644 bedrock.md create mode 100644 dev-environment.md create mode 100644 lambda-template.md diff --git a/README.md b/README.md index fa1e284..ac69c89 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ Engineering conventions and best practices for Sea Haven Industries. ## Contents - [Naming Conventions](naming-conventions.md) -- kebab-case everywhere, no exceptions +- [Development Environment](dev-environment.md) -- workstation directory layout, pyenv, Node, launchd/TCC - [Git Workflow](git-workflow.md) -- feature branches, incremental commits, deploy-then-merge - [Commit Messages](commit-messages.md) -- imperative mood, 50/72 rule, explain "why" - [Pull Requests](pull-requests.md) -- scope, title, description format, merge strategy @@ -12,8 +13,10 @@ Engineering conventions and best practices for Sea Haven Industries. - [GitHub Standards](github-standards.md) -- branch defaults, repo hygiene, Dependabot - [AWS Infrastructure](aws-infrastructure.md) -- SAM vs CDK, Lambda defaults, CloudFormation - [SAM Project Layout](sam-project-layout.md) -- standard directory structure for serverless projects +- [Lambda Starter Template](lambda-template.md) -- minimal SAM scaffold for a new Python Lambda - [Secrets and Configuration](secrets-and-config.md) -- Secrets Manager vs SSM Parameter Store - [CI/CD Pipelines](cicd.md) -- every deployable repo gets a pipeline, no manual deploys +- [Bedrock](bedrock.md) -- cross-region inference profiles, alias pinning, KB Docker requirement - [Git Hooks](hooks/) -- recommended pre-push and pre-commit hooks - [Scripts](scripts/) -- repo provisioning, automation tooling - [CDK Constructs](constructs/) -- shared VPN EC2 instance construct and other reusable patterns diff --git a/bedrock.md b/bedrock.md new file mode 100644 index 0000000..bb65871 --- /dev/null +++ b/bedrock.md @@ -0,0 +1,59 @@ +# AWS Bedrock + +Conventions and gotchas for using AWS Bedrock — Agents, Knowledge Bases, and model invocations. + +## Foundation Model IDs + +For **Claude 4.x models on Bedrock Agents**, always use the cross-region inference profile ID, not the direct model ID. + +| Use | Example | +|---|---| +| Direct invocation (`bedrock-runtime`) | `anthropic.claude-sonnet-4-5-20250929-v1:0` | +| Bedrock Agent `foundationModel` | `us.anthropic.claude-sonnet-4-5-20250929-v1:0` | + +The direct model ID is only valid for `bedrock-runtime` invocations. Using it on `CfnAgent.foundationModel` makes the agent prepare successfully but throws `ResourceNotFoundException` at invocation time. + +## IAM Permissions + +Bedrock Agents need both the underlying foundation model and the cross-region inference profile in their IAM policy: + +```json +{ + "Effect": "Allow", + "Action": "bedrock:InvokeModel", + "Resource": [ + "arn:aws:bedrock:*::foundation-model/anthropic.claude-sonnet-4-5-20250929-v1:0", + "arn:aws:bedrock:us-east-1::inference-profile/us.anthropic.claude-sonnet-4-5-20250929-v1:0" + ] +} +``` + +The foundation model ARN uses the wildcard region (`*`) since cross-region inference can route to any of the profile's regions. + +## Alias Version Pinning + +When updating an agent's `foundationModel` or `instruction` via CDK or CloudFormation, the alias does **not** automatically re-point to the new version. + +CloudFormation only updates the `DRAFT` version when `CfnAgent` changes. The alias stays pinned to the previous version unless the alias resource itself changes. + +Force version rotation by bumping the alias `description` in the same change set: + +```typescript +new bedrock.CfnAgentAlias(this, 'Alias', { + agentAliasName: 'live', + agentId: agent.attrAgentId, + description: `bumped 2026-05-14 — model update`, // change this to force a new version +}); +``` + +## Knowledge Bases + +The `@cdklabs/generative-ai-cdk-constructs` `VectorKnowledgeBase` L2 construct requires Docker Desktop on the deploying machine — it uses `Code.fromDockerBuild` for a custom resource Lambda. CI runners need Docker available. + +For Knowledge Base S3 buckets, follow the standard tagging convention from [AWS Infrastructure](aws-infrastructure.md#s3): `Purpose` and `ManagedBy` tags. + +## Action Groups + +Action group Lambdas receive a Bedrock-specific event shape — they are not invoked as plain Lambda function URLs. The `apiPath` and `httpMethod` fields in the event identify which OpenAPI operation triggered the call. Validate them before dispatching. + +Document the action group's OpenAPI schema in the same repo as the Lambda code; CloudFormation stores it inline. diff --git a/dev-environment.md b/dev-environment.md new file mode 100644 index 0000000..a727fb1 --- /dev/null +++ b/dev-environment.md @@ -0,0 +1,55 @@ +# Development Environment + +Local workstation conventions for Sea Haven engineering work. + +## Directory Layout + +``` +~/ +├── Documents/ +│ ├── repositories/ # Git repos only — no loose files, scripts, or data +│ ├── working-docs/ # Cross-cutting docs, audits, drafts, reference material +│ └── ... +├── Desktop/ # Temporary workspace — nothing permanent +├── Downloads/ # Transient — delete installers after use +├── .ssh/ # All keys, certs, PEMs, credential files (chmod 600) +└── (standard macOS dirs) +``` + +- `repositories/` contains only git-tracked project directories. No loose scripts, CSVs, or data files. +- `working-docs/` is for documentation that spans multiple repos — not a git repo. +- Never store secrets in `repositories/` or `Downloads/`. +- `.env` files are gitignored and local-only. +- No credentials in Notion, Confluence, Slack, or other plaintext docs. + +## Python + +Use **pyenv**, not Homebrew Python. Homebrew auto-upgrades Python on minor releases (3.13 → 3.14) which breaks compiled tools like SAM CLI and pip packages with C extensions. + +- Global default: Python 3.12 (matches Lambda runtime — see [AWS Infrastructure](aws-infrastructure.md#lambda-defaults)) +- SAM CLI installed via pip under pyenv, not via Homebrew +- `pyenv init` belongs in `~/.zshrc` + +When troubleshooting Python issues, check that pyenv is active before anything else. `which python` should point inside `~/.pyenv/`. + +## Node.js + +- Local: Node 24 / npm 11 (generates `lockfileVersion: 3`) +- Lambda: Node 22.x or 24.x (set explicitly in IaC) +- Reusable workflows: always pass `node-version: "24"` (the default is 22 / npm 10, which can fail `npm ci` on npm 11 lockfiles) + +## macOS launchd and the TCC Sandbox + +macOS TCC blocks launchd from reading `~/Documents/`, `~/Desktop/`, `~/Downloads/`, and iCloud folders. A script in `~/Documents/repositories/...` fails silently when launched by launchd even when `chmod +x` and manual invocation both succeed. The failure surfaces as `LastExitStatus = 32256` (exit 126) in `launchctl list`. + +Any launchd agent or scheduled automation must reference a script outside TCC-protected directories. Keep the source in the repo for version control, and install the runnable copy to `~/.local/bin/` or `~/Library/Application Support//`. Do not grant Full Disk Access to `/bin/bash`. + +If you edit the repo source, re-copy to the installed location — launchd reads the installed copy. + +## Cleanup Cadence + +| Cadence | Tasks | +|---|---| +| Weekly | Clear Desktop of anything older than 2 weeks | +| Monthly | Check Downloads for stale installers; check `repositories/` for loose files; prune old VS Code extension versions | +| Quarterly | Audit Docker (`docker system df`), npm/yarn caches; review GitHub repos for archival candidates | diff --git a/lambda-template.md b/lambda-template.md new file mode 100644 index 0000000..8094c49 --- /dev/null +++ b/lambda-template.md @@ -0,0 +1,113 @@ +# 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). diff --git a/naming-conventions.md b/naming-conventions.md index 07a9dae..cb83e75 100644 --- a/naming-conventions.md +++ b/naming-conventions.md @@ -37,3 +37,12 @@ new MyStack(app, 'MyStack', { | `expenseApprovalBot` | `expense-approval-bot` | camelCase | | `Expense-Approval-Bot` | `expense-approval-bot` | Mixed case | | `feature/AddParser` | `feature/add-parser` | PascalCase in branch | + +## Legacy Stacks + +A handful of stacks predate this convention and remain PascalCase because renaming would require stack replacement (data loss, deploy windows). Examples in active use: + +- `SeaHavenDoorUnlockStack` +- `WorkorderIngestStack` + +Do not rename these solely to enforce kebab-case if doing so means tearing down and recreating production resources. New stacks must follow the convention; legacy stacks may keep their names until a planned migration brings them in line. diff --git a/sam-project-layout.md b/sam-project-layout.md index fadeed0..1122b7e 100644 --- a/sam-project-layout.md +++ b/sam-project-layout.md @@ -35,7 +35,57 @@ One directory per Lambda function. Each contains its own handler (`app.py`) and ### `src/shared/` -Optional. Used for code shared across multiple functions, typically deployed as a Lambda layer. +Optional. Used for code shared across multiple functions, typically deployed as a Lambda layer. See the next section for layout details. + +## Lambda Layers + +When sharing code across functions via a layer, the source layout matters because SAM transforms `ContentUri` depending on whether `BuildMethod` is set. + +### Correct layout (with `BuildMethod`) + +``` +src/shared/ +├── shared/ # Package goes here directly — SAM wraps in python/ at build time +│ ├── __init__.py +│ └── utils.py +└── requirements.txt # Layer-level pip dependencies +``` + +Template: + +```yaml +SharedLayer: + Type: AWS::Serverless::LayerVersion + Properties: + LayerName: my-stack-shared + ContentUri: src/shared/ + CompatibleRuntimes: + - python3.12 + CompatibleArchitectures: + - arm64 + Metadata: + BuildMethod: python3.12 + BuildArchitecture: arm64 +``` + +### BuildMethod nesting gotcha + +With `BuildMethod: python3.12`, SAM copies `ContentUri` into a `python/` subdirectory during build, then pip-installs `requirements.txt` deps into that same `python/` directory. + +Do **not** include a `python/` wrapper in your source — SAM adds it. The wrong layout: + +``` +src/shared/ +├── python/ # SAM wraps this again → python/python/shared/ — module unreachable +│ └── shared/ +└── requirements.txt +``` + +A real production incident (~22 hours of outage) traced to this exact pattern when a refactor moved layer code under an extra `python/` directory. + +### Without BuildMethod (raw zip) + +If the layer has no pip dependencies and you omit `BuildMethod`, SAM zips `ContentUri` as-is — you DO need the `python/` wrapper. Reserve raw zip for layers that ship only Python source. ## Standard .gitignore