Skip to content

Commit 5450f13

Browse files
authored
Merge pull request #20 from dwhswenson/more-docs
Docs: First batch of explanation, tutorials, and how-tos
2 parents 8131e65 + 31c630b commit 5450f13

18 files changed

+863
-22
lines changed

.readthedocs.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
version: 2
2+
3+
build:
4+
os: ubuntu-22.04
5+
tools:
6+
python: "3.12"
7+
8+
python:
9+
install:
10+
- method: pip
11+
path: .
12+
extra_requirements:
13+
- dev
14+
15+
mkdocs:
16+
configuration: mkdocs.yml

AGENTS.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,16 @@
3131
- Use structured logging over prints; avoid noisy logs in hot paths.
3232
- Fail fast with clear errors; prefer explicit exceptions to silent fallbacks.
3333
- Keep configuration externalized (env vars/config files) and document defaults.
34+
35+
## Documentation (Diataxis + MkDocs)
36+
- Write docs in Markdown and keep them under `docs/` for MkDocs rendering.
37+
- Follow Diataxis and keep content intent-specific:
38+
- `docs/tutorials/` for guided learning.
39+
- `docs/how-to/` for task-oriented procedures.
40+
- `docs/reference/` for factual API/module/config details.
41+
- `docs/explanation/` for rationale, tradeoffs, and architecture context.
42+
- Do not mix Diataxis types on a single page; split pages when intent changes.
43+
- Any PR that changes behavior, inputs/outputs, or operations should update the corresponding docs in the same PR.
44+
- Prefer runnable, copy/paste-safe examples with prerequisites and expected outcomes.
45+
- Keep MkDocs navigation explicit in `mkdocs.yml` and use stable page paths/titles to reduce link churn.
46+

docs/explanation/AWSArchitecture-ConceptualArchitecture.drawio.svg

Lines changed: 4 additions & 0 deletions
Loading

docs/explanation/AWSArchitecture-LambdaModules.drawio.svg

Lines changed: 4 additions & 0 deletions
Loading

docs/explanation/AWSArchitecture-ModuleArchitecture.drawio.svg

Lines changed: 4 additions & 0 deletions
Loading

docs/explanation/architecture.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Architecture
2+
3+
## Overview
4+
5+
The basic idea is that we have a scheduled lambda function (triggered by EventBridge events) that performs some tasks and publishes results to a shared SNS topic. Then we have notification modules that subscribe to this SNS topic with filter policies to receive only the relevant messages. This allows us to decouple the task execution from the notification handling and makes it easier to add new notification modules in the future without changing the core logic of the scheduled lambda.
6+
7+
![Architecture Diagram](AWSArchitecture-ConceptualArchitecture.drawio.svg)
8+
9+
The overall data flow is that an EventBridge event triggers the scheduled lambda. That scheduled lambda executes its tasks and then publishes results to the SNS topic.The results are a JSON object where the keys represent the type of result (e.g., "success", "failure", "warning") and the values contain additional information that can be useful for notifying the end user. The SQS FIFO queues subscribe to specific notification types based on filter policies, so a given queue might, for example, only receive messages where `result_type` is "failure". The notification handler lambdas are then triggered by messages in the SQS queues, and use Jinja templates to format the notifications before sending them to the end user (e.g., via email).
10+
11+
## Modules
12+
13+
When we decompose the architecture into Terraform modules, the architecture splits differently from the conceptual architecture. The message broker splits into some components being associated with the notification lambdas (the SQS queues) and some parts being associated with the core functionality (the SNS topic).
14+
15+
![Module-based architecture diagram](AWSArchitecture-ModuleArchitecture.drawio.svg)
16+
17+
We provide a `scheduled-lambda` module that handles everything about the EventBridge rules and the Lambda function that performs the scheduled tasks. You'll need to provide a Docker image for the Lambda function, which will do the actual business logic.
18+
19+
The root module wraps the `scheduled-lambda` module with an SNS topic, and handles the wiring of that.
20+
21+
Notification modules are separate, and when you deploy, you'll need to include your own notification modules: the root module does not contain any default notifications. Each notification module manages the SQS queue and the Lambda function to handle messages, as well as the subscription to the SNS topic with the appropriate filter policy. There's a core `notification-plumbing` module, and individual modules for different notification types (e.g., email via SES), are wrappers around that core module, exposing the correct parameters to configure the specific notification type.
22+
23+
24+
25+
## Notification Templates
26+
27+
One of the big ideas of LambdaCron is that the notifications use Jinja templates to format the messages sent to the end user. This allows for a lot of flexibility in how the notifications are presented, and makes it easy to include dynamic content based on the results of the scheduled tasks.
28+
29+
The key thing is that the template must correspond to the structure of the JSON object published to the SNS topic. This is how we can insert a message broker that knows nothing about the specific use case of the scheduled lambda: it just gives us a message (as a JSON object) and then it tells us how to format that message using the Jinja template.

