Skip to content
5 changes: 5 additions & 0 deletions infrastructure/modules/lambda/data.tf
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
data "aws_caller_identity" "current" {}

data "aws_lambda_function" "existing" {
function_name = var.lambda_func_name
qualifier = "$LATEST"
}
2 changes: 1 addition & 1 deletion infrastructure/modules/lambda/kms.tf
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ resource "aws_kms_key" "lambda_cmk" {
}

resource "aws_kms_alias" "lambda_cmk" {
name = "alias/${terraform.workspace == "default" ? "" : "${terraform.workspace}-"}${var.lambda_func_name}-cmk"
name = "alias/${terraform.workspace == "default" ? "" : "${terraform.workspace}-"}${var.lambda_func_name}-key"
target_key_id = aws_kms_key.lambda_cmk.key_id
}

Expand Down
33 changes: 31 additions & 2 deletions infrastructure/modules/lambda/lambda.tf
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
resource "aws_lambda_function" "eligibility_signposting_lambda" {
#checkov:skip=CKV_AWS_116: No deadletter queue is configured for this Lambda function, yet
#checkov:skip=CKV_AWS_115: Concurrent execution limit will be set at APIM level, not at Lambda level
#checkov:skip=CKV_AWS_272: Skipping code signing but flagged to create ticket to investigate on ELI-238
# If the file is not in the current working directory you will need to include a
Expand All @@ -11,7 +10,7 @@ resource "aws_lambda_function" "eligibility_signposting_lambda" {

source_code_hash = filebase64sha256(var.file_name)

runtime = "python3.13"
runtime = var.runtime
timeout = 30
memory_size = 2048

Expand All @@ -33,7 +32,37 @@ resource "aws_lambda_function" "eligibility_signposting_lambda" {
security_group_ids = var.security_group_ids
}

dead_letter_config {
target_arn = aws_sqs_queue.lambda_dlq.arn
}

layers = compact([
var.environment == "prod" ? "arn:aws:lambda:${var.region}:580247275435:layer:LambdaInsightsExtension:${var.lambda_insights_extension_version}" : null
])

tracing_config {
mode = "Active"
}
}

# lambda alias required for provisioning concurrency
resource "aws_lambda_alias" "campaign_alias" {
count = var.environment == "prod" ? 1 : 0
name = "live"
function_name = coalesce(
aws_lambda_function.eligibility_signposting_lambda.function_name,
data.aws_lambda_function.existing.function_name
)
function_version = coalesce(
aws_lambda_function.eligibility_signposting_lambda.version,
data.aws_lambda_function.existing.version
)
}

# provisioned concurrency - number of pre-warmed lambda containers
resource "aws_lambda_provisioned_concurrency_config" "campaign_pc" {
count = var.environment == "prod" ? 1 : 0
function_name = var.lambda_func_name
qualifier = aws_lambda_alias.campaign_alias[0].name
provisioned_concurrent_executions = var.provisioned_concurrency_count
}
23 changes: 23 additions & 0 deletions infrastructure/modules/lambda/sqs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
resource "aws_sqs_queue" "lambda_dlq" {
name = "${var.lambda_func_name}_dead_letter_queue"
kms_master_key_id = aws_kms_key.lambda_cmk.id
tags = var.tags
}

# sql policy attachment
resource "aws_iam_role_policy" "lambda_sqs_send_inline" {
name = "LambdaSQSMessageSendPolicy"
role = var.eligibility_lambda_role_name

policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Sid = "AllowSQSSendMessage",
Effect = "Allow",
Action = ["sqs:SendMessage"],
Resource = aws_sqs_queue.lambda_dlq.arn
}
]
})
}
23 changes: 22 additions & 1 deletion infrastructure/modules/lambda/variables.tf
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
variable "eligibility_lambda_role_arn" {
description = "lambda read role arn for dynamodb"
description = "lambda role arn"
type = string
}

variable "eligibility_lambda_role_name" {
description = "lambda role name"
type = string
}

Expand All @@ -8,6 +13,12 @@ variable "lambda_func_name" {
type = string
}

variable "runtime" {
description = "runtime of the Lambda function"
type = string
}


