diff --git a/infrastructure/modules/lambda/data.tf b/infrastructure/modules/lambda/data.tf index 8fc4b38c..9331249e 100644 --- a/infrastructure/modules/lambda/data.tf +++ b/infrastructure/modules/lambda/data.tf @@ -1 +1,6 @@ data "aws_caller_identity" "current" {} + +data "aws_lambda_function" "existing" { + function_name = var.lambda_func_name + qualifier = "$LATEST" +} diff --git a/infrastructure/modules/lambda/kms.tf b/infrastructure/modules/lambda/kms.tf index 55c5133f..738cb32b 100644 --- a/infrastructure/modules/lambda/kms.tf +++ b/infrastructure/modules/lambda/kms.tf @@ -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 } diff --git a/infrastructure/modules/lambda/lambda.tf b/infrastructure/modules/lambda/lambda.tf index f31a6e76..47466b3b 100644 --- a/infrastructure/modules/lambda/lambda.tf +++ b/infrastructure/modules/lambda/lambda.tf @@ -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 @@ -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 @@ -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 +} diff --git a/infrastructure/modules/lambda/sqs.tf b/infrastructure/modules/lambda/sqs.tf new file mode 100644 index 00000000..f538b838 --- /dev/null +++ b/infrastructure/modules/lambda/sqs.tf @@ -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 + } + ] + }) +} diff --git a/infrastructure/modules/lambda/variables.tf b/infrastructure/modules/lambda/variables.tf index 229c1fbb..92d9d081 100644 --- a/infrastructure/modules/lambda/variables.tf +++ b/infrastructure/modules/lambda/variables.tf @@ -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 } @@ -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) @@ -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 +} diff --git a/infrastructure/stacks/api-layer/assumed_role_permissions_boundary.tf b/infrastructure/stacks/api-layer/assumed_role_permissions_boundary.tf index efc3168b..7a9e28f9 100644 --- a/infrastructure/stacks/api-layer/assumed_role_permissions_boundary.tf +++ b/infrastructure/stacks/api-layer/assumed_role_permissions_boundary.tf @@ -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 = ["*"] diff --git a/infrastructure/stacks/api-layer/iam_policies.tf b/infrastructure/stacks/api-layer/iam_policies.tf index 5f384895..2e7de68e 100644 --- a/infrastructure/stacks/api-layer/iam_policies.tf +++ b/infrastructure/stacks/api-layer/iam_policies.tf @@ -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 { diff --git a/infrastructure/stacks/api-layer/lambda.tf b/infrastructure/stacks/api-layer/lambda.tf index 68885b6d..5c71a278 100644 --- a/infrastructure/stacks/api-layer/lambda.tf +++ b/infrastructure/stacks/api-layer/lambda.tf @@ -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 } diff --git a/infrastructure/stacks/iams-developer-roles/github_actions_policies.tf b/infrastructure/stacks/iams-developer-roles/github_actions_policies.tf index cb72a796..22e3aa64 100644 --- a/infrastructure/stacks/iams-developer-roles/github_actions_policies.tf +++ b/infrastructure/stacks/iams-developer-roles/github_actions_policies.tf @@ -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:*" ] } ] @@ -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" @@ -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({ @@ -529,7 +513,10 @@ resource "aws_iam_policy" "cloudwatch_alarms" { { Effect = "Allow", Action = [ - # CloudWatch Alarms management + "logs:ListTagsForResource", + "logs:DescribeLogGroups", + "logs:PutRetentionPolicy", + "cloudwatch:PutMetricAlarm", "cloudwatch:DeleteAlarms", "cloudwatch:DescribeAlarms", @@ -537,7 +524,7 @@ resource "aws_iam_policy" "cloudwatch_alarms" { "cloudwatch:ListTagsForResource", "cloudwatch:TagResource", "cloudwatch:UntagResource", - # SNS Topic management for alarm notifications + "sns:CreateTopic", "sns:DeleteTopic", "sns:GetTopicAttributes", @@ -552,6 +539,7 @@ 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*" ] @@ -559,7 +547,33 @@ resource "aws_iam_policy" "cloudwatch_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 @@ -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 } + diff --git a/infrastructure/stacks/iams-developer-roles/iams_permissions_boundary.tf b/infrastructure/stacks/iams-developer-roles/iams_permissions_boundary.tf index 4b37cde6..7631ab5a 100644 --- a/infrastructure/stacks/iams-developer-roles/iams_permissions_boundary.tf +++ b/infrastructure/stacks/iams-developer-roles/iams_permissions_boundary.tf @@ -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", @@ -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 = ["*"]