docs/explanation/data-flow.md

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Data Flow: `_perform_task` to Rendered Notification
2+
3+
This page traces the data flow from a scheduled task's `_perform_task` return value through SNS publication, SQS subscription, notifier parsing, and final template rendering. It illustrates how the shape of the data evolves at each stage and how it interacts with filter policies and message attributes.
4+
5+
## Scenario
6+
7+
Assume one scheduled task publishes into one shared SNS topic, and two notifiers are subscribed with filter policies:
8+
9+
* Print notifier queue: `result_types = ["OK"]`
10+
* Email notifier queue: `result_types = ["ERROR"]`
11+
12+
## Phase 1: `_perform_task` Return Value
13+
14+
Let's say the lambda runs a few tasks, which either pass (status `OK`) or fail (status `ERROR`). Let's say that the task-specific information to be used in the templates are `taskid` and `name`. Here's an example of what the return value of `_perform_task` might look like:
15+
16+
```json
17+
{
18+
"OK": {
19+
"tasks": [{"taskid": 1, "name": "Foo"}, {"taskid": 2, "name": "Bar"}]
20+
},
21+
"ERROR": {
22+
"tasks": [{"taskid": 3, "name": "Baz"}]
23+
}
24+
}
25+
```
26+
27+
## Phase 2: What Gets Published to SNS
28+
29+
We publish one SNS message per status type, so in this case two messages: one for `OK` and one for `ERROR`. Each message includes the relevant portion of the `_perform_task` return value (JSON-encoded) as the `Message` payload. The `result_type` is included as a `MessageAttribute` to allow for filtering by the SQS subscriptions.
30+
31+
For `OK`, the publish call payload shape is:
32+
33+
```json
34+
{
35+
"TopicArn": "arn:aws:sns:us-east-1:123456789012:lambdacron-results.fifo",
36+
"Message": "{\"tasks\": [{\"taskid\": 1, \"name\": \"Foo\"}, {\"taskid\": 2, \"name\": \"Bar\"}]}",
37+
"Subject": "Notification for OK",
38+
"MessageAttributes": {
39+
"result_type": {
40+
"DataType": "String",
41+
"StringValue": "OK"
42+
}
43+
},
44+
"MessageGroupId": "lambdacron"
45+
}
46+
```
47+
48+
For `ERROR`, the shape is identical except:
49+
50+
- `Message` is `"{\"tasks\": [{\"taskid\": 3, \"name\": \"Baz\"}]}"`
51+
- `Subject` is `"Notification for ERROR"`
52+
- `MessageAttributes.result_type.StringValue` is `"ERROR"`
53+
54+
## Phase 3: What Arrives in Each SQS Queue
55+
56+
Because subscriptions filter on `MessageAttributes.result_type`, each queue receives only matching messages. From here, we'll just follow the message for `OK`; the message for `ERROR` has the same shape but different content (and a different template rendering result in the end).
57+
58+
The SQS event record (as seen by notifier Lambda) looks like:
59+
60+
```json
61+
{
62+
"Records": [
63+
{
64+
"messageId": "msg-ok-1",
65+
"eventSource": "aws:sqs",
66+
"body": "{\"tasks\": [{\"taskid\": 1, \"name\": \"Foo\"}, {\"taskid\": 2, \"name\": \"Bar\"}]}",
67+
"messageAttributes": {
68+
"result_type": {
69+
"stringValue": "OK",
70+
"dataType": "String"
71+
}
72+
}
73+
}
74+
]
75+
}
76+
```
77+
78+
## Phase 4: Notifier Parse Behavior with This Shape
79+
80+
The notifier parser converts the JSON string back into an object, and, if `result_type` is missing from the payload, injects it from the SQS `messageAttributes`. In this case, the payload doesn't include `result_type`, so it is injected.
81+
82+
The result, which is fed to the notifier's templates, is:
83+
84+
```json
85+
{
86+
"tasks": [{"taskid": 1, "name": "Foo"}, {"taskid": 2, "name": "Bar"}],
87+
"result_type": "OK"
88+
}
89+
```
90+
91+
## Phase 5: Template Rendering
92+
93+
Let's say the template used by the print notifier for `OK` messages is:
94+
95+
```jinja2
96+
Result {{ result_type }}: {% for task in tasks %}#{{ task.taskid }} {{ task.name }}{% if not loop.last %}, {% endif %}{% endfor %}
97+
```
98+
99+
The output for the `OK` message would be:
100+
101+
```text
102+
Result OK: #1 Foo, #2 Bar
103+
```
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Lambda Container Image Architecture
2+
3+
4+
## Overview
5+
6+
LambdaCron uses Docker images to back our Lambda functions. Docker-based Lambdas are easier to develop and test locally, and are actually faster to start up than zip-based Lambdas.
7+
8+
Since your Lambda needs to be backed by an ECR repository in your own region, we require that our lambda modules take an image URI as input. We provide modules that cover 2 approaches to having the user deploy the image into their own ECR repository:
9+
10+
1. `lambda-image-build`: Directly build the code. This takes a local code source and builds the Docker image, then deploys it to the user's account. This is most suitable for development and for users who want to customize the code.
11+
2. `lambda-image-republish`: Republish a public ECR image. This simply copies a public image into the user's ECR repository. This is for users who want to use the default code, or pin to a specific and well-documented version of the image.
12+
13+
In order to facilitate developers creating their own tools build on LambdaCron, we also provide a `lambda-image-public` module, which builds the image and publishes it to a public ECR repository.
14+
15+
![AWSArchitecture-LambdaModules.drawio.svg](AWSArchitecture-LambdaModules.drawio.svg)
16+
17+
The two approaches are illustrated in the figure. The direct-from-source approach is on the top, and the republish approach is on the bottom. Normally, the core maintainer of the image does the first stage in the republish approach, and users can do the second stage to copy the image into their own account.

