Repository navigation
feat(monitoring): alert on ECS services that cannot start tasks - #160
Conversation
Terraform Plan🚧 Changes detected — Plan: 2 to add, 2 to change, 0 to destroy. Plan outputLLM ReviewRisk: 🟢 LOW Plan updates two Lambda functions' code (source hash change only) and adds a CloudWatch Event rule and SNS target to alert on ECS task start impairment in the platform cluster; 0 destroys.
|
Terraform Plan🚧 Changes detected — Plan: 2 to add, 2 to change, 0 to destroy. Plan outputLLM ReviewRisk: 🟢 LOW Plan updates two existing Lambda functions (code-only source_code_hash change) and adds an EventBridge rule and target that forward ECS task-start-impaired events from the javabin-platform cluster to the javabin-security SNS topic; nothing is destroyed.
|
… control (#159) ## Why A cost audit (2026-10, live Cost Explorer/CloudWatch/Config data) found that the cost guardrails we already run have gaps: - **The account budget fires every month.** It is $500, but steady spend is about $550 a month (including 25% VAT), and the ACTUAL 80%/100% alerts trigger on that. There is no forecast alert. - **`javabin-billing-alarm` (us-east-1) does nothing.** It has been `INSUFFICIENT_DATA` since 2026-03-26 because the AWS/Billing EstimatedCharges metric is not enabled for the account. Its SNS topic has 0 subscribers, and its $200 threshold is below monthly spend anyway. - **AWS Config churn.** Over 30 days Config recorded about 12.6k configuration items ($0.003 each): NetworkInterface 2451, VPC 2446, Subnet 2446, SecurityGroup 2445. ENI + VPC + Subnet is about 7.3k items (~$22). Each Fargate task start/stop re-records the ENI and its relationships. Almost all of it comes from `testteam-platform-test-app` crash-looping (~80 starts/day), so **fixing that service is the main saving**; this PR only trims what remains. - **The 15 `/aws/lambda/javabin-*` log groups and auth-claims never expire.** Lambda auto-created them, and Terraform doesn't manage them. - **The Config delivery bucket has no lifecycle rule.** ## What - `cost-analytics`: - Budget raised to $600. - Notifications are now FORECASTED >100%, ACTUAL >100% and ACTUAL >150%, all to `javabin-alerts`, which the slack-alert Lambda posts to #javabin-cost-alerts. - Removed `aws_cloudwatch_metric_alarm.billing`, `aws_sns_topic.billing_alarm` and the `billing_alarm_threshold_usd` variable. The us-east-1 provider alias stays because the CUR export uses it. - `monitoring`: - Config recorder still records all supported types, but `AWS::EC2::NetworkInterface`, `AWS::EC2::VPC` and `AWS::EC2::Subnet` use a DAILY override. Security groups stay CONTINUOUS. ENIs are deliberately **not** excluded: Security Hub **EC2.180** (ENI source/destination check, enabled) evaluates `AWS::EC2::NetworkInterface`, and **Config.1** fails when an enabled control's resource type is not recorded. Trade-off: EC2.180 evaluates daily instead of on every change, and an ENI created and deleted within one day may never be recorded. Expected saving ~$15–20/mo while the crash loop lasts, a few dollars a month after it is fixed. - The `javabin-config-<acct>` bucket now expires objects after 730 days. The bucket is not versioned. - `lambdas/log-groups.tf` and `identity/log-groups.tf`: `aws_cloudwatch_log_group` with 365-day retention for every platform Lambda. - `imports.tf`: `for_each` import blocks (TF 1.7) adopt the 14 existing groups plus auth-claims. budget-enforcer has never logged, so its group is created fresh. - `scripts/tests/test_lambda_log_groups.py`: scans every `terraform/platform/**/*.tf` and fails if a `${var.project}-*` Lambda has no log group (shared `local.platform_lambda_names` or an explicit `/aws/lambda/...` group in its module). - Docs: - New `docs/cost-management.md` covering alerts, tags, CUR, built-in controls and why Config records ENIs daily, and the post-apply checks. - Updated CLAUDE.md (file table, a separate account-budget alert-routing line so it doesn't conflict with #160, stale cost-allocation-tag note), `docs/platform-modules.md` and `docs/lambda-functions.md`. Not changed, on purpose: - The anomaly monitor (SERVICE, $10 IMMEDIATE) is adequate. - Cost-allocation tags are already Active. - ECR lifecycle and app log retention (30 days) already exist. ## Expected plan - `module.cost_analytics.aws_budgets_budget.account`: updated in place. - `module.cost_analytics.aws_cloudwatch_metric_alarm.billing` and `aws_sns_topic.billing_alarm`: **destroyed**. Both are inert, as explained above. - `module.monitoring.aws_config_configuration_recorder.main`: updated in place. - `module.monitoring.aws_s3_bucket_lifecycle_configuration.config_logs`: created. - 15 log groups imported (retention updated from never to 365 days) and 1 created (budget-enforcer). ## How verified - `terraform fmt -recursive -check`, then `terraform init -backend=false` and `terraform validate` in `terraform/platform`. All pass. - `python3 -m unittest discover -s scripts/tests` (46 OK) and `-s terraform/lambda-src/tests` (OK). Negative check: renaming the auth-claims log group makes the guard test fail. - `git merge-tree` against `feat/ecs-impaired-alert` (#160): no conflict. - Live: Config.1 currently FAILED for IAM global types + custom recorder role (pre-existing); EC2.180 rule scoped to `AWS::EC2::NetworkInterface`. - Live checks: the `javabin-config-*` bucket is unversioned with no lifecycle, the recorder is `ALL_SUPPORTED_RESOURCE_TYPES` + CONTINUOUS, and the list of existing `/aws/lambda/javabin-*` log groups matches the import list. ## Follow-ups - 7 days after apply: - Check `AWS/Config ConfigurationItemsRecorded` per ResourceType. If SecurityGroup still dominates, add it to the DAILY override. - Security Hub EC2.180 still gets fresh evaluations (not NO_DATA). - Security Hub Config.1 has no new reason naming `AWS::EC2::NetworkInterface`. - Scale `testteam-platform-test-app` to 0, or re-push its image. It is the root cause of the Config churn. - Lower `account_budget_usd` as the legacy cleanup lands. The cleanup is in the cost audit report: idle EB environments, Default-ALB, Coolify, NAT. - The import blocks can be dropped after the first apply. - If the budget-enforcer Lambda runs before this is applied, its log group will already exist and the create will fail. In that case, add it to the import list. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
EventBridge rule on SERVICE_TASK_START_IMPAIRED for the platform cluster, routed through javabin-security to #javabin-infra-alerts. slack-alert formats it and dedups once per service per UTC day in the alert-dedup table. A crash-looping service otherwise burns Fargate starts, image pulls and AWS Config items unnoticed (testteam-platform-test-app, ~80 attempts/day). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
02d1507 to
cc43e1d
Compare
Terraform Plan🚧 Changes detected — Plan: 2 to add, 3 to change, 0 to destroy. Plan outputLLM ReviewRisk: 🟢 LOW Plan makes 2 additive changes (a CloudWatch EventBridge rule and target for ECS task start-impaired events forwarded to SNS) and 3 in-place updates (Google identity provider authorize URL gaining hd=java.no restriction and prompt parameter, and two Lambda code updates), with 0 destroys.
|
Why
testteam-platform-test-apphas been crash-looping for weeks with desired=1 and running=0, because of aCannotPullContainerError(its ECR repo is empty). That is about 80 failed task starts a day, and nobody noticed. Each attempt creates and deletes a Fargate ENI, which AWS Config records. This was the main driver of about $29/mo in Config charges found in the 2026-10 cost audit. PR #159 reduces the Config side. This PR makes the crash loop itself visible.What
monitoring/main.tf: new EventBridge rulejavabin-ecs-task-start-impaired. It matchessource=aws.ecs,detail-type=ECS Service ActionandeventName=SERVICE_TASK_START_IMPAIRED, for thejavabin-platformcluster only (legacy CDK clusters are excluded). The target is thejavabin-securitySNS topic, whose policy already allowsevents.amazonaws.com. slack-alert sends non-finding events from that topic to #javabin-infra-alerts. I chose that channel over the cost channel because this is an operational problem.slack_alert/handler.py:format_ecs_service_impairedformatter, routed before the generic EventBridge branch. Without it, the event would fall through to the IAM-change formatter.javabin-alert-deduptable, keyed per service ARN per UTC day. ECS keeps emitting the event while the service is impaired.terraform/lambda-src/tests/test_slack_alert_ecs.pychecks the formatting, once-per-day dedup per service, and that other ECS service actions are ignored.docs/lambda-functions.md.Expected plan
2 to add:
aws_cloudwatch_event_rule.ecs_task_start_impairedandaws_cloudwatch_event_target.ecs_task_start_impaired_sns. 1 in-place update: the slack-alert Lambda code. No IAM changes, because slack-alert already has GetItem/PutItem on the dedup table.How verified
python3 -m unittest discover -s terraform/lambda-src/tests(67 OK, 3 new) and-s scripts/testspass.terraform fmt -recursive -check, thenterraform init -backend=falseandterraform validatepass.Follow-ups
testteam-platform-test-app. Scale it to 0 or push its image.monitoring/main.tf, but in different blocks.🤖 Generated with Claude Code