django-lambda-tasks 0.2.0__tar.gz → 0.2.1__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.1}/.kiro/steering/product.md +13 -10
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/steering/structure.md +3 -3
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/PKG-INFO +20 -16
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/README.md +19 -15
- django_lambda_tasks-0.2.1/lambda_tasks/environment_loader.py +155 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/lambda_tasks/handler.py +4 -4
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/pyproject.toml +1 -1
- django_lambda_tasks-0.2.0/tests/test_ssm_environment_loader.py → django_lambda_tasks-0.2.1/tests/test_environment_loader.py +234 -192
- django_lambda_tasks-0.2.0/lambda_tasks/ssm_environment_loader.py +0 -93
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.github/workflows/ci.yml +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.github/workflows/release.yml +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.gitignore +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/deferred-task-enqueue/.config.kiro +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/deferred-task-enqueue/design.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/deferred-task-enqueue/requirements.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/deferred-task-enqueue/tasks.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/eager-mode-example-app/.config.kiro +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/eager-mode-example-app/design.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/eager-mode-example-app/requirements.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/eager-mode-example-app/tasks.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/ignore-errors-decorator-option/.config.kiro +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/ignore-errors-decorator-option/design.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/ignore-errors-decorator-option/requirements.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/ignore-errors-decorator-option/tasks.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/import-string-task-resolution/.config.kiro +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/import-string-task-resolution/design.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/import-string-task-resolution/requirements.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/import-string-task-resolution/tasks.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/retry-delay/.config.kiro +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/retry-delay/design.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/retry-delay/requirements.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/retry-delay/tasks.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/rse-background-tasks/.config.kiro +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/rse-background-tasks/design.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/rse-background-tasks/requirements.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/rse-background-tasks/tasks.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/rse-background-tasks-bugfix/.config.kiro +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/rse-background-tasks-bugfix/bugfix.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/rse-background-tasks-bugfix/design.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/rse-background-tasks-bugfix/tasks.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/singleton-task/.config.kiro +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/singleton-task/design.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/singleton-task/requirements.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/singleton-task/tasks.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/ssm-environment-loader/.config.kiro +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/ssm-environment-loader/design.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/ssm-environment-loader/requirements.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/ssm-environment-loader/tasks.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/task-retry/.config.kiro +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/task-retry/design.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/task-retry/requirements.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/specs/task-retry/tasks.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.kiro/steering/tech.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.pre-commit-config.yaml +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/.vscode/settings.json +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/example/README.md +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/example/example_app/__init__.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/example/example_app/apps.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/example/example_app/tasks.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/example/example_app/urls.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/example/example_app/views.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/example/example_project/__init__.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/example/example_project/settings.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/example/example_project/urls.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/example/example_project/wsgi.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/example/manage.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/lambda_tasks/__init__.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/lambda_tasks/admin.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/lambda_tasks/apps.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/lambda_tasks/decorators.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/lambda_tasks/logging.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/lambda_tasks/migrations/0001_initial.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/lambda_tasks/migrations/__init__.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/lambda_tasks/models.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/lambda_tasks/secret_loader.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/lambda_tasks/settings.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/lambda_tasks/tasks.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/lambda_tasks/timeouts.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/tests/conftest.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/tests/settings.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/tests/test_admin.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/tests/test_decorator.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/tests/test_decorators.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/tests/test_deferred_enqueue.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/tests/test_handler.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/tests/test_kwargs_only.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/tests/test_logging.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/tests/test_models.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/tests/test_secret_loader.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/tests/test_serializer.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/tests/test_settings.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/tests/test_tasks.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/tests/test_timeout_validation.py +0 -0
- {django_lambda_tasks-0.2.0 → django_lambda_tasks-0.2.1}/tests/test_timeouts.py +0 -0
|
@@ -135,26 +135,29 @@ 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
|
-
- Both loaders run unconditionally (outside the `DJANGO_SETTINGS_MODULE` check) —
|
|
138
|
+
- Cold-start sequence: `resolve_environment()` → `resolve_secrets_into_env()` → conditional `django.setup()`
|
|
139
|
+
- 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
140
|
|
|
141
|
-
##
|
|
141
|
+
## Environment Loader
|
|
142
142
|
|
|
143
|
-
`
|
|
143
|
+
`resolve_environment()` in `environment_loader.py` runs once at Lambda cold start, before `resolve_secrets_into_env()` and `django.setup()`.
|
|
144
144
|
|
|
145
|
-
When the environment variable `
|
|
145
|
+
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.
|
|
146
|
+
|
|
147
|
+
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
148
|
|
|
147
149
|
Behaviour:
|
|
148
|
-
- If `
|
|
149
|
-
-
|
|
150
|
-
-
|
|
150
|
+
- If `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` is not set, does nothing (no AWS API calls)
|
|
151
|
+
- Validates the reference format before any AWS call — malformed references raise `ValueError` immediately
|
|
152
|
+
- Fetches the secret via `secretsmanager.get_secret_value(SecretId=..., VersionStage=..., VersionId=...)`
|
|
153
|
+
- Validates the secret value is a flat JSON object (all values must be strings, no empty keys)
|
|
151
154
|
- Sets each key-value pair in `os.environ` — existing env vars are overridden (no conflict detection)
|
|
152
155
|
- 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
|
|
156
|
+
- Invalid reference format, invalid JSON, non-flat objects, or empty keys raise `ValueError` at cold start
|
|
154
157
|
|
|
155
158
|
## Secret Loader
|
|
156
159
|
|
|
157
|
-
`resolve_secrets_into_env()` in `secret_loader.py` runs once at Lambda cold start, after `
|
|
160
|
+
`resolve_secrets_into_env()` in `secret_loader.py` runs once at Lambda cold start, after `resolve_environment()` and before `django.setup()`.
|
|
158
161
|
|
|
159
162
|
Any env var prefixed `LAMBDA_TASKS_SECRET_` is treated as a Secrets Manager reference. The unprefixed name is the target env var.
|
|
160
163
|
|
|
@@ -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; calls `
|
|
44
|
-
- `
|
|
43
|
+
- `handler.py` — Lambda entry point; calls `resolve_environment()` then `resolve_secrets_into_env()` then conditionally `django.setup()` at cold start; 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.1
|
|
4
4
|
Summary: Run async tasks in a lambda function
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: awslambdaric
|
|
@@ -440,7 +440,7 @@ Ensure the Lambda execution environment has `DJANGO_SETTINGS_MODULE` set and tha
|
|
|
440
440
|
| Environment Variable | Required | Description |
|
|
441
441
|
|---|---|---|
|
|
442
442
|
| `DJANGO_SETTINGS_MODULE` | Yes | Django settings module path (e.g. `myapp.settings.production`). |
|
|
443
|
-
| `
|
|
443
|
+
| `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | No | Secrets Manager reference (`<arn>:<version-stage>:<version-id>`) to load as environment variables at cold start. |
|
|
444
444
|
| `LAMBDA_TASKS_SECRET_*` | No | Secrets Manager references resolved into env vars at cold start (see below). |
|
|
445
445
|
|
|
446
446
|
### Resolving Django settings from AWS Secrets Manager
|
|
@@ -493,17 +493,19 @@ The following all raise `ValueError` at cold start, preventing the Lambda contai
|
|
|
493
493
|
- The named JSON key does not exist in the fetched secret
|
|
494
494
|
- The secret value is not valid JSON
|
|
495
495
|
|
|
496
|
-
### Loading environment variables from
|
|
496
|
+
### Loading environment variables from Secrets Manager
|
|
497
497
|
|
|
498
|
-
The Lambda handler supports loading environment variables from an AWS
|
|
498
|
+
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
499
|
|
|
500
|
-
Set the `
|
|
500
|
+
Set the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` environment variable to a full reference including version stage and version ID:
|
|
501
501
|
|
|
502
502
|
```
|
|
503
|
-
|
|
503
|
+
LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod/environment:AWSCURRENT:v1
|
|
504
504
|
```
|
|
505
505
|
|
|
506
|
-
The
|
|
506
|
+
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.
|
|
507
|
+
|
|
508
|
+
The secret value must be a flat JSON object where all keys and values are strings:
|
|
507
509
|
|
|
508
510
|
```json
|
|
509
511
|
{
|
|
@@ -513,25 +515,27 @@ The parameter value must be a flat JSON object where all keys and values are str
|
|
|
513
515
|
}
|
|
514
516
|
```
|
|
515
517
|
|
|
516
|
-
At cold start, before `resolve_secrets_into_env()` and `django.setup()`, the handler calls `
|
|
518
|
+
At cold start, before `resolve_secrets_into_env()` and `django.setup()`, the handler calls `resolve_environment()` which:
|
|
517
519
|
|
|
518
|
-
1. Checks for the `
|
|
519
|
-
2.
|
|
520
|
-
3.
|
|
521
|
-
4.
|
|
522
|
-
5.
|
|
520
|
+
1. Checks for the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` env var — if not set, does nothing
|
|
521
|
+
2. Parses and validates the reference format (9 segments, non-empty version-stage and version-id)
|
|
522
|
+
3. Fetches the secret via `secretsmanager.get_secret_value(SecretId=..., VersionStage=..., VersionId=...)`
|
|
523
|
+
4. Validates the secret value is a flat JSON object (all values must be strings, no empty keys)
|
|
524
|
+
5. Sets each key-value pair in `os.environ` — existing env vars are overridden
|
|
525
|
+
6. Caches the result via a module-level sentinel — subsequent calls are free no-ops
|
|
523
526
|
|
|
524
|
-
Because
|
|
527
|
+
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
528
|
|
|
526
529
|
#### Validation errors
|
|
527
530
|
|
|
528
531
|
The following raise `ValueError` at cold start, preventing the Lambda container from starting:
|
|
529
532
|
|
|
530
|
-
-
|
|
533
|
+
- Reference format is invalid (wrong segment count, empty version-stage or version-id)
|
|
534
|
+
- Secret value is not valid JSON
|
|
531
535
|
- JSON is not a flat object (contains non-string values) — error message lists the offending keys
|
|
532
536
|
- JSON contains an empty string key
|
|
533
537
|
|
|
534
|
-
AWS errors (
|
|
538
|
+
AWS errors (secret not found, permission denied) propagate as boto3 exceptions and crash the container at cold start.
|
|
535
539
|
|
|
536
540
|
---
|
|
537
541
|
|
|
@@ -428,7 +428,7 @@ Ensure the Lambda execution environment has `DJANGO_SETTINGS_MODULE` set and tha
|
|
|
428
428
|
| Environment Variable | Required | Description |
|
|
429
429
|
|---|---|---|
|
|
430
430
|
| `DJANGO_SETTINGS_MODULE` | Yes | Django settings module path (e.g. `myapp.settings.production`). |
|
|
431
|
-
| `
|
|
431
|
+
| `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` | No | Secrets Manager reference (`<arn>:<version-stage>:<version-id>`) to load as environment variables at cold start. |
|
|
432
432
|
| `LAMBDA_TASKS_SECRET_*` | No | Secrets Manager references resolved into env vars at cold start (see below). |
|
|
433
433
|
|
|
434
434
|
### Resolving Django settings from AWS Secrets Manager
|
|
@@ -481,17 +481,19 @@ The following all raise `ValueError` at cold start, preventing the Lambda contai
|
|
|
481
481
|
- The named JSON key does not exist in the fetched secret
|
|
482
482
|
- The secret value is not valid JSON
|
|
483
483
|
|
|
484
|
-
### Loading environment variables from
|
|
484
|
+
### Loading environment variables from Secrets Manager
|
|
485
485
|
|
|
486
|
-
The Lambda handler supports loading environment variables from an AWS
|
|
486
|
+
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
487
|
|
|
488
|
-
Set the `
|
|
488
|
+
Set the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` environment variable to a full reference including version stage and version ID:
|
|
489
489
|
|
|
490
490
|
```
|
|
491
|
-
|
|
491
|
+
LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN=arn:aws:secretsmanager:eu-west-1:123456789012:secret:myapp/prod/environment:AWSCURRENT:v1
|
|
492
492
|
```
|
|
493
493
|
|
|
494
|
-
The
|
|
494
|
+
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.
|
|
495
|
+
|
|
496
|
+
The secret value must be a flat JSON object where all keys and values are strings:
|
|
495
497
|
|
|
496
498
|
```json
|
|
497
499
|
{
|
|
@@ -501,25 +503,27 @@ The parameter value must be a flat JSON object where all keys and values are str
|
|
|
501
503
|
}
|
|
502
504
|
```
|
|
503
505
|
|
|
504
|
-
At cold start, before `resolve_secrets_into_env()` and `django.setup()`, the handler calls `
|
|
506
|
+
At cold start, before `resolve_secrets_into_env()` and `django.setup()`, the handler calls `resolve_environment()` which:
|
|
505
507
|
|
|
506
|
-
1. Checks for the `
|
|
507
|
-
2.
|
|
508
|
-
3.
|
|
509
|
-
4.
|
|
510
|
-
5.
|
|
508
|
+
1. Checks for the `LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN` env var — if not set, does nothing
|
|
509
|
+
2. Parses and validates the reference format (9 segments, non-empty version-stage and version-id)
|
|
510
|
+
3. Fetches the secret via `secretsmanager.get_secret_value(SecretId=..., VersionStage=..., VersionId=...)`
|
|
511
|
+
4. Validates the secret value is a flat JSON object (all values must be strings, no empty keys)
|
|
512
|
+
5. Sets each key-value pair in `os.environ` — existing env vars are overridden
|
|
513
|
+
6. Caches the result via a module-level sentinel — subsequent calls are free no-ops
|
|
511
514
|
|
|
512
|
-
Because
|
|
515
|
+
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
516
|
|
|
514
517
|
#### Validation errors
|
|
515
518
|
|
|
516
519
|
The following raise `ValueError` at cold start, preventing the Lambda container from starting:
|
|
517
520
|
|
|
518
|
-
-
|
|
521
|
+
- Reference format is invalid (wrong segment count, empty version-stage or version-id)
|
|
522
|
+
- Secret value is not valid JSON
|
|
519
523
|
- JSON is not a flat object (contains non-string values) — error message lists the offending keys
|
|
520
524
|
- JSON contains an empty string key
|
|
521
525
|
|
|
522
|
-
AWS errors (
|
|
526
|
+
AWS errors (secret not found, permission denied) propagate as boto3 exceptions and crash the container at cold start.
|
|
523
527
|
|
|
524
528
|
---
|
|
525
529
|
|
|
@@ -0,0 +1,155 @@
|
|
|
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 runs at Lambda cold start — before ``resolve_secrets_into_env()`` and
|
|
17
|
+
before ``django.setup()`` — so that environment variables loaded from the
|
|
18
|
+
secret are available to both the secret loader and Django configuration.
|
|
19
|
+
|
|
20
|
+
The result is cached at module level via a ``_loaded`` sentinel so that
|
|
21
|
+
subsequent calls (warm invocations) are free no-ops.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import logging
|
|
26
|
+
import os
|
|
27
|
+
from typing import NamedTuple
|
|
28
|
+
|
|
29
|
+
import boto3
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
_loaded: bool = False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _EnvironmentSecretReference(NamedTuple):
|
|
37
|
+
arn: str
|
|
38
|
+
version_stage: str
|
|
39
|
+
version_id: str
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def resolve_environment() -> None:
|
|
43
|
+
"""Load secret content into os.environ.
|
|
44
|
+
|
|
45
|
+
Reads the secret identified by LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN,
|
|
46
|
+
parses it as a flat JSON object, and sets each key-value pair
|
|
47
|
+
as an environment variable. Idempotent — cached after first call.
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
ValueError: If the env var format is invalid, the secret content
|
|
51
|
+
is not valid JSON, not a flat string→string mapping,
|
|
52
|
+
or contains an empty string key.
|
|
53
|
+
"""
|
|
54
|
+
global _loaded
|
|
55
|
+
|
|
56
|
+
raw_reference = os.environ.get("LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN")
|
|
57
|
+
if not raw_reference:
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
if _loaded:
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
ref = _parse_reference(value=raw_reference)
|
|
64
|
+
raw_value = _fetch_secret(ref=ref)
|
|
65
|
+
parsed = _validate_and_parse(raw_value=raw_value, secret_arn=ref.arn)
|
|
66
|
+
|
|
67
|
+
for key, value in parsed.items():
|
|
68
|
+
os.environ[key] = value
|
|
69
|
+
|
|
70
|
+
_loaded = True
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _parse_reference(*, value: str) -> _EnvironmentSecretReference:
|
|
74
|
+
"""Parse and validate the environment secret reference.
|
|
75
|
+
|
|
76
|
+
Expected format::
|
|
77
|
+
|
|
78
|
+
<arn>:<version-stage>:<version-id>
|
|
79
|
+
|
|
80
|
+
The ARN itself is 7 colon-separated segments, plus 2 suffix segments
|
|
81
|
+
(version-stage, version-id) = 9 total.
|
|
82
|
+
Both suffix fields must be non-empty.
|
|
83
|
+
|
|
84
|
+
Raises ``ValueError`` if the format is invalid.
|
|
85
|
+
"""
|
|
86
|
+
parts = value.split(":")
|
|
87
|
+
|
|
88
|
+
if len(parts) != 9:
|
|
89
|
+
raise ValueError(
|
|
90
|
+
"LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN has an invalid format. "
|
|
91
|
+
"Expected <arn>:<version-stage>:<version-id> "
|
|
92
|
+
f"(9 colon-separated segments), got {len(parts)}: {value!r}"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
arn = ":".join(parts[:7])
|
|
96
|
+
version_stage = parts[7]
|
|
97
|
+
version_id = parts[8]
|
|
98
|
+
|
|
99
|
+
for field, field_value in (
|
|
100
|
+
("version-stage", version_stage),
|
|
101
|
+
("version-id", version_id),
|
|
102
|
+
):
|
|
103
|
+
if not field_value:
|
|
104
|
+
raise ValueError(
|
|
105
|
+
f"LAMBDA_TASKS_ENVIRONMENT_SECRETS_MANAGER_ARN is missing the "
|
|
106
|
+
f"{field} segment: {value!r}"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
return _EnvironmentSecretReference(
|
|
110
|
+
arn=arn,
|
|
111
|
+
version_stage=version_stage,
|
|
112
|
+
version_id=version_id,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _fetch_secret(*, ref: _EnvironmentSecretReference) -> str:
|
|
117
|
+
"""Fetch a secret string from Secrets Manager using boto3."""
|
|
118
|
+
client = boto3.client("secretsmanager")
|
|
119
|
+
response = client.get_secret_value(
|
|
120
|
+
SecretId=ref.arn,
|
|
121
|
+
VersionStage=ref.version_stage,
|
|
122
|
+
VersionId=ref.version_id,
|
|
123
|
+
)
|
|
124
|
+
return response["SecretString"]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _validate_and_parse(*, raw_value: str, secret_arn: str) -> dict[str, str]:
|
|
128
|
+
"""Parse JSON and validate it is a flat str→str mapping.
|
|
129
|
+
|
|
130
|
+
Raises ValueError with descriptive messages on failure.
|
|
131
|
+
"""
|
|
132
|
+
try:
|
|
133
|
+
parsed = json.loads(raw_value)
|
|
134
|
+
except json.JSONDecodeError as exc:
|
|
135
|
+
raise ValueError(
|
|
136
|
+
f"Secret {secret_arn} does not contain valid JSON: {exc}"
|
|
137
|
+
) from exc
|
|
138
|
+
|
|
139
|
+
if not isinstance(parsed, dict):
|
|
140
|
+
raise ValueError(
|
|
141
|
+
f"Secret {secret_arn} must be a JSON object, got {type(parsed).__name__}"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
non_string_keys = [
|
|
145
|
+
key for key, value in parsed.items() if not isinstance(value, str)
|
|
146
|
+
]
|
|
147
|
+
if non_string_keys:
|
|
148
|
+
raise ValueError(
|
|
149
|
+
f"Secret {secret_arn} contains non-string values for keys: {non_string_keys}"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
if "" in parsed:
|
|
153
|
+
raise ValueError(f"Secret {secret_arn} contains an empty string key")
|
|
154
|
+
|
|
155
|
+
return parsed
|
|
@@ -12,13 +12,13 @@ import os
|
|
|
12
12
|
import django
|
|
13
13
|
from django.apps import apps
|
|
14
14
|
|
|
15
|
+
from lambda_tasks.environment_loader import resolve_environment
|
|
15
16
|
from lambda_tasks.secret_loader import resolve_secrets_into_env
|
|
16
|
-
from lambda_tasks.ssm_environment_loader import resolve_ssm_environment
|
|
17
17
|
|
|
18
18
|
# Both loaders are idempotent and run unconditionally before the
|
|
19
|
-
# DJANGO_SETTINGS_MODULE check —
|
|
20
|
-
# secrets may depend on
|
|
21
|
-
|
|
19
|
+
# DJANGO_SETTINGS_MODULE check — the environment secret may provide
|
|
20
|
+
# that var, and individual secrets may depend on environment-loaded vars.
|
|
21
|
+
resolve_environment()
|
|
22
22
|
resolve_secrets_into_env()
|
|
23
23
|
|
|
24
24
|
if os.environ.get("DJANGO_SETTINGS_MODULE") and not apps.ready:
|