docs/how-to/set-up-ses.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Set Up SES Prerequisites
2+
3+
Set up Amazon SES once per AWS account and region where your notifier Lambda runs. AWS now guides much of this in the SES setup wizard; this page is the short checklist for LambdaCron.
4+
5+
6+
## When to Use
7+
8+
* You want to use Amazon SES as the delivery provider for LambdaCron email notifications.
9+
* You have not yet prepared SES identities or account access in the target AWS region.
10+
11+
## Before You Begin
12+
13+
* Choose the AWS account/region where email will be sent (SES setup is regional).
14+
* Choose a sender identity:
15+
* Email identity for one sender address.
16+
* Domain identity if you want to send from multiple addresses in one domain.
17+
* Make sure you can edit DNS records if you choose a domain identity.
18+
19+
SES requires verified identities for senders and (in sandbox) recipients. That means you must verify the email address or domain you want to send from, and if in sandbox, also verify any recipient addresses. Sandbox mode limits how much you can send, and you'll probably want to request production access if you want to send to more than one recipient.
20+
21+
## Steps
22+
23+
1. Open Amazon SES in the notifier's region and follow the setup flow.
24+
* Start with AWS's setup guide and wizard: <https://docs.aws.amazon.com/ses/latest/dg/setting-up.html>.
25+
2. Create and verify your sender identity.
26+
* SES requires verified identities for `From`/`Sender` addresses.
27+
* Use the verified identities guide: <https://docs.aws.amazon.com/ses/latest/dg/verify-addresses-and-domains.html>.
28+
* If using a domain, publish SES-provided DNS records (including DKIM) and wait for `Verified`.
29+
3. Account for sandbox restrictions while testing.
30+
* New SES accounts are in sandbox mode per region.
31+
* In sandbox, you can only send to verified recipients (or the SES mailbox simulator).
32+
4. Request production access when you are ready to send to unverified recipients.
33+
* Submit a production access request in the same region: <https://docs.aws.amazon.com/ses/latest/dg/request-production-access.html>.
34+
* Keep your verified sender identity in place after approval.
35+
5. (Recommended) Publish SPF/DMARC records for your sending domain to improve deliverability.
36+
6. Send a test email from SES in that region before deploying the notifier.
37+
* Use the exact sender address you plan to configure in the `email-notification` module.
38+
39+
## Validation
40+
41+
* SES identity status is `Verified` for your sender.
42+
* All required tasks are completed on the SES console's "Get set up" page.
43+
* If still in sandbox, all test recipients are verified.
44+
* If in production, you can send to non-verified recipients.
45+
* A console/API test send succeeds in the same region as your notifier Lambda.
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
# Use Email Notifications
2+
3+
The LambdaCron root module creates the scheduled Lambda and shared SNS topic, but it does not create notification channels. Use `email-notification` to send selected `result_type` values through Amazon SES.
4+
5+
## When to Use
6+
* You want SES email notifications for one or more LambdaCron `result_type` values.
7+
* You already have, or can provide, a notification-handler container image.
8+
9+
## Before You Begin
10+
11+
* Complete SES setup in [Set Up SES Prerequisites](set-up-ses.md).
12+
* Verify your SES sender identity in the same AWS region as your notifier Lambda.
13+
* If your account is in SES sandbox mode, verify recipient addresses too.
14+
* Create your subject/text/html template files.
15+
16+
## Inputs to Provide
17+
18+
* `sns_topic_arn` from your LambdaCron stack output.
19+
* `lambda_image_uri` for the notification handler container.
20+
* `fifo_queue_name` ending with `.fifo`.
21+
* `sender`, `recipients`, and optional `reply_to`.
22+
* `subject_template_file`, `text_template_file`, and `html_template_file`.
23+
* Optional runtime/routing settings such as `result_types`, `lambda_name`, `batch_size`, and `tags`.
24+
25+
## Steps
26+
27+
### 1. Create email templates
28+
29+
The email notification module requires 3 Jinja templates: one for the email subject, one for the text-only body, and one for the HTML body.
30+
31+
```jinja
32+
{# templates/email-subject.txt #}
33+
[{{ result_type }}] LambdaCron notification
34+
```
35+
36+
```jinja
37+
{# templates/email-body.txt #}
38+
Result type: {{ result_type }}
39+
Message: {{ message }}
40+
```
41+
42+
```html
43+
<!-- templates/email-body.html -->
44+
<h2>LambdaCron notification</h2>
45+
<p><strong>Result type:</strong> {{ result_type }}</p>
46+
<p><strong>Message:</strong> {{ message }}</p>
47+
```
48+
49+
### 2. Republish the notification image or get the image URI
50+
51+
If you have already republished the notification handler image to your own ECR repository, you can skip this step. Otherwise, use the `lambda-image-republish` module to copy the public image to your account/region.
52+
53+
```hcl
54+
module "notification_image_republish" {
55+
source = "../../modules/lambda-image-republish"
56+
57+
source_lambda_repo = "public.ecr.aws/i9p4w7k9/lambdacron-notifications"
58+
source_lambda_tag = "latest"
59+
}
60+
```
61+
62+
If you do this, you can wire in the lambda image URI from the module output as shown in the next step.
63+
64+
On the other hand, if you have your own notification handler image, you can provide the URI directly. To get that, you can find it in the ECR console or use the AWS CLI:
65+
66+
```bash
67+
aws ecr describe-repositories --repository-names "your-repo-name" --query "repositories[0].repositoryUri" --output text
68+
```
69+
70+
71+
### 3. Add the `email-notification` module
72+
73+
You'll connect it to the LambdaCron SNS topic and provide the email settings and templates. You'll also select which result types to send email for. If you leave `result_types` empty or omit it, the notification handler will send emails for all result types. **You will get one email for each notification type**, this will not combine multiple notification types into a single email.
74+
75+
```hcl
76+
module "email_notification" {
77+
source = "../../modules/email-notification"
78+
79+
sns_topic_arn = module.lambdacron.sns_topic_arn
80+
fifo_queue_name = "lambdacron-email.fifo"
81+
lambda_image_uri = module.notification_image_republish.lambda_image_uri_with_digest
82+
83+
result_types = ["example", "ERROR"]
84+
85+
sender = var.email_sender
86+
recipients = var.email_recipients
87+
reply_to = var.email_reply_to
88+
89+
subject_template_file = "${path.module}/templates/email-subject.txt"
90+
text_template_file = "${path.module}/templates/email-body.txt"
91+
html_template_file = "${path.module}/templates/email-body.html"
92+
93+
tags = local.common_tags
94+
}
95+
```
96+
97+
### 4. Plan and apply
98+
99+
Here we use `tofu`, but you could also use `terraform`. This will deploy your infrastructure.
100+
101+
```bash
102+
tofu plan
103+
tofu apply
104+
```
105+
106+
## Validation
107+
108+
There are two relatively easy ways you can trigger the email notification lambda:
109+
110+
1. [Deploy your LambdaCron with `create_test_url`](use-test-url.md) enabled and invoke the test URL.
111+
2. Publish a test message with a `result_type` value included in `result_types`:
112+
113+
```bash
114+
aws sns publish \
115+
--topic-arn "$(tofu output -raw sns_topic_arn)" \
116+
--message '{"message":"Email notification smoke test"}' \
117+
--message-attributes '{"result_type":{"DataType":"String","StringValue":"example"}}' \
118+
--message-group-id "email-smoke-test"
119+
```
120+
121+
Note that the test URL won't create an email unless the result creates a message with a `result_type` value included in your notifier's `result_types` list.
122+
123+
If you receive the email, then it was successful!
124+
125+
If not, here are some troubleshooting tips:
126+
127+
* Check the CloudWatch logs for the notifier Lambda for any errors. In particular, it might complain if you aren't allowed to send email (see setup instructions in [Set Up SES Prerequisites](set-up-ses.md)).
128+
* If using the test URL, check the CloudWatch logs for the scheduled Lambda to confirm that it is working.
129+
* If you don't see logs in the CloudWatch logs for your lambdas, check the Lambda console to confirm that the Lambda was created successfully and that it has the correct triggers.

0 commit comments

Comments
 (0)