procurement-ingest/terraform/wo_ddb.tf
Adam Moussa 13efee9682
feat(infra): add kebab WO tables and PLAT-11 cutover tooling (PLAT-11) (#172)
* feat(infra): add kebab WO tables and PLAT-11 cutover tooling

Create empty work-orders/work-order-comments under Terraform while writers
stay on PascalCase; make emitter table classification env-driven and add
same-account migrate/verify plus cutover runbook.

* style(test): ruff-format shoc emitter envelope tests
2026-08-07 13:42:35 -04:00

169 lines
3.9 KiB
HCL

# PLAT-11: PascalCase tables remain authoritative until the freeze cutover.
# Kebab tables are created empty in the prep apply; writers/ESMs stay on legacy
# until docs/plat-11/cutover-runbook.md Phase 2.
resource "aws_dynamodb_table" "work_orders" {
name = "WorkOrders"
billing_mode = "PAY_PER_REQUEST"
hash_key = "work_order_id"
attribute {
name = "work_order_id"
type = "S"
}
stream_enabled = true
stream_view_type = "NEW_AND_OLD_IMAGES"
lifecycle {
prevent_destroy = true
}
}
resource "aws_dynamodb_table" "work_order_comments" {
name = "WorkOrderComments"
billing_mode = "PAY_PER_REQUEST"
hash_key = "work_order_id"
range_key = "comment_id"
attribute {
name = "work_order_id"
type = "S"
}
attribute {
name = "comment_id"
type = "S"
}
stream_enabled = true
stream_view_type = "NEW_AND_OLD_IMAGES"
lifecycle {
prevent_destroy = true
}
}
resource "aws_dynamodb_table" "work_orders_kebab" {
name = "work-orders"
billing_mode = "PAY_PER_REQUEST"
hash_key = "work_order_id"
attribute {
name = "work_order_id"
type = "S"
}
stream_enabled = true
stream_view_type = "NEW_AND_OLD_IMAGES"
lifecycle {
prevent_destroy = true
}
}
resource "aws_dynamodb_table" "work_order_comments_kebab" {
name = "work-order-comments"
billing_mode = "PAY_PER_REQUEST"
hash_key = "work_order_id"
range_key = "comment_id"
attribute {
name = "work_order_id"
type = "S"
}
attribute {
name = "comment_id"
type = "S"
}
stream_enabled = true
stream_view_type = "NEW_AND_OLD_IMAGES"
lifecycle {
prevent_destroy = true
}
}
locals {
# Alarms stay on the live writer tables (legacy until cutover).
wo_ddb_alarm_tables = {
"WorkOrders" = aws_dynamodb_table.work_orders.name
"WorkOrderComments" = aws_dynamodb_table.work_order_comments.name
}
}
resource "aws_cloudwatch_metric_alarm" "wo_ddb_throttles" {
for_each = local.wo_ddb_alarm_tables
alarm_name = "${each.key}-throttles"
alarm_description = "${each.key} DynamoDB throttled requests"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
threshold = 0
treat_missing_data = "notBreaching"
alarm_actions = [data.aws_sns_topic.site_alerts.arn]
metric_query {
id = "expr_1"
expression = local.ddb_throttle_expr
label = "Sum of throttled requests across all operations"
return_data = true
}
dynamic "metric_query" {
for_each = local.ddb_alarm_operations
content {
id = lower(metric_query.value)
metric {
metric_name = "ThrottledRequests"
namespace = "AWS/DynamoDB"
period = 300
stat = "Sum"
dimensions = {
TableName = each.value
Operation = metric_query.value
}
}
return_data = false
}
}
}
resource "aws_cloudwatch_metric_alarm" "wo_ddb_system_errors" {
for_each = local.wo_ddb_alarm_tables
alarm_name = "${each.key}-system-errors"
alarm_description = "${each.key} DynamoDB server-side (5xx) errors"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
threshold = 0
treat_missing_data = "notBreaching"
alarm_actions = [data.aws_sns_topic.site_alerts.arn]
metric_query {
id = "expr_1"
expression = local.ddb_throttle_expr
label = "Sum of system errors across all operations"
return_data = true
}
dynamic "metric_query" {
for_each = local.ddb_alarm_operations
content {
id = lower(metric_query.value)
metric {
metric_name = "SystemErrors"
namespace = "AWS/DynamoDB"
period = 300
stat = "Sum"
dimensions = {
TableName = each.value
Operation = metric_query.value
}
}
return_data = false
}
}
}