variable "vpc_intra_subnets" {
description = "vpc private subnets for lambda"
type = list(string)
Expand Down Expand Up @@ -52,3 +63,13 @@ variable "enable_xray_patching"{
description = "flag to enable xray tracing, which puts an entry for dynamodb, s3 and firehose in trace map"
type = string
}

variable "provisioned_concurrency_count" {
description = "Number of prewarmed Lambda instances"
type = number
}

variable "lambda_insights_extension_version" {
description = "version number of LambdaInsightsExtension"
type = number
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ data "aws_iam_policy_document" "assumed_role_permissions_boundary" {

# X-Ray - Lambda tracing
"xray:PutTraceSegments",
"xray:PutTelemetryRecords"
"xray:PutTelemetryRecords",

#SQS - message management
"sqs:SendMessage"
]

resources = ["*"]
Expand Down
6 changes: 6 additions & 0 deletions infrastructure/stacks/api-layer/iam_policies.tf
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,12 @@ resource "aws_iam_role_policy_attachment" "lambda_logs_policy_attachment" {
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

#Attach CloudWatchLambdaInsightsExecutionRolePolicy to lambda for enhanced monitoring
resource "aws_iam_role_policy_attachment" "lambda_insights_policy" {
role = aws_iam_role.eligibility_lambda_role.name
policy_arn = "arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy"
}

# Policy doc for S3 Audit bucket
data "aws_iam_policy_document" "s3_audit_bucket_policy" {
statement {
Expand Down
34 changes: 19 additions & 15 deletions infrastructure/stacks/api-layer/lambda.tf
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,23 @@ data "aws_subnet" "private_subnets" {
}

module "eligibility_signposting_lambda_function" {
source = "../../modules/lambda"
eligibility_lambda_role_arn = aws_iam_role.eligibility_lambda_role.arn
workspace = local.workspace
environment = var.environment
lambda_func_name = "${terraform.workspace == "default" ? "" : "${terraform.workspace}-"}eligibility_signposting_api"
security_group_ids = [data.aws_security_group.main_sg.id]
vpc_intra_subnets = [for v in data.aws_subnet.private_subnets : v.id]
file_name = "../../../dist/lambda.zip"
handler = "eligibility_signposting_api.app.lambda_handler"
eligibility_rules_bucket_name = module.s3_rules_bucket.storage_bucket_name
eligibility_status_table_name = module.eligibility_status_table.table_name
kinesis_audit_stream_to_s3_name = module.eligibility_audit_firehose_delivery_stream.firehose_stream_name
log_level = "INFO"
enable_xray_patching = "true"
stack_name = local.stack_name
source = "../../modules/lambda"
eligibility_lambda_role_arn = aws_iam_role.eligibility_lambda_role.arn
eligibility_lambda_role_name = aws_iam_role.eligibility_lambda_role.name
workspace = local.workspace
environment = var.environment
runtime = "python3.13"
lambda_func_name = "${terraform.workspace == "default" ? "" : "${terraform.workspace}-"}eligibility_signposting_api"
security_group_ids = [data.aws_security_group.main_sg.id]
vpc_intra_subnets = [for v in data.aws_subnet.private_subnets : v.id]
file_name = "../../../dist/lambda.zip"
handler = "eligibility_signposting_api.app.lambda_handler"
eligibility_rules_bucket_name = module.s3_rules_bucket.storage_bucket_name
eligibility_status_table_name = module.eligibility_status_table.table_name
kinesis_audit_stream_to_s3_name = module.eligibility_audit_firehose_delivery_stream.firehose_stream_name
lambda_insights_extension_version = 38
log_level = "INFO"
enable_xray_patching = "true"
stack_name = local.stack_name
provisioned_concurrency_count = 5
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,17 @@ resource "aws_iam_policy" "lambda_management" {
"lambda:ListAliases",
"lambda:AddPermission",
"lambda:RemovePermission",
"lambda:GetPolicy"
"lambda:GetPolicy",
"lambda:GetAlias",
"lambda:GetFunction",
"lambda:GetProvisionedConcurrencyConfig",
"lambda:GetLayerVersion",
"lambda:PutProvisionedConcurrencyConfig"
],
Resource = [
"arn:aws:lambda:*:${data.aws_caller_identity.current.account_id}:function:*eligibility_signposting_api"
"arn:aws:lambda:*:${data.aws_caller_identity.current.account_id}:function:eligibility_signposting_api",
"arn:aws:lambda:*:${data.aws_caller_identity.current.account_id}:function:eligibility_signposting_api:*",
"arn:aws:lambda:*:580247275435:layer:LambdaInsightsExtension:*"
]
}
]
Expand Down Expand Up @@ -465,29 +472,6 @@ data "aws_iam_policy_document" "github_actions_assume_role" {
}
}

resource "aws_iam_policy" "cloudwatch_logging" {
name = "cloudwatch-logging-management"
description = "Allow access to logging resources"
path = "/service-policies/"

policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Action = [
"logs:ListTagsForResource",
"logs:DescribeLogGroups",
"logs:PutRetentionPolicy"
],
Resource = "arn:aws:logs:${var.default_aws_region}:${data.aws_caller_identity.current.account_id}:log-group:/aws/kinesisfirehose/*"
}
]
})

tags = merge(local.tags, { Name = "cloudwatch-logging-management" })
}

