apm-wo-analysis/terraform/lambda.tf

228 lines
6.8 KiB
Terraform
Raw 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 {
lambda_identity = {
classifier = [
{
sid = "ReadRaw"
actions = ["s3:GetObject"]
resources = ["${aws_s3_bucket.exports.arn}/raw/*"]
},
{
sid = "WriteAnalytics"
actions = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:AbortMultipartUpload"]
resources = ["${aws_s3_bucket.exports.arn}/analytics/*"]
},
{
sid = "WriteMeta"
actions = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"]
resources = ["${aws_s3_bucket.exports.arn}/meta/*"]
},
{
sid = "ListExports"
actions = ["s3:ListBucket"]
resources = [aws_s3_bucket.exports.arn]
},
{
sid = "AnthropicSecret"
actions = ["secretsmanager:GetSecretValue"]
resources = ["arn:aws:secretsmanager:${var.aws_region}:${local.account_id}:secret:apm-wo-analysis/anthropic-api-key*"]
},
{
sid = "InvokeSlackPost"
actions = ["lambda:InvokeFunction"]
resources = ["arn:aws:lambda:${var.aws_region}:${local.account_id}:function:apm-wo-analysis-slack-post"]
},
{
sid = "DlqSend"
actions = ["sqs:SendMessage"]
resources = [aws_sqs_queue.classifier_dlq.arn]
},
]
slack_post = [
{
sid = "ReadMeta"
actions = ["s3:GetObject"]
resources = ["${aws_s3_bucket.exports.arn}/meta/*"]
},
{
sid = "ListExports"
actions = ["s3:ListBucket"]
resources = [aws_s3_bucket.exports.arn]
},
{
sid = "SlackSecret"
actions = ["secretsmanager:GetSecretValue"]
resources = ["arn:aws:secretsmanager:${var.aws_region}:${local.account_id}:secret:apm-wo-analysis/slack-credentials*"]
},
{
sid = "DashboardParam"
actions = ["ssm:GetParameter"]
resources = ["arn:aws:ssm:${var.aws_region}:${local.account_id}:parameter${local.grafana_url_param}"]
},
]
slack_interactions = [
{
sid = "ReadMeta"
actions = ["s3:GetObject"]
resources = ["${aws_s3_bucket.exports.arn}/meta/*"]
},
{
sid = "ListExports"
actions = ["s3:ListBucket"]
resources = [aws_s3_bucket.exports.arn]
},
{
sid = "SlackSecret"
actions = ["secretsmanager:GetSecretValue"]
resources = ["arn:aws:secretsmanager:${var.aws_region}:${local.account_id}:secret:apm-wo-analysis/slack-credentials*"]
},
{
sid = "DashboardParam"
actions = ["ssm:GetParameter"]
resources = ["arn:aws:ssm:${var.aws_region}:${local.account_id}:parameter${local.grafana_url_param}"]
},
]
}
lambda_env = {
classifier = {
APM_HAIKU_FALLBACK = "on"
SLACK_POST_FUNCTION_NAME = local.functions.slack_post.function_name
}
slack_post = {
SLACK_SECRET_NAME = "apm-wo-analysis/slack-credentials"
DASHBOARD_URL_PARAM = local.grafana_url_param
ANALYTICS_BUCKET = aws_s3_bucket.exports.bucket
}
slack_interactions = {
SLACK_SECRET_NAME = "apm-wo-analysis/slack-credentials"
DASHBOARD_URL_PARAM = local.grafana_url_param
ANALYTICS_BUCKET = aws_s3_bucket.exports.bucket
}
}
}
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.exec_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_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 = "python3.12"
architectures = ["arm64"]
memory_size = each.value.memory_size
timeout = each.value.timeout
layers = each.value.layers
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 "dead_letter_config" {
for_each = each.key == "classifier" ? [aws_sqs_queue.classifier_dlq.arn] : []
content {
target_arn = dead_letter_config.value
}
}
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,
]
}
resource "aws_lambda_permission" "s3_classifier" {
statement_id = "AllowS3InvokeClassifier"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.this["classifier"].function_name
principal = "s3.amazonaws.com"
source_arn = aws_s3_bucket.exports.arn
}
resource "aws_s3_bucket_notification" "raw_exports" {
bucket = aws_s3_bucket.exports.id
lambda_function {
lambda_function_arn = aws_lambda_function.this["classifier"].arn
events = ["s3:ObjectCreated:*"]
filter_prefix = "raw/"
filter_suffix = ".xlsx"
}
lambda_function {
lambda_function_arn = aws_lambda_function.this["classifier"].arn
events = ["s3:ObjectCreated:*"]
filter_prefix = "raw/"
filter_suffix = ".csv"
}
depends_on = [aws_lambda_permission.s3_classifier]
}