mirror of
https://github.com/Sea-Haven-Industries/procurement-ingest.git
synced 2026-10-04 10:11:57 +00:00
fix(cdk): explicit Lambda LogGroups replace log_retention (INFRA-114) (#126)
Some checks are pending
Deploy / deploy (push) Waiting to run
Some checks are pending
Deploy / deploy (push) Waiting to run
First seahaven-prod deploy failed CREATE on the sender-auth MetricFilter: it imported /aws/lambda/<fn> by name, which pre-existed in mgmt but not in a fresh account. All 5 functions now get an explicit logs.LogGroup (TWO_MONTHS, RETAIN) via common.make_function_log_group, and the metric filter takes the construct so CFN orders it after the group exists. Also removes the deprecated LogRetention custom resource and its wildcard logs:PutRetentionPolicy role (CKV_AWS_111). Function roles keep AWSLambdaBasicExecutionRole (verified in the synthesized template), so log-write permissions are unchanged; cross-review's grant_write FIX was a false positive on that basis. Supersedes PR #84, which hardcoded the mgmt logs-CMK ARN and predates the common.py refactor. mgmt collision note: these CREATEs would collide with the pre-existing groups in mgmt; acceptable because the deploy secret now targets prod and mgmt is frozen pending decommission.
This commit is contained in:
parent
073201f633
commit
00d0d32337
3 changed files with 65 additions and 20 deletions
|
|
@ -73,7 +73,36 @@ def add_ddb_alarms(scope, id_prefix, table, alarm_name_prefix, alarm_topic):
|
||||||
).add_alarm_action(cw_actions.SnsAction(alarm_topic))
|
).add_alarm_action(cw_actions.SnsAction(alarm_topic))
|
||||||
|
|
||||||
|
|
||||||
def add_sender_auth_rejected_alarm(scope, id_prefix, function_name, alarm_topic):
|
def make_function_log_group(scope, id_prefix, function_name):
|
||||||
|
"""Explicit log group for a Lambda, replacing the deprecated
|
||||||
|
``log_retention`` prop (INFRA-114).
|
||||||
|
|
||||||
|
Making the group a real stack resource (a) drops the LogRetention custom
|
||||||
|
resource whose role carried wildcard ``logs:PutRetentionPolicy`` (checkov
|
||||||
|
CKV_AWS_111), and (b) makes the group an orderable CFN dependency:
|
||||||
|
consumers like the sender-auth metric filter now deploy AFTER the group
|
||||||
|
exists. The previous by-name import raced group creation in a fresh
|
||||||
|
account and failed the first seahaven-prod deploy (the mgmt account
|
||||||
|
masked this because its groups predated the filter).
|
||||||
|
|
||||||
|
RETAIN matches the repo convention for stateful resources and mirrors the
|
||||||
|
old behavior (LogRetention never deleted groups on stack delete). NOTE:
|
||||||
|
in an account where ``/aws/lambda/<fn>`` already exists out-of-band
|
||||||
|
(mgmt), deploying this CREATE would collide -- acceptable because mgmt is
|
||||||
|
frozen post-migration and never redeployed from main.
|
||||||
|
"""
|
||||||
|
return logs.LogGroup(
|
||||||
|
scope,
|
||||||
|
f"{id_prefix}LogGroup",
|
||||||
|
log_group_name=f"/aws/lambda/{function_name}",
|
||||||
|
retention=logs.RetentionDays.TWO_MONTHS,
|
||||||
|
removal_policy=RemovalPolicy.RETAIN,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def add_sender_auth_rejected_alarm(
|
||||||
|
scope, id_prefix, function_name, alarm_topic, log_group
|
||||||
|
):
|
||||||
"""Metric-filter + alarm on ``sender_auth_rejected`` warnings (INFRA-107).
|
"""Metric-filter + alarm on ``sender_auth_rejected`` warnings (INFRA-107).
|
||||||
|
|
||||||
A rejected inbound email is skipped without erroring the invocation, so it
|
A rejected inbound email is skipped without erroring the invocation, so it
|
||||||
|
|
@ -84,21 +113,18 @@ def add_sender_auth_rejected_alarm(scope, id_prefix, function_name, alarm_topic)
|
||||||
mail while the pipeline reports healthy.
|
mail while the pipeline reports healthy.
|
||||||
|
|
||||||
ALARM-only SnsAction to site-alerts; no OK action. The metric filter reads
|
ALARM-only SnsAction to site-alerts; no OK action. The metric filter reads
|
||||||
the function's own log group (imported by the deterministic
|
the function's own log group, passed in as the EXPLICIT LogGroup construct
|
||||||
``/aws/lambda/<fn>`` name, created by the function's log_retention). A plain
|
(from ``make_function_log_group``) so CFN orders the filter after the
|
||||||
substring pattern is used because Lambda prefixes each line with its own
|
group exists -- a by-name import here failed the first fresh-account
|
||||||
level/timestamp/request-id, so the JSON payload is not a standalone JSON
|
deploy. A plain substring pattern is used because Lambda prefixes each
|
||||||
log event a `{$.event=...}` pattern could match.
|
line with its own level/timestamp/request-id, so the JSON payload is not
|
||||||
|
a standalone JSON log event a `{$.event=...}` pattern could match.
|
||||||
"""
|
"""
|
||||||
metric_name = f"{function_name}-sender-auth-rejected"
|
metric_name = f"{function_name}-sender-auth-rejected"
|
||||||
logs.MetricFilter(
|
logs.MetricFilter(
|
||||||
scope,
|
scope,
|
||||||
f"{id_prefix}SenderAuthRejectedFilter",
|
f"{id_prefix}SenderAuthRejectedFilter",
|
||||||
log_group=logs.LogGroup.from_log_group_name(
|
log_group=log_group,
|
||||||
scope,
|
|
||||||
f"{id_prefix}LogGroup",
|
|
||||||
f"/aws/lambda/{function_name}",
|
|
||||||
),
|
|
||||||
filter_pattern=logs.FilterPattern.literal('"sender_auth_rejected"'),
|
filter_pattern=logs.FilterPattern.literal('"sender_auth_rejected"'),
|
||||||
metric_namespace=_SENDER_AUTH_METRIC_NAMESPACE,
|
metric_namespace=_SENDER_AUTH_METRIC_NAMESPACE,
|
||||||
metric_name=metric_name,
|
metric_name=metric_name,
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ from aws_cdk import (
|
||||||
aws_kms as kms,
|
aws_kms as kms,
|
||||||
aws_lambda as lambda_,
|
aws_lambda as lambda_,
|
||||||
aws_lambda_event_sources as lambda_event_sources,
|
aws_lambda_event_sources as lambda_event_sources,
|
||||||
aws_logs as logs,
|
|
||||||
aws_s3 as s3,
|
aws_s3 as s3,
|
||||||
aws_s3_notifications as s3n,
|
aws_s3_notifications as s3n,
|
||||||
aws_ses as ses,
|
aws_ses as ses,
|
||||||
|
|
@ -91,6 +90,9 @@ class PoIngestStack(Stack):
|
||||||
email_processor_dlq = common.make_processor_dlq(self, "EmailProcessorDlq")
|
email_processor_dlq = common.make_processor_dlq(self, "EmailProcessorDlq")
|
||||||
|
|
||||||
# --- Lambda function ---
|
# --- Lambda function ---
|
||||||
|
email_processor_log_group = common.make_function_log_group(
|
||||||
|
self, "EmailProcessor", "po-email-processor"
|
||||||
|
)
|
||||||
email_processor = lambda_.Function(
|
email_processor = lambda_.Function(
|
||||||
self,
|
self,
|
||||||
"EmailProcessor",
|
"EmailProcessor",
|
||||||
|
|
@ -134,7 +136,7 @@ class PoIngestStack(Stack):
|
||||||
),
|
),
|
||||||
timeout=Duration.seconds(60),
|
timeout=Duration.seconds(60),
|
||||||
memory_size=256,
|
memory_size=256,
|
||||||
log_retention=logs.RetentionDays.TWO_MONTHS,
|
log_group=email_processor_log_group,
|
||||||
dead_letter_queue=email_processor_dlq,
|
dead_letter_queue=email_processor_dlq,
|
||||||
environment={
|
environment={
|
||||||
"PO_TABLE": "purchase-orders",
|
"PO_TABLE": "purchase-orders",
|
||||||
|
|
@ -196,7 +198,11 @@ class PoIngestStack(Stack):
|
||||||
# false-reject storm pages instead of vanishing. default_value=0 keeps the
|
# false-reject storm pages instead of vanishing. default_value=0 keeps the
|
||||||
# series populated (alarm stays OK, never INSUFFICIENT_DATA) between events.
|
# series populated (alarm stays OK, never INSUFFICIENT_DATA) between events.
|
||||||
common.add_sender_auth_rejected_alarm(
|
common.add_sender_auth_rejected_alarm(
|
||||||
self, "EmailProcessor", "po-email-processor", alarm_topic
|
self,
|
||||||
|
"EmailProcessor",
|
||||||
|
"po-email-processor",
|
||||||
|
alarm_topic,
|
||||||
|
email_processor_log_group,
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- Template fallback-rate alarm: po-email-processor ---
|
# --- Template fallback-rate alarm: po-email-processor ---
|
||||||
|
|
@ -372,6 +378,7 @@ class PoIngestStack(Stack):
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- Web UI Lambda ---
|
# --- Web UI Lambda ---
|
||||||
|
web_ui_log_group = common.make_function_log_group(self, "WebUI", "po-web-ui")
|
||||||
web_ui = lambda_.Function(
|
web_ui = lambda_.Function(
|
||||||
self,
|
self,
|
||||||
"WebUI",
|
"WebUI",
|
||||||
|
|
@ -409,7 +416,7 @@ class PoIngestStack(Stack):
|
||||||
),
|
),
|
||||||
timeout=Duration.seconds(60),
|
timeout=Duration.seconds(60),
|
||||||
memory_size=256,
|
memory_size=256,
|
||||||
log_retention=logs.RetentionDays.TWO_MONTHS,
|
log_group=web_ui_log_group,
|
||||||
environment={
|
environment={
|
||||||
"PO_TABLE": "purchase-orders",
|
"PO_TABLE": "purchase-orders",
|
||||||
# Defense-in-depth shared secret for the web UI handler. The
|
# Defense-in-depth shared secret for the web UI handler. The
|
||||||
|
|
@ -464,6 +471,9 @@ class PoIngestStack(Stack):
|
||||||
# 518 WCU of write amplification. Re-add if a state-level query path ships.
|
# 518 WCU of write amplification. Re-add if a state-level query path ships.
|
||||||
|
|
||||||
# --- Site extractor Lambda (DynamoDB Streams → verified-sites) ---
|
# --- Site extractor Lambda (DynamoDB Streams → verified-sites) ---
|
||||||
|
site_extractor_log_group = common.make_function_log_group(
|
||||||
|
self, "SiteExtractor", "po-ingest-site-extractor"
|
||||||
|
)
|
||||||
site_extractor = lambda_.Function(
|
site_extractor = lambda_.Function(
|
||||||
self,
|
self,
|
||||||
"SiteExtractor",
|
"SiteExtractor",
|
||||||
|
|
@ -483,7 +493,7 @@ class PoIngestStack(Stack):
|
||||||
),
|
),
|
||||||
timeout=Duration.seconds(60),
|
timeout=Duration.seconds(60),
|
||||||
memory_size=256,
|
memory_size=256,
|
||||||
log_retention=logs.RetentionDays.TWO_MONTHS,
|
log_group=site_extractor_log_group,
|
||||||
environment={
|
environment={
|
||||||
"VERIFIED_SITES_TABLE": verified_sites_table.table_name,
|
"VERIFIED_SITES_TABLE": verified_sites_table.table_name,
|
||||||
"PENDING_REVIEW_TABLE": "pending-site-review",
|
"PENDING_REVIEW_TABLE": "pending-site-review",
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ from aws_cdk import (
|
||||||
aws_cloudwatch_actions as cw_actions,
|
aws_cloudwatch_actions as cw_actions,
|
||||||
aws_dynamodb as dynamodb,
|
aws_dynamodb as dynamodb,
|
||||||
aws_lambda as lambda_,
|
aws_lambda as lambda_,
|
||||||
aws_logs as logs,
|
|
||||||
aws_s3 as s3,
|
aws_s3 as s3,
|
||||||
aws_s3_notifications as s3n,
|
aws_s3_notifications as s3n,
|
||||||
aws_ses as ses,
|
aws_ses as ses,
|
||||||
|
|
@ -89,6 +88,9 @@ class WorkorderIngestStack(Stack):
|
||||||
email_processor_dlq = common.make_processor_dlq(self, "EmailProcessorDlq")
|
email_processor_dlq = common.make_processor_dlq(self, "EmailProcessorDlq")
|
||||||
|
|
||||||
# --- Lambda function ---
|
# --- Lambda function ---
|
||||||
|
email_processor_log_group = common.make_function_log_group(
|
||||||
|
self, "EmailProcessor", "workorder-email-processor"
|
||||||
|
)
|
||||||
email_processor = lambda_.Function(
|
email_processor = lambda_.Function(
|
||||||
self,
|
self,
|
||||||
"EmailProcessor",
|
"EmailProcessor",
|
||||||
|
|
@ -119,7 +121,7 @@ class WorkorderIngestStack(Stack):
|
||||||
),
|
),
|
||||||
timeout=Duration.seconds(60),
|
timeout=Duration.seconds(60),
|
||||||
memory_size=256,
|
memory_size=256,
|
||||||
log_retention=logs.RetentionDays.TWO_MONTHS,
|
log_group=email_processor_log_group,
|
||||||
dead_letter_queue=email_processor_dlq,
|
dead_letter_queue=email_processor_dlq,
|
||||||
environment={
|
environment={
|
||||||
"WORK_ORDERS_TABLE": work_orders_table.table_name,
|
"WORK_ORDERS_TABLE": work_orders_table.table_name,
|
||||||
|
|
@ -194,7 +196,11 @@ class WorkorderIngestStack(Stack):
|
||||||
# dropped. This metric filter + alarm turns those warnings into a paging
|
# dropped. This metric filter + alarm turns those warnings into a paging
|
||||||
# signal so a false-reject storm surfaces instead of a silent outage.
|
# signal so a false-reject storm surfaces instead of a silent outage.
|
||||||
common.add_sender_auth_rejected_alarm(
|
common.add_sender_auth_rejected_alarm(
|
||||||
self, "EmailProcessor", "workorder-email-processor", alarm_topic
|
self,
|
||||||
|
"EmailProcessor",
|
||||||
|
"workorder-email-processor",
|
||||||
|
alarm_topic,
|
||||||
|
email_processor_log_group,
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- Template fallback-rate alarm: workorder-email-processor ---
|
# --- Template fallback-rate alarm: workorder-email-processor ---
|
||||||
|
|
@ -304,6 +310,9 @@ class WorkorderIngestStack(Stack):
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- Web UI Lambda ---
|
# --- Web UI Lambda ---
|
||||||
|
web_ui_log_group = common.make_function_log_group(
|
||||||
|
self, "WebUI", "workorder-web-ui"
|
||||||
|
)
|
||||||
web_ui = lambda_.Function(
|
web_ui = lambda_.Function(
|
||||||
self,
|
self,
|
||||||
"WebUI",
|
"WebUI",
|
||||||
|
|
@ -340,7 +349,7 @@ class WorkorderIngestStack(Stack):
|
||||||
),
|
),
|
||||||
timeout=Duration.seconds(15),
|
timeout=Duration.seconds(15),
|
||||||
memory_size=128,
|
memory_size=128,
|
||||||
log_retention=logs.RetentionDays.TWO_MONTHS,
|
log_group=web_ui_log_group,
|
||||||
environment={
|
environment={
|
||||||
"WORK_ORDERS_TABLE": work_orders_table.table_name,
|
"WORK_ORDERS_TABLE": work_orders_table.table_name,
|
||||||
"COMMENTS_TABLE": comments_table.table_name,
|
"COMMENTS_TABLE": comments_table.table_name,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue