Skip to content

Commit f0d0e2f

Browse files
authored
feat: 10GB worker + DLQ depth alarm (#68)
1 parent 24e450c commit f0d0e2f

5 files changed

Lines changed: 134 additions & 0 deletions

File tree

infra/terraform/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ override.tf.json
1010
*_override.tf
1111
*_override.tf.json
1212
# .terraform.lock.hcl IS committed — it pins provider versions for reproducibility.
13+
infra/terraform/notifier/notifier.zip

infra/terraform/monitoring.tf

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# SNS topic that ops alarms publish to. A small Lambda forwards each
2+
# message to the Discord WH_URL webhook so notifications land in the
3+
# same channel as package-processed notices.
4+
resource "aws_sns_topic" "alerts" {
5+
name = "${local.name}-alerts"
6+
}
7+
8+
# Anything in the DLQ means a worker invocation failed twice in a row
9+
# (process_package raised an unhandled exception or the Lambda timed out
10+
# at the 15-min cap). Always worth a look.
11+
resource "aws_cloudwatch_metric_alarm" "worker_dlq_depth" {
12+
alarm_name = "${local.name}-worker-dlq-not-empty"
13+
alarm_description = "A worker invocation failed permanently and the SQS message landed in the DLQ. Check /aws/lambda/${aws_lambda_function.worker.function_name} for the traceback."
14+
comparison_operator = "GreaterThanThreshold"
15+
evaluation_periods = 1
16+
metric_name = "ApproximateNumberOfMessagesVisible"
17+
namespace = "AWS/SQS"
18+
period = 300
19+
statistic = "Maximum"
20+
threshold = 0
21+
treat_missing_data = "notBreaching"
22+
23+
dimensions = {
24+
QueueName = aws_sqs_queue.dlq.name
25+
}
26+
27+
alarm_actions = [aws_sns_topic.alerts.arn]
28+
ok_actions = [aws_sns_topic.alerts.arn]
29+
}
30+
31+
# --- Discord notifier Lambda ---
32+
33+
data "archive_file" "notifier" {
34+
type = "zip"
35+
source_file = "${path.module}/notifier/notifier.py"
36+
output_path = "${path.module}/notifier/notifier.zip"
37+
}
38+
39+
# Pulls the latest WH_URL out of Secrets Manager at apply time so the
40+
# notifier doesn't need its own runtime SM read.
41+
data "aws_secretsmanager_secret_version" "wh_url_current" {
42+
secret_id = aws_secretsmanager_secret.wh_url.id
43+
}
44+
45+
resource "aws_iam_role" "notifier" {
46+
name = "${local.name}-notifier-lambda"
47+
assume_role_policy = data.aws_iam_policy_document.lambda_assume.json
48+
}
49+
50+
resource "aws_iam_role_policy_attachment" "notifier_basic" {
51+
role = aws_iam_role.notifier.name
52+
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
53+
}
54+
55+
resource "aws_cloudwatch_log_group" "notifier" {
56+
name = "/aws/lambda/${local.name}-alert-notifier"
57+
retention_in_days = 30
58+
}
59+
60+
resource "aws_lambda_function" "notifier" {
61+
function_name = "${local.name}-alert-notifier"
62+
role = aws_iam_role.notifier.arn
63+
filename = data.archive_file.notifier.output_path
64+
source_code_hash = data.archive_file.notifier.output_base64sha256
65+
handler = "notifier.handler"
66+
runtime = "python3.13"
67+
timeout = 30
68+
memory_size = 128
69+
70+
environment {
71+
variables = {
72+
WH_URL = data.aws_secretsmanager_secret_version.wh_url_current.secret_string
73+
}
74+
}
75+
76+
depends_on = [
77+
aws_iam_role_policy_attachment.notifier_basic,
78+
aws_cloudwatch_log_group.notifier,
79+
]
80+
}
81+
82+
resource "aws_lambda_permission" "sns_invoke_notifier" {
83+
statement_id = "AllowSNSInvoke"
84+
action = "lambda:InvokeFunction"
85+
function_name = aws_lambda_function.notifier.function_name
86+
principal = "sns.amazonaws.com"
87+
source_arn = aws_sns_topic.alerts.arn
88+
}
89+
90+
resource "aws_sns_topic_subscription" "notifier" {
91+
topic_arn = aws_sns_topic.alerts.arn
92+
protocol = "lambda"
93+
endpoint = aws_lambda_function.notifier.arn
94+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""Forward CloudWatch alarm SNS messages to a Discord webhook.
2+
3+
WH_URL points at the same internal-notification webhook the worker uses
4+
(`send_internal_notification`), so DLQ depth and other ops alarms land
5+
in the same channel as package processing notices.
6+
"""
7+
import json
8+
import os
9+
import urllib.request
10+
11+
12+
def handler(event, context):
13+
wh_url = os.environ["WH_URL"]
14+
15+
for record in event.get("Records", []):
16+
msg = json.loads(record["Sns"]["Message"])
17+
state = msg.get("NewStateValue", "UNKNOWN")
18+
embed = {
19+
"title": f"[{state}] {msg.get('AlarmName', 'unknown alarm')}",
20+
"description": msg.get("NewStateReason") or msg.get("AlarmDescription") or "",
21+
"color": 0xE74C3C if state == "ALARM" else 0x2ECC71 if state == "OK" else 0xF1C40F,
22+
}
23+
body = json.dumps({"embeds": [embed]}).encode()
24+
req = urllib.request.Request(
25+
wh_url,
26+
data=body,
27+
headers={"content-type": "application/json"},
28+
)
29+
urllib.request.urlopen(req, timeout=10).read()

infra/terraform/outputs.tf

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,8 @@ output "fck_nat_eip" {
4141
output "package_data_bucket" {
4242
value = aws_s3_bucket.package_data.id
4343
}
44+
45+
output "alerts_topic_arn" {
46+
description = "SNS topic that the ops alarms publish to. Subscribe an email after first apply."
47+
value = aws_sns_topic.alerts.arn
48+
}

infra/terraform/terraform.ci.tfvars

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,8 @@ image_tag = "bootstrap"
1616

1717
diswho_base_url = "https://diswho.androz2091.fr"
1818
dl_zip_whitelisted_domains = ""
19+
20+
# Bigger memory = proportionally more vCPU. Speeds large-package processing
21+
# without much extra cost (memory is billed per GB-second; faster runs cancel
22+
# out the higher per-second rate).
23+
worker_lambda_memory = 10240

0 commit comments

Comments
 (0)