lambda-forge-cli-aws 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lambda_forge_cli_aws-1.0.0.dist-info/METADATA +352 -0
- lambda_forge_cli_aws-1.0.0.dist-info/RECORD +80 -0
- lambda_forge_cli_aws-1.0.0.dist-info/WHEEL +4 -0
- lambda_forge_cli_aws-1.0.0.dist-info/entry_points.txt +2 -0
- lambda_forge_cli_aws-1.0.0.dist-info/licenses/LICENSE +189 -0
- lambdaforge/__init__.py +10 -0
- lambdaforge/bedrock_client.py +109 -0
- lambdaforge/cli.py +428 -0
- lambdaforge/deploy.py +308 -0
- lambdaforge/logs.py +185 -0
- lambdaforge/renderer.py +72 -0
- lambdaforge/spec_generator.py +171 -0
- lambdaforge/status.py +111 -0
- lambdaforge/templates.py +106 -0
- lambdaforge/upgrade.py +231 -0
- templates/python-api/cookiecutter.json +12 -0
- templates/python-api/{{cookiecutter.project_slug}}/.github/workflows/dependabot.yml +22 -0
- templates/python-api/{{cookiecutter.project_slug}}/.github/workflows/deploy.yml +90 -0
- templates/python-api/{{cookiecutter.project_slug}}/.gitignore +65 -0
- templates/python-api/{{cookiecutter.project_slug}}/Dockerfile +11 -0
- templates/python-api/{{cookiecutter.project_slug}}/README.md +130 -0
- templates/python-api/{{cookiecutter.project_slug}}/infrastructure/app.js +26 -0
- templates/python-api/{{cookiecutter.project_slug}}/infrastructure/cdk.json +20 -0
- templates/python-api/{{cookiecutter.project_slug}}/infrastructure/lib/stack.js +155 -0
- templates/python-api/{{cookiecutter.project_slug}}/infrastructure/package.json +17 -0
- templates/python-api/{{cookiecutter.project_slug}}/pyproject.toml +66 -0
- templates/python-api/{{cookiecutter.project_slug}}/requirements-dev.txt +8 -0
- templates/python-api/{{cookiecutter.project_slug}}/requirements.txt +3 -0
- templates/python-api/{{cookiecutter.project_slug}}/src/handler.py +93 -0
- templates/python-api/{{cookiecutter.project_slug}}/src/utils/logger.py +39 -0
- templates/python-api/{{cookiecutter.project_slug}}/tests/conftest.py +22 -0
- templates/python-api/{{cookiecutter.project_slug}}/tests/test_handler.py +68 -0
- templates/python-cron/cookiecutter.json +13 -0
- templates/python-cron/{{cookiecutter.project_slug}}/.gitignore +65 -0
- templates/python-cron/{{cookiecutter.project_slug}}/Dockerfile +11 -0
- templates/python-cron/{{cookiecutter.project_slug}}/infrastructure/app.js +25 -0
- templates/python-cron/{{cookiecutter.project_slug}}/infrastructure/cdk.json +3 -0
- templates/python-cron/{{cookiecutter.project_slug}}/infrastructure/lib/stack.js +55 -0
- templates/python-cron/{{cookiecutter.project_slug}}/infrastructure/package.json +17 -0
- templates/python-cron/{{cookiecutter.project_slug}}/pyproject.toml +56 -0
- templates/python-cron/{{cookiecutter.project_slug}}/requirements.txt +2 -0
- templates/python-cron/{{cookiecutter.project_slug}}/src/handler.py +53 -0
- templates/python-cron/{{cookiecutter.project_slug}}/tests/conftest.py +22 -0
- templates/python-cron/{{cookiecutter.project_slug}}/tests/test_handler.py +48 -0
- templates/python-queue/cookiecutter.json +12 -0
- templates/python-queue/{{cookiecutter.project_slug}}/.gitignore +65 -0
- templates/python-queue/{{cookiecutter.project_slug}}/Dockerfile +11 -0
- templates/python-queue/{{cookiecutter.project_slug}}/infrastructure/app.js +25 -0
- templates/python-queue/{{cookiecutter.project_slug}}/infrastructure/cdk.json +3 -0
- templates/python-queue/{{cookiecutter.project_slug}}/infrastructure/lib/stack.js +79 -0
- templates/python-queue/{{cookiecutter.project_slug}}/infrastructure/package.json +17 -0
- templates/python-queue/{{cookiecutter.project_slug}}/pyproject.toml +56 -0
- templates/python-queue/{{cookiecutter.project_slug}}/requirements.txt +2 -0
- templates/python-queue/{{cookiecutter.project_slug}}/src/handler.py +64 -0
- templates/python-queue/{{cookiecutter.project_slug}}/tests/conftest.py +22 -0
- templates/python-queue/{{cookiecutter.project_slug}}/tests/test_handler.py +60 -0
- templates/python-s3/cookiecutter.json +13 -0
- templates/python-s3/{{cookiecutter.project_slug}}/.gitignore +65 -0
- templates/python-s3/{{cookiecutter.project_slug}}/Dockerfile +11 -0
- templates/python-s3/{{cookiecutter.project_slug}}/infrastructure/app.js +25 -0
- templates/python-s3/{{cookiecutter.project_slug}}/infrastructure/cdk.json +3 -0
- templates/python-s3/{{cookiecutter.project_slug}}/infrastructure/lib/stack.js +89 -0
- templates/python-s3/{{cookiecutter.project_slug}}/infrastructure/package.json +17 -0
- templates/python-s3/{{cookiecutter.project_slug}}/pyproject.toml +56 -0
- templates/python-s3/{{cookiecutter.project_slug}}/requirements.txt +2 -0
- templates/python-s3/{{cookiecutter.project_slug}}/src/handler.py +86 -0
- templates/python-s3/{{cookiecutter.project_slug}}/tests/conftest.py +22 -0
- templates/python-s3/{{cookiecutter.project_slug}}/tests/test_handler.py +71 -0
- templates/python-stream/cookiecutter.json +12 -0
- templates/python-stream/{{cookiecutter.project_slug}}/.gitignore +65 -0
- templates/python-stream/{{cookiecutter.project_slug}}/Dockerfile +11 -0
- templates/python-stream/{{cookiecutter.project_slug}}/infrastructure/app.js +25 -0
- templates/python-stream/{{cookiecutter.project_slug}}/infrastructure/cdk.json +3 -0
- templates/python-stream/{{cookiecutter.project_slug}}/infrastructure/lib/stack.js +81 -0
- templates/python-stream/{{cookiecutter.project_slug}}/infrastructure/package.json +17 -0
- templates/python-stream/{{cookiecutter.project_slug}}/pyproject.toml +56 -0
- templates/python-stream/{{cookiecutter.project_slug}}/requirements.txt +2 -0
- templates/python-stream/{{cookiecutter.project_slug}}/src/handler.py +69 -0
- templates/python-stream/{{cookiecutter.project_slug}}/tests/conftest.py +22 -0
- templates/python-stream/{{cookiecutter.project_slug}}/tests/test_handler.py +64 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
name: Deploy {{ cookiecutter.project_name }}
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
id-token: write
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
env:
|
|
13
|
+
AWS_REGION: us-east-1
|
|
14
|
+
STACK_NAME: {{ cookiecutter.project_slug }}-prod
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
test:
|
|
18
|
+
name: Test + Lint
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
|
|
23
|
+
- uses: actions/setup-python@v5
|
|
24
|
+
with:
|
|
25
|
+
python-version: "3.12"
|
|
26
|
+
cache: "pip"
|
|
27
|
+
|
|
28
|
+
- name: Install dependencies
|
|
29
|
+
run: |
|
|
30
|
+
python -m pip install --upgrade pip
|
|
31
|
+
pip install -r requirements-dev.txt
|
|
32
|
+
|
|
33
|
+
- name: Lint (ruff)
|
|
34
|
+
run: ruff check .
|
|
35
|
+
|
|
36
|
+
- name: Type check (mypy)
|
|
37
|
+
run: mypy src/
|
|
38
|
+
|
|
39
|
+
- name: Test (pytest)
|
|
40
|
+
run: pytest --cov-report=xml
|
|
41
|
+
env:
|
|
42
|
+
AWS_DEFAULT_REGION: us-east-1
|
|
43
|
+
AWS_ACCESS_KEY_ID: test
|
|
44
|
+
AWS_SECRET_ACCESS_KEY: test
|
|
45
|
+
|
|
46
|
+
- name: Upload coverage
|
|
47
|
+
uses: codecov/codecov-action@v3
|
|
48
|
+
if: always()
|
|
49
|
+
with:
|
|
50
|
+
file: coverage.xml
|
|
51
|
+
|
|
52
|
+
deploy:
|
|
53
|
+
name: Deploy to AWS
|
|
54
|
+
needs: test
|
|
55
|
+
runs-on: ubuntu-latest
|
|
56
|
+
environment: production
|
|
57
|
+
steps:
|
|
58
|
+
- uses: actions/checkout@v4
|
|
59
|
+
|
|
60
|
+
- name: Setup Node.js
|
|
61
|
+
uses: actions/setup-node@v4
|
|
62
|
+
with:
|
|
63
|
+
node-version: "20"
|
|
64
|
+
|
|
65
|
+
- uses: actions/setup-python@v5
|
|
66
|
+
with:
|
|
67
|
+
python-version: "3.12"
|
|
68
|
+
|
|
69
|
+
{% raw %}
|
|
70
|
+
- name: Configure AWS credentials (OIDC)
|
|
71
|
+
uses: aws-actions/configure-aws-credentials@v4
|
|
72
|
+
with:
|
|
73
|
+
role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
|
|
74
|
+
aws-region: ${{ env.AWS_REGION }}
|
|
75
|
+
|
|
76
|
+
- name: Install CDK
|
|
77
|
+
run: npm install -g aws-cdk
|
|
78
|
+
|
|
79
|
+
- name: Install CDK dependencies
|
|
80
|
+
working-directory: infrastructure
|
|
81
|
+
run: npm install
|
|
82
|
+
|
|
83
|
+
- name: CDK Diff
|
|
84
|
+
working-directory: infrastructure
|
|
85
|
+
run: cdk diff ${{ env.STACK_NAME }} || true
|
|
86
|
+
|
|
87
|
+
- name: CDK Deploy
|
|
88
|
+
working-directory: infrastructure
|
|
89
|
+
run: cdk deploy ${{ env.STACK_NAME }} --require-approval never
|
|
90
|
+
{% endraw %}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib64/
|
|
14
|
+
parts/
|
|
15
|
+
sdist/
|
|
16
|
+
var/
|
|
17
|
+
wheels/
|
|
18
|
+
*.egg-info/
|
|
19
|
+
.installed.cfg
|
|
20
|
+
*.egg
|
|
21
|
+
MANIFEST
|
|
22
|
+
|
|
23
|
+
# Virtual environments
|
|
24
|
+
.venv/
|
|
25
|
+
venv/
|
|
26
|
+
env/
|
|
27
|
+
ENV/
|
|
28
|
+
|
|
29
|
+
# Testing
|
|
30
|
+
.pytest_cache/
|
|
31
|
+
.coverage
|
|
32
|
+
.coverage.*
|
|
33
|
+
htmlcov/
|
|
34
|
+
.tox/
|
|
35
|
+
.nox/
|
|
36
|
+
coverage.xml
|
|
37
|
+
*.cover
|
|
38
|
+
|
|
39
|
+
# Type checking
|
|
40
|
+
.mypy_cache/
|
|
41
|
+
.ruff_cache/
|
|
42
|
+
|
|
43
|
+
# IDEs
|
|
44
|
+
.idea/
|
|
45
|
+
.vscode/
|
|
46
|
+
*.swp
|
|
47
|
+
*.swo
|
|
48
|
+
.DS_Store
|
|
49
|
+
|
|
50
|
+
# CDK
|
|
51
|
+
cdk.out/
|
|
52
|
+
cdk.context.json
|
|
53
|
+
|
|
54
|
+
# Lambda
|
|
55
|
+
*.zip
|
|
56
|
+
|
|
57
|
+
# Environment
|
|
58
|
+
.env
|
|
59
|
+
.env.local
|
|
60
|
+
.env.*.local
|
|
61
|
+
|
|
62
|
+
# Secrets
|
|
63
|
+
*.pem
|
|
64
|
+
*.key
|
|
65
|
+
secrets.yml
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
FROM public.ecr.aws/lambda/python:3.12-arm64
|
|
2
|
+
|
|
3
|
+
# Install dependencies first for better layer caching
|
|
4
|
+
COPY requirements.txt ${LAMBDA_TASK_ROOT}/
|
|
5
|
+
RUN pip install --no-cache-dir -r requirements.txt
|
|
6
|
+
|
|
7
|
+
# Copy function code
|
|
8
|
+
COPY src/ ${LAMBDA_TASK_ROOT}/
|
|
9
|
+
|
|
10
|
+
# Set the handler
|
|
11
|
+
CMD [ "handler.handler" ]
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# {{ cookiecutter.project_name }}
|
|
2
|
+
|
|
3
|
+
> {{ cookiecutter.description }}
|
|
4
|
+
|
|
5
|
+
Serverless API built with AWS Lambda + API Gateway, generated by [LambdaForge](https://github.com/lambdaforge/lambdaforge).
|
|
6
|
+
|
|
7
|
+
## Architecture
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Client -> API Gateway (REST) -> Lambda (ARM64) -> DynamoDB
|
|
11
|
+
|
|
|
12
|
+
-> CloudWatch Logs/Metrics/Traces (X-Ray)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
- **Runtime**: Python 3.12 (ARM64 - 20% cheaper, 19% faster than x86_64)
|
|
16
|
+
- **Observability**: aws-lambda-powertools (structured logs, metrics, traces)
|
|
17
|
+
- **IaC**: AWS CDK TypeScript
|
|
18
|
+
- **Cost**: Designed to fit in AWS Free Tier (<1M req/month)
|
|
19
|
+
|
|
20
|
+
## Project Structure
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
{{ cookiecutter.project_slug }}/
|
|
24
|
+
├── src/
|
|
25
|
+
│ ├── handler.py # Lambda entry point
|
|
26
|
+
│ └── utils/
|
|
27
|
+
│ └── logger.py # Powertools logger config
|
|
28
|
+
├── tests/
|
|
29
|
+
│ ├── conftest.py # pytest fixtures (moto for AWS mocking)
|
|
30
|
+
│ └── test_handler.py # Unit + integration tests
|
|
31
|
+
├── infrastructure/
|
|
32
|
+
│ ├── app.js # CDK app entry (JavaScript)
|
|
33
|
+
│ ├── cdk.json # CDK config
|
|
34
|
+
│ ├── package.json # CDK dependencies
|
|
35
|
+
│ └── lib/
|
|
36
|
+
│ └── stack.js # CDK stack (multi-env: dev/staging/prod)
|
|
37
|
+
├── .kiro/
|
|
38
|
+
│ └── specs/ # Generated Kiro specs
|
|
39
|
+
├── Dockerfile # Container image (used by CDK)
|
|
40
|
+
├── pyproject.toml # Dependencies + tooling config
|
|
41
|
+
└── README.md
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Local Development
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Install dependencies
|
|
48
|
+
pip install -r requirements-dev.txt
|
|
49
|
+
|
|
50
|
+
# Run tests (uses moto for AWS mocking - no real AWS calls)
|
|
51
|
+
pytest
|
|
52
|
+
|
|
53
|
+
# Run with type checking + linting
|
|
54
|
+
ruff check .
|
|
55
|
+
mypy src/
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Deployment
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# First time: bootstrap CDK
|
|
62
|
+
npx cdk bootstrap aws://ACCOUNT_ID/REGION
|
|
63
|
+
|
|
64
|
+
# Deploy
|
|
65
|
+
npx cdk deploy
|
|
66
|
+
|
|
67
|
+
# Or via LambdaForge
|
|
68
|
+
lambdaforge deploy --stack {{ cookiecutter.project_slug }}-dev
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## API
|
|
72
|
+
|
|
73
|
+
### `POST /items`
|
|
74
|
+
|
|
75
|
+
Create a new item.
|
|
76
|
+
|
|
77
|
+
**Request body**:
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"key": "user-123",
|
|
81
|
+
"data": {
|
|
82
|
+
"name": "Ada Lovelace",
|
|
83
|
+
"role": "engineer"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Response** (201 Created):
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"key": "user-123",
|
|
92
|
+
"data": { "name": "Ada Lovelace", "role": "engineer" },
|
|
93
|
+
"createdAt": "2026-07-23T10:12:43.123Z"
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### `GET /items/{key}`
|
|
98
|
+
|
|
99
|
+
Retrieve an item by key.
|
|
100
|
+
|
|
101
|
+
**Response** (200 OK):
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"key": "user-123",
|
|
105
|
+
"data": { "name": "Ada Lovelace", "role": "engineer" },
|
|
106
|
+
"createdAt": "2026-07-23T10:12:43.123Z",
|
|
107
|
+
"updatedAt": "2026-07-23T10:12:43.123Z"
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Observability
|
|
112
|
+
|
|
113
|
+
- **Logs**: CloudWatch Logs (`/aws/lambda/{{ cookiecutter.project_slug }}`)
|
|
114
|
+
- **Metrics**: CloudWatch Metrics (custom namespace `{{ cookiecutter.project_slug }}`)
|
|
115
|
+
- **Traces**: X-Ray (end-to-end through API Gateway + Lambda + DynamoDB)
|
|
116
|
+
- **Dashboard**: CloudWatch Dashboard created by CDK stack
|
|
117
|
+
|
|
118
|
+
## Cost Estimate (Free Tier)
|
|
119
|
+
|
|
120
|
+
| Resource | Free Tier | Estimated |
|
|
121
|
+
|----------|-----------|-----------|
|
|
122
|
+
| Lambda | 1M req + 400k GB-s | $0 (under) |
|
|
123
|
+
| API Gateway | 1M calls | $0 (under) |
|
|
124
|
+
| DynamoDB | 25 GB + 25 WCU/RCU | $0 (under) |
|
|
125
|
+
|
|
126
|
+
Beyond Free Tier (worst case 10M req/month): **~$5/month**
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
MIT © {{ cookiecutter.author_name }}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const cdk = require('aws-cdk-lib');
|
|
2
|
+
const { {{ cookiecutter.project_name|replace('-', '')|capitalize }}Stack } = require('./lib/stack');
|
|
3
|
+
|
|
4
|
+
const app = new cdk.App();
|
|
5
|
+
|
|
6
|
+
// Environment from CDK context (default: dev)
|
|
7
|
+
const envName = app.node.tryGetContext('env') || 'dev';
|
|
8
|
+
|
|
9
|
+
new {{ cookiecutter.project_name|replace('-', '')|capitalize }}Stack(app, `{{ cookiecutter.project_name }}-${envName}`, {
|
|
10
|
+
stackName: `{{ cookiecutter.project_name }}-${envName}`,
|
|
11
|
+
env: {
|
|
12
|
+
account: process.env.CDK_DEFAULT_ACCOUNT,
|
|
13
|
+
region: process.env.CDK_DEFAULT_REGION || '{{ cookiecutter.aws_region }}',
|
|
14
|
+
},
|
|
15
|
+
description: `{{ cookiecutter.description }} (${envName})`,
|
|
16
|
+
tags: {
|
|
17
|
+
Project: '{{ cookiecutter.project_name }}',
|
|
18
|
+
Environment: envName,
|
|
19
|
+
ManagedBy: 'LambdaForge',
|
|
20
|
+
CostCenter: '{{ cookiecutter.project_name }}',
|
|
21
|
+
},
|
|
22
|
+
// Pass environment to the stack
|
|
23
|
+
envName: envName,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
app.synth();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"app": "node app.js",
|
|
3
|
+
"watch": {
|
|
4
|
+
"include": ["**"],
|
|
5
|
+
"exclude": [
|
|
6
|
+
"README.md",
|
|
7
|
+
"cdk*.json",
|
|
8
|
+
"node_modules",
|
|
9
|
+
"tests"
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
"context": {
|
|
13
|
+
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
|
|
14
|
+
"@aws-cdk/core:checkSecretUsage": true,
|
|
15
|
+
"@aws-cdk/core:target-partitions": ["aws", "aws-cn"],
|
|
16
|
+
"@aws-cdk/aws-iam:minimizePolicies": true,
|
|
17
|
+
"@aws-cdk/customresources:installLatestAwsSdkDefault": false,
|
|
18
|
+
"@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
const cdk = require('aws-cdk-lib');
|
|
2
|
+
const lambda = require('aws-cdk-lib/aws-lambda');
|
|
3
|
+
const apigateway = require('aws-cdk-lib/aws-apigateway');
|
|
4
|
+
const dynamodb = require('aws-cdk-lib/aws-dynamodb');
|
|
5
|
+
const logs = require('aws-cdk-lib/aws-logs');
|
|
6
|
+
const cloudwatch = require('aws-cdk-lib/aws-cloudwatch');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Environment-specific configuration.
|
|
10
|
+
* Adjust memory, timeout, and policies per environment.
|
|
11
|
+
*/
|
|
12
|
+
const ENV_CONFIG = {
|
|
13
|
+
dev: {
|
|
14
|
+
memorySize: {{ cookiecutter.memory_mb }},
|
|
15
|
+
timeout: {{ cookiecutter.timeout_seconds }},
|
|
16
|
+
logRetention: logs.RetentionDays.ONE_DAY,
|
|
17
|
+
removalPolicy: cdk.RemovalPolicy.DESTROY,
|
|
18
|
+
throttlingRate: 50,
|
|
19
|
+
throttlingBurst: 100,
|
|
20
|
+
},
|
|
21
|
+
staging: {
|
|
22
|
+
memorySize: {{ cookiecutter.memory_mb }},
|
|
23
|
+
timeout: {{ cookiecutter.timeout_seconds }},
|
|
24
|
+
logRetention: logs.RetentionDays.ONE_WEEK,
|
|
25
|
+
removalPolicy: cdk.RemovalPolicy.DESTROY,
|
|
26
|
+
throttlingRate: 100,
|
|
27
|
+
throttlingBurst: 200,
|
|
28
|
+
},
|
|
29
|
+
prod: {
|
|
30
|
+
memorySize: 1024,
|
|
31
|
+
timeout: {{ cookiecutter.timeout_seconds }},
|
|
32
|
+
logRetention: logs.RetentionDays.ONE_MONTH,
|
|
33
|
+
removalPolicy: cdk.RemovalPolicy.RETAIN,
|
|
34
|
+
throttlingRate: 200,
|
|
35
|
+
throttlingBurst: 400,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* CDK Stack for {{ cookiecutter.project_name }}
|
|
41
|
+
*
|
|
42
|
+
* Generated by LambdaForge — production-ready Lambda with:
|
|
43
|
+
* - API Gateway REST API with throttling
|
|
44
|
+
* - DynamoDB table (on-demand)
|
|
45
|
+
* - X-Ray tracing
|
|
46
|
+
* - CloudWatch logs + dashboard
|
|
47
|
+
* - Well-Architected tags
|
|
48
|
+
* - Multi-environment support (dev/staging/prod)
|
|
49
|
+
*/
|
|
50
|
+
class {{ cookiecutter.project_name|replace('-', '')|capitalize }}Stack extends cdk.Stack {
|
|
51
|
+
constructor(scope, id, props) {
|
|
52
|
+
super(scope, id, props);
|
|
53
|
+
|
|
54
|
+
const envName = props.envName || 'dev';
|
|
55
|
+
const config = ENV_CONFIG[envName] || ENV_CONFIG.dev;
|
|
56
|
+
|
|
57
|
+
// ============================================================
|
|
58
|
+
// DynamoDB Table
|
|
59
|
+
// ============================================================
|
|
60
|
+
const table = new dynamodb.Table(this, 'ItemsTable', {
|
|
61
|
+
partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
|
|
62
|
+
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
|
|
63
|
+
pointInTimeRecoverySpecification: { pointInTimeRecoveryEnabled: true },
|
|
64
|
+
removalPolicy: config.removalPolicy,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// ============================================================
|
|
68
|
+
// Lambda Function (ARM64 — 20% cheaper)
|
|
69
|
+
// ============================================================
|
|
70
|
+
const fn = new lambda.Function(this, '{{ cookiecutter.project_name|replace('-', '')|capitalize }}Function', {
|
|
71
|
+
runtime: lambda.Runtime.PYTHON_3_12,
|
|
72
|
+
architecture: lambda.Architecture.ARM_64,
|
|
73
|
+
handler: 'handler.handler',
|
|
74
|
+
code: lambda.Code.fromAsset('../src'),
|
|
75
|
+
memorySize: config.memorySize,
|
|
76
|
+
timeout: cdk.Duration.seconds(config.timeout),
|
|
77
|
+
tracing: lambda.Tracing.ACTIVE,
|
|
78
|
+
logGroup: new logs.LogGroup(this, 'FunctionLogGroup', {
|
|
79
|
+
logGroupName: `/aws/lambda/{{ cookiecutter.project_slug }}-${envName}`,
|
|
80
|
+
retention: config.logRetention,
|
|
81
|
+
removalPolicy: config.removalPolicy,
|
|
82
|
+
}),
|
|
83
|
+
environment: {
|
|
84
|
+
TABLE_NAME: table.tableName,
|
|
85
|
+
STAGE: envName,
|
|
86
|
+
POWERTOOLS_SERVICE_NAME: '{{ cookiecutter.project_slug }}',
|
|
87
|
+
LOG_LEVEL: envName === 'prod' ? 'WARNING' : 'INFO',
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
table.grantReadWriteData(fn);
|
|
92
|
+
|
|
93
|
+
// ============================================================
|
|
94
|
+
// API Gateway
|
|
95
|
+
// ============================================================
|
|
96
|
+
const api = new apigateway.RestApi(this, '{{ cookiecutter.project_name|replace('-', '')|capitalize }}Api', {
|
|
97
|
+
restApiName: `{{ cookiecutter.project_slug }}-${envName}`,
|
|
98
|
+
description: `{{ cookiecutter.description }} (${envName})`,
|
|
99
|
+
deployOptions: {
|
|
100
|
+
stageName: envName,
|
|
101
|
+
tracingEnabled: true,
|
|
102
|
+
loggingLevel: apigateway.MethodLoggingLevel.INFO,
|
|
103
|
+
dataTraceEnabled: false,
|
|
104
|
+
throttlingRateLimit: config.throttlingRate,
|
|
105
|
+
throttlingBurstLimit: config.throttlingBurst,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const items = api.root.addResource('items');
|
|
110
|
+
items.addMethod('GET', new apigateway.LambdaIntegration(fn));
|
|
111
|
+
items.addMethod('POST', new apigateway.LambdaIntegration(fn));
|
|
112
|
+
|
|
113
|
+
const health = api.root.addResource('health');
|
|
114
|
+
health.addMethod('GET', new apigateway.LambdaIntegration(fn));
|
|
115
|
+
|
|
116
|
+
// ============================================================
|
|
117
|
+
// CloudWatch Dashboard
|
|
118
|
+
// ============================================================
|
|
119
|
+
const dashboard = new cloudwatch.Dashboard(this, '{{ cookiecutter.project_name|replace('-', '')|capitalize }}Dashboard', {
|
|
120
|
+
dashboardName: `{{ cookiecutter.project_slug }}-${envName}`,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
dashboard.addWidgets(
|
|
124
|
+
new cloudwatch.GraphWidget({
|
|
125
|
+
title: 'Invocations & Errors',
|
|
126
|
+
left: [fn.metricInvocations()],
|
|
127
|
+
right: [fn.metricErrors()],
|
|
128
|
+
}),
|
|
129
|
+
new cloudwatch.GraphWidget({
|
|
130
|
+
title: 'Duration (ms)',
|
|
131
|
+
left: [
|
|
132
|
+
fn.metricDuration({ statistic: 'p50' }),
|
|
133
|
+
fn.metricDuration({ statistic: 'p95' }),
|
|
134
|
+
],
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
// ============================================================
|
|
139
|
+
// Tags (Well-Architected)
|
|
140
|
+
// ============================================================
|
|
141
|
+
cdk.Tags.of(this).add('Project', '{{ cookiecutter.project_name }}');
|
|
142
|
+
cdk.Tags.of(this).add('ManagedBy', 'LambdaForge');
|
|
143
|
+
cdk.Tags.of(this).add('Environment', envName);
|
|
144
|
+
|
|
145
|
+
// ============================================================
|
|
146
|
+
// Outputs
|
|
147
|
+
// ============================================================
|
|
148
|
+
new cdk.CfnOutput(this, 'ApiUrl', { value: api.url });
|
|
149
|
+
new cdk.CfnOutput(this, 'TableName', { value: table.tableName });
|
|
150
|
+
new cdk.CfnOutput(this, 'FunctionName', { value: fn.functionName });
|
|
151
|
+
new cdk.CfnOutput(this, 'Environment', { value: envName });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = { {{ cookiecutter.project_name|replace('-', '')|capitalize }}Stack };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{ cookiecutter.project_slug }}-infrastructure",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AWS CDK infrastructure for {{ cookiecutter.project_name }}",
|
|
5
|
+
"private": true,
|
|
6
|
+
"scripts": {
|
|
7
|
+
"cdk": "cdk",
|
|
8
|
+
"synth": "cdk synth",
|
|
9
|
+
"deploy": "cdk deploy --require-approval never",
|
|
10
|
+
"destroy": "cdk destroy --force",
|
|
11
|
+
"diff": "cdk diff"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"aws-cdk-lib": "^2.170.0",
|
|
15
|
+
"constructs": "^10.4.0"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "{{ cookiecutter.project_slug }}"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "{{ cookiecutter.description }}"
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "{{ cookiecutter.author_name }}", email = "{{ cookiecutter.author_email }}"}
|
|
7
|
+
]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
keywords = ["aws", "lambda", "serverless", "kiro"]
|
|
12
|
+
|
|
13
|
+
dependencies = [
|
|
14
|
+
"aws-lambda-powertools[tracer,logger,metrics]>=3.0.0",
|
|
15
|
+
"boto3>=1.34.0",
|
|
16
|
+
"pydantic>=2.5.0",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.optional-dependencies]
|
|
20
|
+
dev = [
|
|
21
|
+
"pytest>=7.4.0",
|
|
22
|
+
"pytest-cov>=4.1.0",
|
|
23
|
+
"moto[dynamodb,apigateway]>=5.0.0",
|
|
24
|
+
"ruff>=0.1.0",
|
|
25
|
+
"mypy>=1.7.0",
|
|
26
|
+
"aws-cdk-lib>=2.130.0",
|
|
27
|
+
"constructs>=10.3.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[build-system]
|
|
31
|
+
requires = ["hatchling"]
|
|
32
|
+
build-backend = "hatchling.build"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.wheel]
|
|
35
|
+
packages = ["src"]
|
|
36
|
+
|
|
37
|
+
[tool.pytest.ini_options]
|
|
38
|
+
testpaths = ["tests"]
|
|
39
|
+
python_files = ["test_*.py"]
|
|
40
|
+
python_functions = ["test_*"]
|
|
41
|
+
addopts = "-v --strict-markers --cov=src --cov-report=term-missing"
|
|
42
|
+
|
|
43
|
+
[tool.ruff]
|
|
44
|
+
line-length = 100
|
|
45
|
+
target-version = "py312"
|
|
46
|
+
|
|
47
|
+
[tool.ruff.lint]
|
|
48
|
+
select = ["E", "F", "I", "B", "UP", "N", "S", "RUF"]
|
|
49
|
+
ignore = ["S101"] # allow assert in tests
|
|
50
|
+
|
|
51
|
+
[tool.mypy]
|
|
52
|
+
python_version = "3.12"
|
|
53
|
+
strict = true
|
|
54
|
+
warn_return_any = true
|
|
55
|
+
disallow_untyped_defs = true
|
|
56
|
+
|
|
57
|
+
[tool.coverage.run]
|
|
58
|
+
source = ["src"]
|
|
59
|
+
omit = ["tests/*"]
|
|
60
|
+
|
|
61
|
+
[tool.coverage.report]
|
|
62
|
+
exclude_lines = [
|
|
63
|
+
"pragma: no cover",
|
|
64
|
+
"if __name__ == .__main__.:",
|
|
65
|
+
"raise NotImplementedError",
|
|
66
|
+
]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""{{ cookiecutter.project_name }} — Lambda handler.
|
|
2
|
+
|
|
3
|
+
API Gateway trigger with structured logging and error handling.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import boto3
|
|
11
|
+
from aws_lambda_powertools import Logger, Tracer
|
|
12
|
+
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
|
|
13
|
+
from aws_lambda_powertools.logging import correlation_paths
|
|
14
|
+
from aws_lambda_powertools.utilities.typing import LambdaContext
|
|
15
|
+
|
|
16
|
+
# Initialize powertools
|
|
17
|
+
logger = Logger(service="{{ cookiecutter.project_slug }}")
|
|
18
|
+
tracer = Tracer(service="{{ cookiecutter.project_slug }}")
|
|
19
|
+
|
|
20
|
+
# API Gateway resolver
|
|
21
|
+
app = APIGatewayRestResolver()
|
|
22
|
+
|
|
23
|
+
# Environment variables
|
|
24
|
+
TABLE_NAME = os.environ.get("TABLE_NAME", "")
|
|
25
|
+
STAGE = os.environ.get("STAGE", "dev")
|
|
26
|
+
|
|
27
|
+
# AWS clients (initialized once per container)
|
|
28
|
+
dynamodb = boto3.resource("dynamodb")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@app.get("/health")
|
|
32
|
+
@tracer.capture_method
|
|
33
|
+
def health_check() -> dict[str, Any]:
|
|
34
|
+
"""Health check endpoint."""
|
|
35
|
+
return {"status": "ok", "stage": STAGE}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@app.get("/items")
|
|
39
|
+
@tracer.capture_method
|
|
40
|
+
def list_items() -> dict[str, Any]:
|
|
41
|
+
"""List all items from the DynamoDB table."""
|
|
42
|
+
if not TABLE_NAME:
|
|
43
|
+
logger.warning("TABLE_NAME not set, returning empty list")
|
|
44
|
+
return {"items": [], "count": 0}
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
table = dynamodb.Table(TABLE_NAME)
|
|
48
|
+
response = table.scan()
|
|
49
|
+
items = response.get("Items", [])
|
|
50
|
+
|
|
51
|
+
logger.info("items_listed", extra={"count": len(items)})
|
|
52
|
+
return {"items": items, "count": len(items)}
|
|
53
|
+
|
|
54
|
+
except Exception:
|
|
55
|
+
logger.exception("list_items_failed")
|
|
56
|
+
raise
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@app.post("/items")
|
|
60
|
+
@tracer.capture_method
|
|
61
|
+
def create_item() -> dict[str, Any]:
|
|
62
|
+
"""Create a new item in the DynamoDB table."""
|
|
63
|
+
import uuid
|
|
64
|
+
from datetime import datetime
|
|
65
|
+
|
|
66
|
+
body = app.current_event.json_body
|
|
67
|
+
if not body or "name" not in body:
|
|
68
|
+
return {"error": "Missing required field: name"}, 400
|
|
69
|
+
|
|
70
|
+
item = {
|
|
71
|
+
"id": str(uuid.uuid4()),
|
|
72
|
+
"name": body["name"],
|
|
73
|
+
"created_at": datetime.utcnow().isoformat(),
|
|
74
|
+
**body,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
table = dynamodb.Table(TABLE_NAME)
|
|
79
|
+
table.put_item(Item=item)
|
|
80
|
+
|
|
81
|
+
logger.info("item_created", extra={"item_id": item["id"]})
|
|
82
|
+
return {"item": item}, 201
|
|
83
|
+
|
|
84
|
+
except Exception:
|
|
85
|
+
logger.exception("create_item_failed")
|
|
86
|
+
raise
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)
|
|
90
|
+
@tracer.capture_lambda_handler
|
|
91
|
+
def handler(event: dict, context: LambdaContext) -> dict[str, Any]:
|
|
92
|
+
"""Main Lambda handler — routes to APIGatewayRestResolver."""
|
|
93
|
+
return app.resolve(event, context)
|