resource "aws_iam_policy" "firehose_readonly" {
name = "firehose-describe-access"
description = "Allow GitHub Actions to describe Firehose delivery stream"
Expand Down Expand Up @@ -518,9 +502,9 @@ resource "aws_iam_policy" "firehose_readonly" {
tags = merge(local.tags, { Name = "firehose-describe-access" })
}

resource "aws_iam_policy" "cloudwatch_alarms" {
name = "cloudwatch-alarms-management"
description = "Allow GitHub Actions to manage CloudWatch alarms and SNS topics"
resource "aws_iam_policy" "cloudwatch_management" {
name = "cloudwatch-management"
description = "Allow GitHub Actions to manage CloudWatch logs, alarms, and SNS topics"
path = "/service-policies/"

policy = jsonencode({
Expand All @@ -529,15 +513,18 @@ resource "aws_iam_policy" "cloudwatch_alarms" {
{
Effect = "Allow",
Action = [
# CloudWatch Alarms management
"logs:ListTagsForResource",
"logs:DescribeLogGroups",
"logs:PutRetentionPolicy",

"cloudwatch:PutMetricAlarm",
"cloudwatch:DeleteAlarms",
"cloudwatch:DescribeAlarms",
"cloudwatch:DescribeAlarmsForMetric",
"cloudwatch:ListTagsForResource",
"cloudwatch:TagResource",
"cloudwatch:UntagResource",
# SNS Topic management for alarm notifications

"sns:CreateTopic",
"sns:DeleteTopic",
"sns:GetTopicAttributes",
Expand All @@ -552,14 +539,41 @@ resource "aws_iam_policy" "cloudwatch_alarms" {
"sns:ListSubscriptionsByTopic"
],
Resource = [
"arn:aws:logs:${var.default_aws_region}:${data.aws_caller_identity.current.account_id}:log-group:/aws/kinesisfirehose/*",
"arn:aws:cloudwatch:${var.default_aws_region}:${data.aws_caller_identity.current.account_id}:alarm:*",
"arn:aws:sns:${var.default_aws_region}:${data.aws_caller_identity.current.account_id}:cloudwatch-security-alarms*"
]
}
]
})

tags = merge(local.tags, { Name = "cloudwatch-alarms-management" })
tags = merge(local.tags, { Name = "cloudwatch-management" })
}

# SQS Management Policy for GetQueueAttributes
resource "aws_iam_policy" "sqs_management" {
name = "sqs-management"
description = "Policy granting permissions to get SQS queue attributes"
path = "/service-policies/"

policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Effect = "Allow",
Action = [
"sqs:GetQueueAttributes",
"sqs:listqueuetags",
"sqs:createqueue"
],
Resource = [
"arn:aws:sqs:eu-west-2:${data.aws_caller_identity.current.account_id}:*"
]
}
]
})

tags = merge(local.tags, { Name = "sqs-management" })
}

# Attach the policies to the role
Expand Down Expand Up @@ -598,17 +612,18 @@ resource "aws_iam_role_policy_attachment" "iam_management" {
policy_arn = aws_iam_policy.iam_management.arn
}

resource "aws_iam_role_policy_attachment" "cloudwatch_logging" {
resource "aws_iam_role_policy_attachment" "firehose_readonly_attach" {
role = aws_iam_role.github_actions.name
policy_arn = aws_iam_policy.cloudwatch_logging.arn
policy_arn = aws_iam_policy.firehose_readonly.arn
}

resource "aws_iam_role_policy_attachment" "firehose_readonly_attach" {
resource "aws_iam_role_policy_attachment" "cloudwatch_management" {
role = aws_iam_role.github_actions.name
policy_arn = aws_iam_policy.firehose_readonly.arn
policy_arn = aws_iam_policy.cloudwatch_management.arn
}

resource "aws_iam_role_policy_attachment" "cloudwatch_alarms" {
resource "aws_iam_role_policy_attachment" "sqs_management" {
role = aws_iam_role.github_actions.name
policy_arn = aws_iam_policy.cloudwatch_alarms.arn
policy_arn = aws_iam_policy.sqs_management.arn
}

Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@ data "aws_iam_policy_document" "permissions_boundary" {
"lambda:AddPermission",
"lambda:RemovePermission",
"lambda:GetPolicy",
"lambda:GetAlias",
"lambda:GetProvisionedConcurrencyConfig",
"lambda:GetLayerVersion",
"lambda:PutProvisionedConcurrencyConfig",

# CloudWatch Logs - log management
"logs:CreateLogGroup",
Expand Down Expand Up @@ -217,7 +221,13 @@ data "aws_iam_policy_document" "permissions_boundary" {
"ssm:GetParameters",
"ssm:ListTagsForResource",
"ssm:PutParameter",
"ssm:AddTagsToResource"
"ssm:AddTagsToResource",

#SQS - message management
"sqs:SendMessage",
"sqs:GetQueueAttributes",
"sqs:listqueuetags",
"sqs:createqueue"
]

resources = ["*"]
Expand Down