payments-dashboard/terraform/lambda.tf

251 lines
8.1 KiB
Terraform
Raw Permalink Normal View History

# Terraform owns the function skeletons (role, runtime, memory, environment).
# Code is owned by .github/workflows/deploy.yaml, which uploads
# functions/<name>/<sha>.zip and calls update-function-code. The lifecycle
# block is the seam: an app deploy is not drift, and a Terraform apply never
# rolls the code back to the bootstrap stub.
data "aws_iam_policy_document" "lambda_assume" {
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["lambda.amazonaws.com"]
}
}
}
locals {
table_arn = aws_dynamodb_table.dashboard.arn
cmk_arn = local.dynamodb_cmk_arn
lambda_identity = {
process_csv = [
{
sid = "CsvRead"
actions = ["s3:GetObject", "s3:GetObjectVersion"]
resources = ["${aws_s3_bucket.csv.arn}/*"]
},
{
sid = "DdbCrud"
actions = ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:UpdateItem", "dynamodb:DeleteItem", "dynamodb:Query", "dynamodb:Scan", "dynamodb:BatchGetItem", "dynamodb:BatchWriteItem", "dynamodb:DescribeTable", "dynamodb:ConditionCheckItem"]
resources = [local.table_arn, "${local.table_arn}/*"]
},
{
sid = "Cmk"
actions = ["kms:Decrypt", "kms:GenerateDataKey", "kms:DescribeKey"]
resources = [local.cmk_arn]
},
{
sid = "BoaCheckMgmtSecret"
actions = ["secretsmanager:GetSecretValue"]
resources = [local.secret_arns["payments-dashboard/boa-check-mgmt"]]
},
{
sid = "DlqSend"
actions = ["sqs:SendMessage"]
resources = [aws_sqs_queue.process_csv_dlq.arn]
},
]
slack_app_home = [
{
sid = "DdbRead"
actions = ["dynamodb:GetItem", "dynamodb:Query", "dynamodb:Scan", "dynamodb:BatchGetItem", "dynamodb:DescribeTable"]
resources = [local.table_arn, "${local.table_arn}/*"]
},
{
sid = "CmkDecrypt"
actions = ["kms:Decrypt", "kms:DescribeKey"]
resources = [local.cmk_arn]
},
{
sid = "SlackSecrets"
actions = ["secretsmanager:GetSecretValue"]
resources = [
local.secret_arns["payments-dashboard/slack-bot-token"],
local.secret_arns["payments-dashboard/slack-signing-secret"],
]
},
]
fetch_boa = [
{
sid = "DdbCrud"
actions = ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:UpdateItem", "dynamodb:DeleteItem", "dynamodb:Query", "dynamodb:Scan", "dynamodb:BatchGetItem", "dynamodb:BatchWriteItem", "dynamodb:DescribeTable", "dynamodb:ConditionCheckItem"]
resources = [local.table_arn, "${local.table_arn}/*"]
},
{
sid = "BoaRawPut"
actions = ["s3:PutObject"]
resources = ["${aws_s3_bucket.boa_raw.arn}/*"]
},
{
sid = "Cmk"
actions = ["kms:Decrypt", "kms:GenerateDataKey", "kms:DescribeKey"]
resources = [local.cmk_arn]
},
{
sid = "BoaReportingSecret"
actions = ["secretsmanager:GetSecretValue"]
resources = [local.secret_arns["payments-dashboard/boa-reporting"]]
},
]
expense_receiver = [
{
sid = "InvokeProcessor"
actions = ["lambda:InvokeFunction"]
resources = ["arn:aws:lambda:${var.aws_region}:${local.account_id}:function:payments-expenseProcessor"]
},
{
sid = "ExpenseSigningSecret"
actions = ["secretsmanager:GetSecretValue"]
resources = [local.secret_arns["payments-dashboard/expense-slack-signing-secret"]]
},
]
expense_processor = [
{
sid = "ExpenseBotSecret"
actions = ["secretsmanager:GetSecretValue"]
resources = [local.secret_arns["payments-dashboard/expense-slack-token"]]
},
]
}
lambda_env = {
process_csv = {
TABLE_NAME = aws_dynamodb_table.dashboard.name
BOA_BASE_URL = var.boa_base_url
BOA_CHECK_MGMT_SECRET_NAME = "payments-dashboard/boa-check-mgmt"
}
slack_app_home = {
TABLE_NAME = aws_dynamodb_table.dashboard.name
SLACK_BOT_TOKEN_SECRET_NAME = "payments-dashboard/slack-bot-token"
SLACK_SIGNING_SECRET_NAME = "payments-dashboard/slack-signing-secret"
}
fetch_boa = {
TABLE_NAME = aws_dynamodb_table.dashboard.name
BOA_BASE_URL = var.boa_base_url
BOA_REPORTING_SECRET_NAME = "payments-dashboard/boa-reporting"
BOA_RAW_BUCKET = aws_s3_bucket.boa_raw.id
}
expense_receiver = {
TABLE_NAME = aws_dynamodb_table.dashboard.name
EXPENSE_PROCESSOR_FN = "payments-expenseProcessor"
EXPENSE_SIGNING_SECRET_NAME = "payments-dashboard/expense-slack-signing-secret"
}
expense_processor = {
TABLE_NAME = aws_dynamodb_table.dashboard.name
EXPENSE_BOT_TOKEN_SECRET_NAME = "payments-dashboard/expense-slack-token"
}
}
}
resource "aws_iam_role" "lambda" {
for_each = local.functions
name = each.value.role_name
path = "/tf-managed/"
description = "Lambda execution role for ${each.value.function_name}"
assume_role_policy = data.aws_iam_policy_document.lambda_assume.json
permissions_boundary = aws_iam_policy.lambda_boundary.arn
}
data "aws_iam_policy_document" "lambda" {
for_each = local.functions
dynamic "statement" {
for_each = local.lambda_identity[each.key]
content {
sid = statement.value.sid
effect = "Allow"
actions = statement.value.actions
resources = statement.value.resources
}
}
}
resource "aws_iam_role_policy" "lambda" {
for_each = local.functions
name = each.key
role = aws_iam_role.lambda[each.key].id
policy = data.aws_iam_policy_document.lambda[each.key].json
}
resource "aws_iam_role_policy_attachment" "lambda_basic" {
for_each = local.functions
role = aws_iam_role.lambda[each.key].name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
resource "aws_iam_role_policy_attachment" "lambda_vpc" {
for_each = { for k, v in local.functions : k => v if v.vpc }
role = aws_iam_role.lambda[each.key].name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
}
resource "aws_lambda_function" "this" {
for_each = local.functions
function_name = each.value.function_name
role = aws_iam_role.lambda[each.key].arn
handler = each.value.handler
runtime = "nodejs24.x"
architectures = ["arm64"]
memory_size = 256
timeout = each.value.timeout
s3_bucket = aws_s3_bucket.artifacts.id
s3_key = aws_s3_object.bootstrap_stub.key
source_code_hash = data.archive_file.bootstrap_stub.output_base64sha256
environment {
variables = local.lambda_env[each.key]
}
dynamic "vpc_config" {
for_each = each.value.vpc ? [1] : []
content {
subnet_ids = [aws_subnet.private.id]
security_group_ids = [aws_security_group.lambda.id]
}
}
lifecycle {
ignore_changes = [filename, s3_bucket, s3_key, s3_object_version, source_code_hash]
}
depends_on = [
aws_cloudwatch_log_group.lambda,
aws_iam_role_policy.lambda,
aws_iam_role_policy_attachment.lambda_basic,
aws_iam_role_policy_attachment.lambda_vpc,
aws_nat_gateway.this,
]
}
resource "aws_lambda_function_event_invoke_config" "process_csv" {
function_name = aws_lambda_function.this["process_csv"].function_name
maximum_event_age_in_seconds = 21600
maximum_retry_attempts = 2
destination_config {
on_failure {
destination = aws_sqs_queue.process_csv_dlq.arn
}
}
}
resource "aws_lambda_permission" "s3_csv" {
statement_id = "AllowS3InvokeProcessCsv"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.this["process_csv"].function_name
principal = "s3.amazonaws.com"
source_arn = aws_s3_bucket.csv.arn
source_account = local.account_id
}