django-lambda-tasks 0.2.0__tar.gz → 0.2.2__tar.gz
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.
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/steering/product.md +15 -11
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/steering/structure.md +3 -3
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/PKG-INFO +23 -17
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/README.md +22 -16
- django_lambda_tasks-0.2.2/lambda_tasks/environment_loader.py +156 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/lambda_tasks/handler.py +34 -12
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/pyproject.toml +1 -1
- django_lambda_tasks-0.2.2/tests/test_environment_loader.py +691 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/tests/test_handler.py +64 -16
- django_lambda_tasks-0.2.0/lambda_tasks/ssm_environment_loader.py +0 -93
- django_lambda_tasks-0.2.0/tests/test_ssm_environment_loader.py +0 -654
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.github/workflows/ci.yml +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.github/workflows/release.yml +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.gitignore +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/deferred-task-enqueue/.config.kiro +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/deferred-task-enqueue/design.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/deferred-task-enqueue/requirements.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/deferred-task-enqueue/tasks.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/eager-mode-example-app/.config.kiro +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/eager-mode-example-app/design.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/eager-mode-example-app/requirements.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/eager-mode-example-app/tasks.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/ignore-errors-decorator-option/.config.kiro +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/ignore-errors-decorator-option/design.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/ignore-errors-decorator-option/requirements.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/ignore-errors-decorator-option/tasks.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/import-string-task-resolution/.config.kiro +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/import-string-task-resolution/design.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/import-string-task-resolution/requirements.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/import-string-task-resolution/tasks.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/retry-delay/.config.kiro +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/retry-delay/design.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/retry-delay/requirements.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/retry-delay/tasks.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/rse-background-tasks/.config.kiro +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/rse-background-tasks/design.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/rse-background-tasks/requirements.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/rse-background-tasks/tasks.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/rse-background-tasks-bugfix/.config.kiro +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/rse-background-tasks-bugfix/bugfix.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/rse-background-tasks-bugfix/design.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/rse-background-tasks-bugfix/tasks.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/singleton-task/.config.kiro +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/singleton-task/design.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/singleton-task/requirements.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/singleton-task/tasks.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/ssm-environment-loader/.config.kiro +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/ssm-environment-loader/design.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/ssm-environment-loader/requirements.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/ssm-environment-loader/tasks.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/task-retry/.config.kiro +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/task-retry/design.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/task-retry/requirements.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/specs/task-retry/tasks.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.kiro/steering/tech.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.pre-commit-config.yaml +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/.vscode/settings.json +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/example/README.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/example/example_app/__init__.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/example/example_app/apps.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/example/example_app/tasks.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/example/example_app/urls.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/example/example_app/views.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/example/example_project/__init__.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/example/example_project/settings.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/example/example_project/urls.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/example/example_project/wsgi.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/example/manage.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/lambda_tasks/__init__.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/lambda_tasks/admin.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/lambda_tasks/apps.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/lambda_tasks/decorators.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/lambda_tasks/logging.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/lambda_tasks/migrations/0001_initial.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/lambda_tasks/migrations/__init__.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/lambda_tasks/models.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/lambda_tasks/secret_loader.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/lambda_tasks/settings.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/lambda_tasks/tasks.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/lambda_tasks/timeouts.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/tests/conftest.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/tests/settings.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/tests/test_admin.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/tests/test_decorator.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/tests/test_decorators.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/tests/test_deferred_enqueue.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/tests/test_kwargs_only.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/tests/test_logging.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/tests/test_models.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/tests/test_secret_loader.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/tests/test_serializer.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/tests/test_settings.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/tests/test_tasks.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/tests/test_timeout_validation.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.2}/tests/test_timeouts.py +0 -0
|
@@ -15,7 +15,7 @@ View → @lambda_task.execute_on_commit() → SQS → Lambda handler → SQSLamb
|
|
|
15
15
|
Key modules:
|
|
16
16
|
- `decorators.py` — `@lambda_task` decorator and `LambdaTaskWrapper`
|
|
17
17
|
- `models.py` — `TaskRecord` (Django ORM), `SQSLambdaTaskMessage` (SQS schema + execution), `SQSLambdaTask` (routing + SQS publish)
|
|
18
|
-
- `handler.py` — AWS Lambda entry point with partial-batch failure reporting
|
|
18
|
+
- `handler.py` — AWS Lambda entry point; cold-start init runs on first invocation (not at import time) with partial-batch failure reporting
|
|
19
19
|
- `logging.py` — `task_logger` for invocation-scoped log output
|
|
20
20
|
- `settings.py` — lazy `LambdaTasksSettings` reading from Django settings
|
|
21
21
|
|
|
@@ -135,26 +135,30 @@ class SQSLambdaTaskMessage(BaseModel):
|
|
|
135
135
|
- Returns `{"batchItemFailures": [...]}` for partial-batch failure reporting
|
|
136
136
|
- Only pre-execution failures (malformed message, import error, misconfiguration) are reported as `batchItemFailures` — task logic failures are caught and recorded as `FAILED` TaskRecords without raising
|
|
137
137
|
- Recommended SQS queue settings: `maxReceiveCount=1` with a DLQ configured; automatic retries are not useful since task failures are not re-driven by design
|
|
138
|
-
- Cold-start sequence: `
|
|
139
|
-
-
|
|
138
|
+
- Cold-start sequence runs inside the handler on the first invocation (not at module import time) to avoid Lambda init-duration timeouts: `resolve_environment()` → `resolve_secrets_into_env()` → conditional `django.setup()`
|
|
139
|
+
- A module-level `_cold_start_done` sentinel ensures the sequence runs only once; subsequent warm invocations skip it
|
|
140
|
+
- Both loaders run unconditionally (outside the `DJANGO_SETTINGS_MODULE` check) — the environment secret may provide that var, and individual secrets may depend on environment-loaded vars
|
|
140
141
|
|
|
141
|
-
##
|
|
142
|
+
## Environment Loader
|
|
142
143
|
|
|
143
|
-
`
|
|
144
|
+
`resolve_environment()` in `environment_loader.py` runs once at Lambda cold start, before `resolve_secrets_into_env()` and `django.setup()`.
|
|
144
145
|
|
|
145
|
-
When the environment variable `
|
|
146
|
+
When the environment variable `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` is set, the loader parses the reference, fetches the named Secrets Manager secret, parses its JSON content as a flat key-value mapping, and sets the resulting pairs as environment variables.
|
|
147
|
+
|
|
148
|
+
Required format: `<arn>:<version-stage>:<version-id>` (9 colon-separated segments — the ARN is 7 segments, plus version-stage and version-id). Both suffix fields must be non-empty.
|
|
146
149
|
|
|
147
150
|
Behaviour:
|
|
148
|
-
- If `
|
|
149
|
-
-
|
|
150
|
-
-
|
|
151
|
+
- If `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` is not set, does nothing (no AWS API calls)
|
|
152
|
+
- Validates the reference format before any AWS call — malformed references raise `ValueError` immediately
|
|
153
|
+
- Fetches the secret via `secretsmanager.get_secret_value(SecretId=..., VersionStage=..., VersionId=...)`
|
|
154
|
+
- Validates the secret value is a flat JSON object (all values must be strings, no empty keys)
|
|
151
155
|
- Sets each key-value pair in `os.environ` — existing env vars are overridden (no conflict detection)
|
|
152
156
|
- Idempotent via a module-level `_loaded` sentinel — subsequent calls are free no-ops
|
|
153
|
-
- Invalid JSON, non-flat objects, or empty keys raise `ValueError` at cold start
|
|
157
|
+
- Invalid reference format, invalid JSON, non-flat objects, or empty keys raise `ValueError` at cold start
|
|
154
158
|
|
|
155
159
|
## Secret Loader
|
|
156
160
|
|
|
157
|
-
`resolve_secrets_into_env()` in `secret_loader.py` runs once at Lambda cold start, after `
|
|
161
|
+
`resolve_secrets_into_env()` in `secret_loader.py` runs once at Lambda cold start, after `resolve_environment()` and before `django.setup()`.
|
|
158
162
|
|
|
159
163
|
Any env var prefixed `LAMBDA_TASKS_SECRET_` is treated as a Secrets Manager reference. The unprefixed name is the target env var.
|
|
160
164
|
|
|
@@ -18,7 +18,7 @@ django-lambda-tasks/
|
|
|
18
18
|
│ ├── models.py # TaskRecord, SQSLambdaTaskMessage, SQSLambdaTask
|
|
19
19
|
│ ├── settings.py # LambdaTasksSettings (lazy Django settings reader)
|
|
20
20
|
│ ├── secret_loader.py # Resolves LAMBDA_TASKS_SECRET_* env vars at cold start
|
|
21
|
-
│ ├──
|
|
21
|
+
│ ├── environment_loader.py # Loads env vars from Secrets Manager at cold start
|
|
22
22
|
│ ├── tasks.py # Built-in maintenance tasks (cleanup_task_records)
|
|
23
23
|
│ ├── timeouts.py # TimeoutContext implementation
|
|
24
24
|
│ └── migrations/ # Django migrations for TaskRecord
|
|
@@ -40,8 +40,8 @@ django-lambda-tasks/
|
|
|
40
40
|
|
|
41
41
|
- `decorators.py` — defines `@lambda_task`; enforces kwargs-only at decoration time
|
|
42
42
|
- `models.py` — `TaskRecord` (Django ORM), `SQSLambdaTaskMessage` (Pydantic, SQS schema + execution logic), `SQSLambdaTask` (Pydantic, holds message + routing; `_execute()` publishes to SQS or executes eagerly; `execute_on_commit()` registers `_execute` with `transaction.on_commit`)
|
|
43
|
-
- `handler.py` — Lambda entry point;
|
|
44
|
-
- `
|
|
43
|
+
- `handler.py` — Lambda entry point; cold-start init (`resolve_environment()` → `resolve_secrets_into_env()` → conditional `django.setup()`) runs inside the handler on first invocation, guarded by `_cold_start_done` sentinel; processes SQS records independently; returns `batchItemFailures`
|
|
44
|
+
- `environment_loader.py` — loads env vars from a Secrets Manager secret at cold start; validates flat JSON format; idempotent via `_loaded` sentinel
|
|
45
45
|
- `secret_loader.py` — resolves `LAMBDA_TASKS_SECRET_*` env vars from Secrets Manager before Django starts; validates format, detects conflicts, batches API calls; idempotent via `_loaded` sentinel
|
|
46
46
|
- `logging.py` — `task_logger` singleton; `message_id` set/cleared around each task execution
|
|
47
47
|
- `settings.py` — `LambdaTasksSettings` instantiated fresh per use (reads live Django settings)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-lambda-tasks
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Run async tasks in a lambda function
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: awslambdaric
|
|
@@ -435,12 +435,14 @@ Point your Lambda function's handler at:
|
|
|
435
435
|
lambda_tasks.handler.handler
|
|
436
436
|
```
|
|
437
437
|
|
|
438
|
+
The handler performs cold-start initialisation on the first invocation (not at module import time) to avoid Lambda init-duration timeouts. The sequence is: `resolve_environment()` → `resolve_secrets_into_env()` → conditional `django.setup()`. A module-level sentinel ensures this runs only once; subsequent warm invocations skip it entirely.
|
|
439
|
+
|
|
438
440
|
Ensure the Lambda execution environment has `DJANGO_SETTINGS_MODULE` set and that all task modules are importable (i.e. your application code is on the Python path).
|
|
439
441
|
|
|
440
442
|
| Environment Variable | Required | Description |
|
|
441
443
|
|---|---|---|
|
|
442
444
|
| `DJANGO_SETTINGS_MODULE` | Yes | Django settings module path (e.g. `myapp.settings.production`). |
|
|
443
|
-
| `
|
|
445
|
+
| `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | No | Secrets Manager reference (`<arn>:<version-stage>:<version-id>`) to load as environment variables at cold start. |
|
|
444
446
|
| `LAMBDA_TASKS_SECRET_*` | No | Secrets Manager references resolved into env vars at cold start (see below). |
|
|
445
447
|
|
|
446
448
|
### Resolving Django settings from AWS Secrets Manager
|
|
@@ -453,7 +455,7 @@ Set any env var with the prefix `LAMBDA_TASKS_SECRET_` to a full Secrets Manager
|
|
|
453
455
|
LAMBDA_TASKS_SECRET_DATABASE_URL=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL:AWSCURRENT:v1
|
|
454
456
|
```
|
|
455
457
|
|
|
456
|
-
At cold start, before `django.setup()` is called, the handler calls `resolve_secrets_into_env()` which:
|
|
458
|
+
At cold start (on the first handler invocation), before `django.setup()` is called, the handler calls `resolve_secrets_into_env()` which:
|
|
457
459
|
|
|
458
460
|
1. Scans all env vars for the `LAMBDA_TASKS_SECRET_` prefix
|
|
459
461
|
2. Validates every reference — malformed references raise immediately so the container fails to start rather than misconfiguring Django silently
|
|
@@ -493,17 +495,19 @@ The following all raise `ValueError` at cold start, preventing the Lambda contai
|
|
|
493
495
|
- The named JSON key does not exist in the fetched secret
|
|
494
496
|
- The secret value is not valid JSON
|
|
495
497
|
|
|
496
|
-
### Loading environment variables from
|
|
498
|
+
### Loading environment variables from Secrets Manager
|
|
497
499
|
|
|
498
|
-
The Lambda handler supports loading environment variables from an AWS
|
|
500
|
+
The Lambda handler supports loading environment variables from an AWS Secrets Manager secret at cold start. This lets you manage environment configuration centrally in Secrets Manager without baking values into the Lambda deployment package.
|
|
499
501
|
|
|
500
|
-
Set the `
|
|
502
|
+
Set the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` environment variable to a full reference including version stage and version ID:
|
|
501
503
|
|
|
502
504
|
```
|
|
503
|
-
|
|
505
|
+
LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod/environment:AWSCURRENT:v1
|
|
504
506
|
```
|
|
505
507
|
|
|
506
|
-
The
|
|
508
|
+
The format is `<arn>:<version-stage>:<version-id>` (9 colon-separated segments — the ARN is 7 segments, plus version-stage and version-id). Both suffix fields must be non-empty.
|
|
509
|
+
|
|
510
|
+
The secret value must be a flat JSON object where all keys and values are strings:
|
|
507
511
|
|
|
508
512
|
```json
|
|
509
513
|
{
|
|
@@ -513,25 +517,27 @@ The parameter value must be a flat JSON object where all keys and values are str
|
|
|
513
517
|
}
|
|
514
518
|
```
|
|
515
519
|
|
|
516
|
-
At cold start, before `resolve_secrets_into_env()` and `django.setup()`, the handler calls `
|
|
520
|
+
At cold start (on the first handler invocation), before `resolve_secrets_into_env()` and `django.setup()`, the handler calls `resolve_environment()` which:
|
|
517
521
|
|
|
518
|
-
1. Checks for the `
|
|
519
|
-
2.
|
|
520
|
-
3.
|
|
521
|
-
4.
|
|
522
|
-
5.
|
|
522
|
+
1. Checks for the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` env var — if not set, does nothing
|
|
523
|
+
2. Parses and validates the reference format (9 segments, non-empty version-stage and version-id)
|
|
524
|
+
3. Fetches the secret via `secretsmanager.get_secret_value(SecretId=..., VersionStage=..., VersionId=...)`
|
|
525
|
+
4. Validates the secret value is a flat JSON object (all values must be strings, no empty keys)
|
|
526
|
+
5. Sets each key-value pair in `os.environ` — existing env vars are overridden
|
|
527
|
+
6. Caches the result via a module-level sentinel — subsequent calls are free no-ops
|
|
523
528
|
|
|
524
|
-
Because
|
|
529
|
+
Because environment loading runs first, the secret can provide `DJANGO_SETTINGS_MODULE` itself, and individual secrets loaded by `resolve_secrets_into_env()` can reference environment-loaded values.
|
|
525
530
|
|
|
526
531
|
#### Validation errors
|
|
527
532
|
|
|
528
533
|
The following raise `ValueError` at cold start, preventing the Lambda container from starting:
|
|
529
534
|
|
|
530
|
-
-
|
|
535
|
+
- Reference format is invalid (wrong segment count, empty version-stage or version-id)
|
|
536
|
+
- Secret value is not valid JSON
|
|
531
537
|
- JSON is not a flat object (contains non-string values) — error message lists the offending keys
|
|
532
538
|
- JSON contains an empty string key
|
|
533
539
|
|
|
534
|
-
AWS errors (
|
|
540
|
+
AWS errors (secret not found, permission denied) propagate as boto3 exceptions and crash the container at cold start.
|
|
535
541
|
|
|
536
542
|
---
|
|
537
543
|
|
|
@@ -423,12 +423,14 @@ Point your Lambda function's handler at:
|
|
|
423
423
|
lambda_tasks.handler.handler
|
|
424
424
|
```
|
|
425
425
|
|
|
426
|
+
The handler performs cold-start initialisation on the first invocation (not at module import time) to avoid Lambda init-duration timeouts. The sequence is: `resolve_environment()` → `resolve_secrets_into_env()` → conditional `django.setup()`. A module-level sentinel ensures this runs only once; subsequent warm invocations skip it entirely.
|
|
427
|
+
|
|
426
428
|
Ensure the Lambda execution environment has `DJANGO_SETTINGS_MODULE` set and that all task modules are importable (i.e. your application code is on the Python path).
|
|
427
429
|
|
|
428
430
|
| Environment Variable | Required | Description |
|
|
429
431
|
|---|---|---|
|
|
430
432
|
| `DJANGO_SETTINGS_MODULE` | Yes | Django settings module path (e.g. `myapp.settings.production`). |
|
|
431
|
-
| `
|
|
433
|
+
| `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | No | Secrets Manager reference (`<arn>:<version-stage>:<version-id>`) to load as environment variables at cold start. |
|
|
432
434
|
| `LAMBDA_TASKS_SECRET_*` | No | Secrets Manager references resolved into env vars at cold start (see below). |
|
|
433
435
|
|
|
434
436
|
### Resolving Django settings from AWS Secrets Manager
|
|
@@ -441,7 +443,7 @@ Set any env var with the prefix `LAMBDA_TASKS_SECRET_` to a full Secrets Manager
|
|
|
441
443
|
LAMBDA_TASKS_SECRET_DATABASE_URL=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod:DATABASE_URL:AWSCURRENT:v1
|
|
442
444
|
```
|
|
443
445
|
|
|
444
|
-
At cold start, before `django.setup()` is called, the handler calls `resolve_secrets_into_env()` which:
|
|
446
|
+
At cold start (on the first handler invocation), before `django.setup()` is called, the handler calls `resolve_secrets_into_env()` which:
|
|
445
447
|
|
|
446
448
|
1. Scans all env vars for the `LAMBDA_TASKS_SECRET_` prefix
|
|
447
449
|
2. Validates every reference — malformed references raise immediately so the container fails to start rather than misconfiguring Django silently
|
|
@@ -481,17 +483,19 @@ The following all raise `ValueError` at cold start, preventing the Lambda contai
|
|
|
481
483
|
- The named JSON key does not exist in the fetched secret
|
|
482
484
|
- The secret value is not valid JSON
|
|
483
485
|
|
|
484
|
-
### Loading environment variables from
|
|
486
|
+
### Loading environment variables from Secrets Manager
|
|
485
487
|
|
|
486
|
-
The Lambda handler supports loading environment variables from an AWS
|
|
488
|
+
The Lambda handler supports loading environment variables from an AWS Secrets Manager secret at cold start. This lets you manage environment configuration centrally in Secrets Manager without baking values into the Lambda deployment package.
|
|
487
489
|
|
|
488
|
-
Set the `
|
|
490
|
+
Set the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` environment variable to a full reference including version stage and version ID:
|
|
489
491
|
|
|
490
492
|
```
|
|
491
|
-
|
|
493
|
+
LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod/environment:AWSCURRENT:v1
|
|
492
494
|
```
|
|
493
495
|
|
|
494
|
-
The
|
|
496
|
+
The format is `<arn>:<version-stage>:<version-id>` (9 colon-separated segments — the ARN is 7 segments, plus version-stage and version-id). Both suffix fields must be non-empty.
|
|
497
|
+
|
|
498
|
+
The secret value must be a flat JSON object where all keys and values are strings:
|
|
495
499
|
|
|
496
500
|
```json
|
|
497
501
|
{
|
|
@@ -501,25 +505,27 @@ The parameter value must be a flat JSON object where all keys and values are str
|
|
|
501
505
|
}
|
|
502
506
|
```
|
|
503
507
|
|
|
504
|
-
At cold start, before `resolve_secrets_into_env()` and `django.setup()`, the handler calls `
|
|
508
|
+
At cold start (on the first handler invocation), before `resolve_secrets_into_env()` and `django.setup()`, the handler calls `resolve_environment()` which:
|
|
505
509
|
|
|
506
|
-
1. Checks for the `
|
|
507
|
-
2.
|
|
508
|
-
3.
|
|
509
|
-
4.
|
|
510
|
-
5.
|
|
510
|
+
1. Checks for the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` env var — if not set, does nothing
|
|
511
|
+
2. Parses and validates the reference format (9 segments, non-empty version-stage and version-id)
|
|
512
|
+
3. Fetches the secret via `secretsmanager.get_secret_value(SecretId=..., VersionStage=..., VersionId=...)`
|
|
513
|
+
4. Validates the secret value is a flat JSON object (all values must be strings, no empty keys)
|
|
514
|
+
5. Sets each key-value pair in `os.environ` — existing env vars are overridden
|
|
515
|
+
6. Caches the result via a module-level sentinel — subsequent calls are free no-ops
|
|
511
516
|
|
|
512
|
-
Because
|
|
517
|
+
Because environment loading runs first, the secret can provide `DJANGO_SETTINGS_MODULE` itself, and individual secrets loaded by `resolve_secrets_into_env()` can reference environment-loaded values.
|
|
513
518
|
|
|
514
519
|
#### Validation errors
|
|
515
520
|
|
|
516
521
|
The following raise `ValueError` at cold start, preventing the Lambda container from starting:
|
|
517
522
|
|
|
518
|
-
-
|
|
523
|
+
- Reference format is invalid (wrong segment count, empty version-stage or version-id)
|
|
524
|
+
- Secret value is not valid JSON
|
|
519
525
|
- JSON is not a flat object (contains non-string values) — error message lists the offending keys
|
|
520
526
|
- JSON contains an empty string key
|
|
521
527
|
|
|
522
|
-
AWS errors (
|
|
528
|
+
AWS errors (secret not found, permission denied) propagate as boto3 exceptions and crash the container at cold start.
|
|
523
529
|
|
|
524
530
|
---
|
|
525
531
|
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Resolves environment variables from an AWS Secrets Manager secret.
|
|
3
|
+
|
|
4
|
+
When the environment variable ``LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN``
|
|
5
|
+
is set, this module fetches the named secret, parses its value as a flat JSON
|
|
6
|
+
object (all keys and values must be strings), and sets each key-value pair in
|
|
7
|
+
``os.environ``.
|
|
8
|
+
|
|
9
|
+
Required value format::
|
|
10
|
+
|
|
11
|
+
LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN=arn:aws:secretsmanager:eu-west-1:123:secret:my-env:AWSCURRENT:v1
|
|
12
|
+
|
|
13
|
+
That is: ``<arn>:<version-stage>:<version-id>`` (9 colon-separated segments).
|
|
14
|
+
The ARN is 7 segments, plus version-stage and version-id.
|
|
15
|
+
|
|
16
|
+
This is called by the handler on the first invocation (cold start) — before
|
|
17
|
+
``resolve_secrets_into_env()`` and before ``django.setup()`` — so that
|
|
18
|
+
environment variables loaded from the secret are available to both the
|
|
19
|
+
secret loader and Django configuration.
|
|
20
|
+
|
|
21
|
+
The result is cached at module level via a ``_loaded`` sentinel so that
|
|
22
|
+
subsequent calls (warm invocations) are free no-ops.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
import logging
|
|
27
|
+
import os
|
|
28
|
+
from typing import NamedTuple
|
|
29
|
+
|
|
30
|
+
import boto3
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
_loaded: bool = False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class _EnvironmentSecretReference(NamedTuple):
|
|
38
|
+
arn: str
|
|
39
|
+
version_stage: str
|
|
40
|
+
version_id: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def resolve_environment() -> None:
|
|
44
|
+
"""Load secret content into os.environ.
|
|
45
|
+
|
|
46
|
+
Reads the secret identified by LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN,
|
|
47
|
+
parses it as a flat JSON object, and sets each key-value pair
|
|
48
|
+
as an environment variable. Idempotent — cached after first call.
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
ValueError: If the env var format is invalid, the secret content
|
|
52
|
+
is not valid JSON, not a flat string→string mapping,
|
|
53
|
+
or contains an empty string key.
|
|
54
|
+
"""
|
|
55
|
+
global _loaded
|
|
56
|
+
|
|
57
|
+
raw_reference = os.environ.get("LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN")
|
|
58
|
+
if not raw_reference:
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
if _loaded:
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
ref = _parse_reference(value=raw_reference)
|
|
65
|
+
raw_value = _fetch_secret(ref=ref)
|
|
66
|
+
parsed = _validate_and_parse(raw_value=raw_value, secret_arn=ref.arn)
|
|
67
|
+
|
|
68
|
+
for key, value in parsed.items():
|
|
69
|
+
os.environ[key] = value
|
|
70
|
+
|
|
71
|
+
_loaded = True
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _parse_reference(*, value: str) -> _EnvironmentSecretReference:
|
|
75
|
+
"""Parse and validate the environment secret reference.
|
|
76
|
+
|
|
77
|
+
Expected format::
|
|
78
|
+
|
|
79
|
+
<arn>:<version-stage>:<version-id>
|
|
80
|
+
|
|
81
|
+
The ARN itself is 7 colon-separated segments, plus 2 suffix segments
|
|
82
|
+
(version-stage, version-id) = 9 total.
|
|
83
|
+
Both suffix fields must be non-empty.
|
|
84
|
+
|
|
85
|
+
Raises ``ValueError`` if the format is invalid.
|
|
86
|
+
"""
|
|
87
|
+
parts = value.split(":")
|
|
88
|
+
|
|
89
|
+
if len(parts) != 9:
|
|
90
|
+
raise ValueError(
|
|
91
|
+
"LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN has an invalid format. "
|
|
92
|
+
"Expected <arn>:<version-stage>:<version-id> "
|
|
93
|
+
f"(9 colon-separated segments), got {len(parts)}: {value!r}"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
arn = ":".join(parts[:7])
|
|
97
|
+
version_stage = parts[7]
|
|
98
|
+
version_id = parts[8]
|
|
99
|
+
|
|
100
|
+
for field, field_value in (
|
|
101
|
+
("version-stage", version_stage),
|
|
102
|
+
("version-id", version_id),
|
|
103
|
+
):
|
|
104
|
+
if not field_value:
|
|
105
|
+
raise ValueError(
|
|
106
|
+
f"LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN is missing the "
|
|
107
|
+
f"{field} segment: {value!r}"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return _EnvironmentSecretReference(
|
|
111
|
+
arn=arn,
|
|
112
|
+
version_stage=version_stage,
|
|
113
|
+
version_id=version_id,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _fetch_secret(*, ref: _EnvironmentSecretReference) -> str:
|
|
118
|
+
"""Fetch a secret string from Secrets Manager using boto3."""
|
|
119
|
+
client = boto3.client("secretsmanager")
|
|
120
|
+
response = client.get_secret_value(
|
|
121
|
+
SecretId=ref.arn,
|
|
122
|
+
VersionStage=ref.version_stage,
|
|
123
|
+
VersionId=ref.version_id,
|
|
124
|
+
)
|
|
125
|
+
return response["SecretString"]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _validate_and_parse(*, raw_value: str, secret_arn: str) -> dict[str, str]:
|
|
129
|
+
"""Parse JSON and validate it is a flat str→str mapping.
|
|
130
|
+
|
|
131
|
+
Raises ValueError with descriptive messages on failure.
|
|
132
|
+
"""
|
|
133
|
+
try:
|
|
134
|
+
parsed = json.loads(raw_value)
|
|
135
|
+
except json.JSONDecodeError as exc:
|
|
136
|
+
raise ValueError(
|
|
137
|
+
f"Secret {secret_arn} does not contain valid JSON: {exc}"
|
|
138
|
+
) from exc
|
|
139
|
+
|
|
140
|
+
if not isinstance(parsed, dict):
|
|
141
|
+
raise ValueError(
|
|
142
|
+
f"Secret {secret_arn} must be a JSON object, got {type(parsed).__name__}"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
non_string_keys = [
|
|
146
|
+
key for key, value in parsed.items() if not isinstance(value, str)
|
|
147
|
+
]
|
|
148
|
+
if non_string_keys:
|
|
149
|
+
raise ValueError(
|
|
150
|
+
f"Secret {secret_arn} contains non-string values for keys: {non_string_keys}"
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if "" in parsed:
|
|
154
|
+
raise ValueError(f"Secret {secret_arn} contains an empty string key")
|
|
155
|
+
|
|
156
|
+
return parsed
|
|
@@ -4,28 +4,48 @@ AWS Lambda handler for lambda_tasks.
|
|
|
4
4
|
Processes a batch of SQS records using partial-batch failure reporting.
|
|
5
5
|
Each record is processed independently — a failure in one record does not
|
|
6
6
|
prevent processing of other records.
|
|
7
|
+
|
|
8
|
+
Cold-start initialisation (environment loading, secret resolution, Django
|
|
9
|
+
setup) runs inside the handler on the first invocation rather than at module
|
|
10
|
+
import time. This keeps the Lambda init phase fast and avoids init-duration
|
|
11
|
+
timeouts. The sequence is guarded by a module-level sentinel so subsequent
|
|
12
|
+
warm invocations skip it.
|
|
7
13
|
"""
|
|
8
14
|
|
|
9
15
|
import logging
|
|
10
16
|
import os
|
|
11
17
|
|
|
12
|
-
import
|
|
13
|
-
from django.apps import apps
|
|
14
|
-
|
|
18
|
+
from lambda_tasks.environment_loader import resolve_environment
|
|
15
19
|
from lambda_tasks.secret_loader import resolve_secrets_into_env
|
|
16
|
-
from lambda_tasks.ssm_environment_loader import resolve_ssm_environment
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
resolve_ssm_environment()
|
|
22
|
-
resolve_secrets_into_env()
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
_cold_start_done: bool = False
|
|
23
24
|
|
|
24
|
-
if os.environ.get("DJANGO_SETTINGS_MODULE") and not apps.ready:
|
|
25
|
-
django.setup()
|
|
26
25
|
|
|
26
|
+
def _perform_cold_start() -> None:
|
|
27
|
+
"""Run one-time initialisation: env loading, secrets, Django setup.
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
Both loaders are idempotent and run unconditionally — the environment
|
|
30
|
+
secret may provide DJANGO_SETTINGS_MODULE, and individual secrets may
|
|
31
|
+
depend on environment-loaded vars.
|
|
32
|
+
"""
|
|
33
|
+
global _cold_start_done
|
|
34
|
+
|
|
35
|
+
if _cold_start_done:
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
resolve_environment()
|
|
39
|
+
resolve_secrets_into_env()
|
|
40
|
+
|
|
41
|
+
if os.environ.get("DJANGO_SETTINGS_MODULE"):
|
|
42
|
+
import django
|
|
43
|
+
from django.apps import apps
|
|
44
|
+
|
|
45
|
+
if not apps.ready:
|
|
46
|
+
django.setup()
|
|
47
|
+
|
|
48
|
+
_cold_start_done = True
|
|
29
49
|
|
|
30
50
|
|
|
31
51
|
def handler(event: dict, context: object) -> dict:
|
|
@@ -34,6 +54,8 @@ def handler(event: dict, context: object) -> dict:
|
|
|
34
54
|
Returns a partial-batch failure report so AWS only re-drives failed records.
|
|
35
55
|
Signature is fixed by AWS and uses two args only.
|
|
36
56
|
"""
|
|
57
|
+
_perform_cold_start()
|
|
58
|
+
|
|
37
59
|
# Local import due to AppRegistryNotReady
|
|
38
60
|
from lambda_tasks.models import SQSLambdaTaskMessage
|
|
39
61
|
|