# Terraform owns the function skeletons (role, runtime, memory, environment). # Code is owned by .github/workflows/deploy.yaml, which uploads # functions//.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. GIT_SHA is written into # shared/build_info.py at zip time, not set here. 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.shifts.arn lambda_identity = { slack_bot = [ { 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}/*"] condition = null }, { sid = "Secrets" actions = ["secretsmanager:GetSecretValue"] resources = ["arn:aws:secretsmanager:${var.aws_region}:${local.account_id}:secret:afterhours-shift-manager/*"] condition = null }, { sid = "HolidaySchedules" actions = ["scheduler:CreateSchedule", "scheduler:DeleteSchedule", "scheduler:GetSchedule"] resources = ["arn:aws:scheduler:${var.aws_region}:${local.account_id}:schedule/default/holiday-*"] condition = null }, { sid = "PassHolidayScheduler" actions = ["iam:PassRole"] resources = [local.holiday_scheduler_role_arn] condition = { test = "StringEquals" variable = "iam:PassedToService" values = ["scheduler.amazonaws.com"] } }, { sid = "InvokeHolidayRouter" actions = ["lambda:InvokeFunction"] resources = [local.holiday_router_arn] condition = null }, ] weekly_post = [ { 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}/*"] condition = null }, { sid = "Secrets" actions = ["secretsmanager:GetSecretValue"] resources = ["arn:aws:secretsmanager:${var.aws_region}:${local.account_id}:secret:afterhours-shift-manager/slack-bot-token-*"] condition = null }, { sid = "CheckcomponentsSend" actions = ["sqs:SendMessage"] resources = [var.checkcomponents_queue_arn] condition = null }, ] roster_sync = [ { 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}/*"] condition = null }, { sid = "Secrets" actions = ["secretsmanager:GetSecretValue"] resources = ["arn:aws:secretsmanager:${var.aws_region}:${local.account_id}:secret:afterhours-shift-manager/3cx-*"] condition = null }, ] roster_api = [ { sid = "DdbWrite" actions = ["dynamodb:UpdateItem", "dynamodb:DeleteItem"] resources = [local.table_arn] condition = null }, { sid = "Secrets" actions = ["secretsmanager:GetSecretValue"] resources = ["arn:aws:secretsmanager:${var.aws_region}:${local.account_id}:secret:afterhours-shift-manager/roster-api-token-*"] condition = null }, ] ring_scheduler = [ { sid = "DdbRead" actions = ["dynamodb:GetItem", "dynamodb:Query", "dynamodb:Scan", "dynamodb:BatchGetItem", "dynamodb:DescribeTable"] resources = [local.table_arn, "${local.table_arn}/*"] condition = null }, { sid = "Secrets" actions = ["secretsmanager:GetSecretValue"] resources = ["arn:aws:secretsmanager:${var.aws_region}:${local.account_id}:secret:afterhours-shift-manager/3cx-*"] condition = null }, ] holiday_router = [ { 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}/*"] condition = null }, { sid = "Secrets" actions = ["secretsmanager:GetSecretValue"] resources = ["arn:aws:secretsmanager:${var.aws_region}:${local.account_id}:secret:afterhours-shift-manager/3cx-*"] condition = null }, ] release_notifier = [ { sid = "Secrets" actions = ["secretsmanager:GetSecretValue"] resources = ["arn:aws:secretsmanager:${var.aws_region}:${local.account_id}:secret:afterhours-shift-manager/slack-bot-token-*"] condition = null }, ] portal_api = [ { 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}/*"] condition = null }, { sid = "Secrets" actions = ["secretsmanager:GetSecretValue"] resources = ["arn:aws:secretsmanager:${var.aws_region}:${local.account_id}:secret:afterhours-shift-manager/*"] condition = null }, { sid = "HolidaySchedules" actions = ["scheduler:CreateSchedule", "scheduler:DeleteSchedule", "scheduler:GetSchedule"] resources = ["arn:aws:scheduler:${var.aws_region}:${local.account_id}:schedule/default/holiday-*"] condition = null }, { sid = "PassHolidayScheduler" actions = ["iam:PassRole"] resources = [local.holiday_scheduler_role_arn] condition = { test = "StringEquals" variable = "iam:PassedToService" values = ["scheduler.amazonaws.com"] } }, { sid = "InvokeHolidayRouter" actions = ["lambda:InvokeFunction"] resources = [local.holiday_router_arn] condition = null }, ] } lambda_env = { slack_bot = { SHIFT_TABLE = aws_dynamodb_table.shifts.name SLACK_BOT_TOKEN_SECRET = "afterhours-shift-manager/slack-bot-token" SLACK_SIGNING_SECRET = "afterhours-shift-manager/slack-signing-secret" SHIFT_CHANNEL = var.shift_channel TCX_SECRET_PREFIX = "afterhours-shift-manager/3cx-" QUEUE_NUMBER = var.queue_number TZ = var.timezone HOLIDAY_ROUTER_ARN = local.holiday_router_arn HOLIDAY_SCHEDULER_ROLE_ARN = local.holiday_scheduler_role_arn SENTRY_DSN = var.sentry_dsn } weekly_post = { SHIFT_TABLE = aws_dynamodb_table.shifts.name SLACK_BOT_TOKEN_SECRET = "afterhours-shift-manager/slack-bot-token" SHIFT_CHANNEL = var.shift_channel PAY_REPORT_USER = var.pay_report_user TZ = var.timezone CHECKCOMPONENTS_QUEUE_URL = var.checkcomponents_queue_url SENTRY_DSN = var.sentry_dsn } roster_sync = { SHIFT_TABLE = aws_dynamodb_table.shifts.name TCX_SECRET_PREFIX = "afterhours-shift-manager/3cx-" SYNC_GROUP = "DEFAULT" TZ = var.timezone SENTRY_DSN = var.sentry_dsn } roster_api = { SHIFT_TABLE = aws_dynamodb_table.shifts.name ROSTER_API_TOKEN_SECRET = "afterhours-shift-manager/roster-api-token" TZ = var.timezone SENTRY_DSN = var.sentry_dsn } ring_scheduler = { SHIFT_TABLE = aws_dynamodb_table.shifts.name TCX_SECRET_PREFIX = "afterhours-shift-manager/3cx-" QUEUE_NUMBER = var.queue_number TZ = var.timezone SENTRY_DSN = var.sentry_dsn } holiday_router = { SHIFT_TABLE = aws_dynamodb_table.shifts.name TCX_SECRET_PREFIX = "afterhours-shift-manager/3cx-" TZ = var.timezone SENTRY_DSN = var.sentry_dsn } release_notifier = { SLACK_BOT_TOKEN_SECRET = "afterhours-shift-manager/slack-bot-token" SHIFT_CHANNEL = var.shift_channel TZ = var.timezone SENTRY_DSN = var.sentry_dsn } portal_api = { SHIFT_TABLE = aws_dynamodb_table.shifts.name SLACK_BOT_TOKEN_SECRET = "afterhours-shift-manager/slack-bot-token" SHIFT_CHANNEL = var.shift_channel TCX_SECRET_PREFIX = "afterhours-shift-manager/3cx-" QUEUE_NUMBER = var.queue_number TZ = var.timezone HOLIDAY_ROUTER_ARN = local.holiday_router_arn HOLIDAY_SCHEDULER_ROLE_ARN = local.holiday_scheduler_role_arn SENTRY_DSN = var.sentry_dsn PORTAL_COGNITO_ISSUER = var.portal_cognito_issuer PORTAL_COGNITO_AUDIENCE = var.portal_cognito_audience PORTAL_COGNITO_TRUST = jsonencode(concat( var.portal_cognito_issuer != "" && var.portal_cognito_audience != "" ? [{ issuer = var.portal_cognito_issuer, audience = var.portal_cognito_audience }] : [], var.portal_cognito_extra_trust, )) } } } 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 dynamic "condition" { for_each = try(statement.value.condition, null) == null ? [] : [statement.value.condition] content { test = condition.value.test variable = condition.value.variable values = condition.value.values } } } } } 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 = 1024 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] } 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, ] }