procurement-ingest/terraform/wo_shoc.tf
Adam Moussa 419293b8ed
feat(infra): retarget WO writers and ESMs to kebab tables (PLAT-11) (#174)
* feat(infra): retarget WO writers and ESMs to kebab Dynamo tables

Point Lambda env, IAM, emitter streams, and DDB alarms at work-orders /
work-order-comments for the PLAT-11 freeze cutover. Legacy PascalCase
tables remain in state until the decommission soak.

* style(infra): terraform fmt wo_ddb alarm map alignment
2026-08-07 14:07:57 -04:00

348 lines
9.6 KiB
HCL

data "aws_iam_policy_document" "shoc_webhook_kms" {
statement {
sid = "EnableRootAccountPermissions"
effect = "Allow"
actions = ["kms:*"]
resources = ["*"]
principals {
type = "AWS"
identifiers = ["arn:aws:iam::${local.account_id}:root"]
}
}
statement {
sid = "ShocBackendDevDecrypt"
effect = "Allow"
actions = ["kms:Decrypt"]
resources = ["*"]
principals {
type = "AWS"
identifiers = [local.shoc_consumer_role_arn]
}
condition {
test = "StringEquals"
variable = "kms:ViaService"
values = ["secretsmanager.${var.aws_region}.amazonaws.com"]
}
}
statement {
sid = "AllowSecretsManagerViaServiceAccount"
effect = "Allow"
actions = [
"kms:Decrypt",
"kms:Encrypt",
"kms:ReEncrypt*",
"kms:GenerateDataKey*",
]
resources = ["*"]
principals {
type = "AWS"
identifiers = ["arn:aws:iam::${local.account_id}:root"]
}
condition {
test = "StringEquals"
variable = "kms:ViaService"
values = ["secretsmanager.${var.aws_region}.amazonaws.com"]
}
}
statement {
sid = "AllowSecretsManagerGrants"
effect = "Allow"
actions = [
"kms:CreateGrant",
"kms:DescribeKey",
]
resources = ["*"]
principals {
type = "AWS"
identifiers = ["arn:aws:iam::${local.account_id}:root"]
}
condition {
test = "StringEquals"
variable = "kms:ViaService"
values = ["secretsmanager.${var.aws_region}.amazonaws.com"]
}
}
statement {
sid = "ShocHmacRotator"
effect = "Allow"
actions = [
"kms:Decrypt",
"kms:Encrypt",
"kms:ReEncrypt*",
"kms:GenerateDataKey*",
]
resources = ["*"]
principals {
type = "AWS"
identifiers = [aws_iam_role.wo_shoc_hmac_rotator.arn]
}
condition {
test = "StringEquals"
variable = "kms:ViaService"
values = ["secretsmanager.${var.aws_region}.amazonaws.com"]
}
}
statement {
sid = "ShocEmitterDecrypt"
effect = "Allow"
actions = ["kms:Decrypt"]
resources = ["*"]
principals {
type = "AWS"
identifiers = [aws_iam_role.wo_shoc_emitter.arn]
}
condition {
test = "StringEquals"
variable = "kms:ViaService"
values = ["secretsmanager.${var.aws_region}.amazonaws.com"]
}
}
}
resource "aws_kms_key" "shoc_webhook" {
description = "Dedicated CMK for the workorder-ingest/shoc-webhook-hmac secret (cross-account readable by the SHOC backend)"
enable_key_rotation = true
deletion_window_in_days = 7
policy = data.aws_iam_policy_document.shoc_webhook_kms.json
}
resource "aws_kms_alias" "shoc_webhook" {
name = "alias/workorder-ingest-shoc-webhook-kms"
target_key_id = aws_kms_key.shoc_webhook.key_id
}
resource "aws_secretsmanager_secret" "shoc_webhook_hmac" {
name = "workorder-ingest/shoc-webhook-hmac"
description = "HMAC signing keys for the SHOC work-order webhook (docs/shoc-webhook-contract.md section 6)"
kms_key_id = aws_kms_key.shoc_webhook.arn
tags = {
Purpose = "shoc-webhook-hmac"
ManagedBy = "terraform"
}
}
resource "aws_secretsmanager_secret_policy" "shoc_webhook_hmac" {
secret_arn = aws_secretsmanager_secret.shoc_webhook_hmac.arn
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
AWS = local.shoc_consumer_role_arn
}
Action = [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret",
]
Resource = "*"
},
{
Effect = "Deny"
Principal = {
AWS = "arn:aws:iam::${local.account_id}:root"
}
Action = "secretsmanager:DeleteSecret"
Resource = "*"
}
]
})
}
resource "aws_cloudwatch_log_group" "wo_shoc_hmac_rotator" {
name = "/aws/lambda/workorder-shoc-hmac-rotator"
retention_in_days = 60
lifecycle {
prevent_destroy = true
}
}
resource "aws_lambda_function" "wo_shoc_hmac_rotator" {
function_name = "workorder-shoc-hmac-rotator"
role = aws_iam_role.wo_shoc_hmac_rotator.arn
handler = "handler.handler"
runtime = "python3.12"
architectures = ["arm64"]
memory_size = 128
timeout = 60
s3_bucket = aws_s3_bucket.artifacts.id
s3_key = aws_s3_object.lambda["wo_shoc_hmac_rotator"].key
source_code_hash = data.archive_file.lambda["wo_shoc_hmac_rotator"].output_base64sha256
depends_on = [
aws_s3_object.lambda,
aws_cloudwatch_log_group.wo_shoc_hmac_rotator,
aws_iam_role_policy_attachment.wo_shoc_hmac_rotator_basic,
aws_iam_role_policy.wo_shoc_hmac_rotator_secrets,
aws_iam_role_policy.wo_shoc_hmac_rotator_kms,
]
}
resource "aws_lambda_permission" "wo_shoc_hmac_rotator" {
statement_id = "AllowSecretsManagerRotate"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.wo_shoc_hmac_rotator.function_name
principal = "secretsmanager.amazonaws.com"
source_account = local.account_id
source_arn = aws_secretsmanager_secret.shoc_webhook_hmac.arn
}
resource "aws_secretsmanager_secret_rotation" "shoc_webhook_hmac" {
secret_id = aws_secretsmanager_secret.shoc_webhook_hmac.id
rotation_lambda_arn = aws_lambda_function.wo_shoc_hmac_rotator.arn
rotation_rules {
automatically_after_days = 30
}
depends_on = [aws_lambda_permission.wo_shoc_hmac_rotator]
}
resource "aws_sqs_queue" "shoc_emitter_failures" {
name = "workorder-shoc-emitter-failures"
message_retention_seconds = 1209600
sqs_managed_sse_enabled = true
}
resource "aws_sqs_queue_policy" "shoc_emitter_failures" {
queue_url = aws_sqs_queue.shoc_emitter_failures.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Deny"
Principal = { AWS = "*" }
Action = "sqs:*"
Resource = aws_sqs_queue.shoc_emitter_failures.arn
Condition = {
Bool = { "aws:SecureTransport" = "false" }
}
}
]
})
}
resource "aws_sqs_queue" "shoc_emitter_rejected" {
name = "workorder-shoc-emitter-rejected"
message_retention_seconds = 1209600
sqs_managed_sse_enabled = true
}
resource "aws_sqs_queue_policy" "shoc_emitter_rejected" {
queue_url = aws_sqs_queue.shoc_emitter_rejected.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Deny"
Principal = { AWS = "*" }
Action = "sqs:*"
Resource = aws_sqs_queue.shoc_emitter_rejected.arn
Condition = {
Bool = { "aws:SecureTransport" = "false" }
}
}
]
})
}
resource "aws_cloudwatch_log_group" "wo_shoc_emitter" {
name = "/aws/lambda/workorder-shoc-emitter"
retention_in_days = 60
lifecycle {
prevent_destroy = true
}
}
resource "aws_lambda_function" "wo_shoc_emitter" {
function_name = "workorder-shoc-emitter"
role = aws_iam_role.wo_shoc_emitter.arn
handler = "handler.handler"
runtime = "python3.12"
architectures = ["arm64"]
memory_size = 256
timeout = 60
s3_bucket = aws_s3_bucket.artifacts.id
s3_key = aws_s3_object.lambda["wo_shoc_emitter"].key
source_code_hash = data.archive_file.lambda["wo_shoc_emitter"].output_base64sha256
environment {
variables = {
HMAC_SECRET_ARN = var.shoc_hmac_secret_arn
REJECTED_QUEUE_URL = aws_sqs_queue.shoc_emitter_rejected.url
SHOC_WEBHOOK_URL = var.shoc_webhook_url
# Stream ARN classification (PLAT-11); must match ESM source table names.
WORK_ORDERS_TABLE = aws_dynamodb_table.work_orders_kebab.name
COMMENTS_TABLE = aws_dynamodb_table.work_order_comments_kebab.name
}
}
depends_on = [
aws_s3_object.lambda,
aws_cloudwatch_log_group.wo_shoc_emitter,
aws_iam_role_policy_attachment.wo_shoc_emitter_basic,
aws_iam_role_policy.wo_shoc_emitter_streams,
aws_iam_role_policy.wo_shoc_emitter_secrets,
aws_iam_role_policy.wo_shoc_emitter_kms,
aws_iam_role_policy.wo_shoc_emitter_sqs,
]
}
resource "aws_lambda_event_source_mapping" "wo_shoc_emitter_work_orders" {
event_source_arn = aws_dynamodb_table.work_orders_kebab.stream_arn
function_name = aws_lambda_function.wo_shoc_emitter.arn
starting_position = "LATEST"
batch_size = 10
parallelization_factor = 1
maximum_record_age_in_seconds = 86400
maximum_retry_attempts = -1
bisect_batch_on_function_error = false
function_response_types = ["ReportBatchItemFailures"]
enabled = true
destination_config {
on_failure {
destination_arn = aws_sqs_queue.shoc_emitter_failures.arn
}
}
}
resource "aws_lambda_event_source_mapping" "wo_shoc_emitter_comments" {
event_source_arn = aws_dynamodb_table.work_order_comments_kebab.stream_arn
function_name = aws_lambda_function.wo_shoc_emitter.arn
starting_position = "LATEST"
batch_size = 10
parallelization_factor = 1
maximum_record_age_in_seconds = 86400
maximum_retry_attempts = -1
bisect_batch_on_function_error = false
function_response_types = ["ReportBatchItemFailures"]
enabled = true
destination_config {
on_failure {
destination_arn = aws_sqs_queue.shoc_emitter_failures.arn
}
}
}