Skip to content

Commit

Permalink
Custom Lambdas
Browse files Browse the repository at this point in the history
* Allows deploying custom Lambda functions
* This creates an S3 bucket where zipped lambda functions can be placed,
  and referenced to deploy as a Lambda function.
* Policies can also be placed in the S3 bucket and referenced, which
  will then be created and attached to the Lambda role.
  • Loading branch information
Stretch96 committed Mar 28, 2024
1 parent 2773f2b commit 96d0745
Show file tree
Hide file tree
Showing 9 changed files with 288 additions and 4 deletions.
24 changes: 22 additions & 2 deletions README.md

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions data.tf
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,24 @@ data "aws_cloudfront_response_headers_policy" "managed_policy" {
name = "Managed-${each.value}"
}

data "aws_s3_object" "lambda_custom_functions" {
for_each = {
for k, custom_lambda in local.custom_lambda_functions : k => custom_lambda if custom_lambda["s3_function_store_zip_key"] != null
}

bucket = aws_s3_bucket.lambda_custom_functions_store[0].id
key = each.value["s3_function_store_zip_key"]
}

data "aws_s3_object" "lambda_custom_functions_policy" {
for_each = {
for k, custom_lambda in local.custom_lambda_functions : k => custom_lambda if custom_lambda["s3_function_store_policy_key"] != null
}

bucket = aws_s3_bucket.lambda_custom_functions_store[0].id
key = each.value["s3_function_store_policy_key"]
}

# aws_ssm_service_setting doesn't yet have a data source, so we need to use
# a script to retrieve SSM service settings
# https://github.com/hashicorp/terraform-provider-aws/issues/25170
Expand Down
23 changes: 22 additions & 1 deletion elasticache-infrastructure-security-group.tf
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ resource "aws_security_group" "infrastructure_elasticache" {
vpc_id = aws_vpc.infrastructure[0].id
}

resource "aws_security_group_rule" "infrastructure_elasticache_ingress_tcp" {
resource "aws_security_group_rule" "infrastructure_elasticache_ingress_tcp_ecs_instances" {
for_each = local.infrastructure_vpc_network_enable_public || local.infrastructure_vpc_network_enable_private ? local.infrastructure_elasticache : {}

description = "Allow ElastiCache port tcp ingress from ECS instances if launched, otherwise from the subnet"
Expand All @@ -18,3 +18,24 @@ resource "aws_security_group_rule" "infrastructure_elasticache_ingress_tcp" {
source_security_group_id = local.enable_infrastructure_ecs_cluster ? aws_security_group.infrastructure_ecs_cluster_container_instances[0].id : null
security_group_id = aws_security_group.infrastructure_elasticache[each.key].id
}

resource "aws_security_group_rule" "infrastructure_elasticache_ingress_tcp_custom_lambda" {
for_each = local.infrastructure_vpc_network_enable_public || local.infrastructure_vpc_network_enable_private ? {
for k, v in merge(flatten([for elasticache_k, elasticache_v in local.infrastructure_elasticache :
flatten([
for lambda_k, lambda_v in local.custom_lambda_functions : { "${elasticache_k}-${lambda_k}" = merge(elasticache_k, {
lambda_source_security_group = lambda_v["launch_in_infrastructure_vpc"] == true ? aws_security_group.custom_lambda[lambda_k].id : null
}) }
])
])...) : k => v if v["launch_in_infrastructure_vpc"] != null
} : {}

description = "Allow ElastiCache port tcp ingress from Custom Lambdas if launched"
type = "ingress"
from_port = local.elasticache_ports[each.value["engine"]]
to_port = local.elasticache_ports[each.value["engine"]]
protocol = "tcp"
cidr_blocks = local.infrastructure_vpc_network_enable_private ? [for subnet in aws_subnet.infrastructure_private : subnet.cidr_block] : local.infrastructure_vpc_network_enable_public ? [for subnet in aws_subnet.infrastructure_public : subnet.cidr_block] : null
source_security_group_id = each.value["launch_in_infrastructure_vpc"]
security_group_id = aws_security_group.infrastructure_elasticache[each.key].id
}
67 changes: 67 additions & 0 deletions lambda-custom-functions-s3-store.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
resource "aws_s3_bucket" "lambda_custom_functions_store" {
count = local.enable_lambda_functions_s3_store ? 1 : 0

bucket = "${local.resource_prefix_hash}-lambda-custom-functions"
}

resource "aws_s3_bucket_policy" "lambda_custom_functions_store" {
count = local.enable_lambda_functions_s3_store ? 1 : 0

bucket = aws_s3_bucket.lambda_custom_functions_store[0].id
policy = templatefile(
"${path.module}/policies/s3-bucket-policy.json.tpl",
{
statement = <<EOT
[
${templatefile("${path.root}/policies/s3-bucket-policy-statements/enforce-tls.json.tpl",
{
bucket_arn = aws_s3_bucket.lambda_custom_functions_store[0].arn
}
)}
]
EOT
}
)
}

resource "aws_s3_bucket_public_access_block" "lambda_custom_functions_store" {
count = local.enable_lambda_functions_s3_store ? 1 : 0

bucket = aws_s3_bucket.lambda_custom_functions_store[0].id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}

resource "aws_s3_bucket_versioning" "lambda_custom_functions_store" {
count = local.enable_lambda_functions_s3_store ? 1 : 0

bucket = aws_s3_bucket.lambda_custom_functions_store[0].id

versioning_configuration {
status = "Enabled"
}
}

resource "aws_s3_bucket_logging" "lambda_custom_functions_store" {
count = local.enable_lambda_functions_s3_store ? 1 : 0

bucket = aws_s3_bucket.lambda_custom_functions_store[0].id

target_bucket = aws_s3_bucket.infrastructure_logs[0].id
target_prefix = "s3/cloudformation-custom-stack-templates"
}

resource "aws_s3_bucket_server_side_encryption_configuration" "lambda_custom_functions_store" {
count = local.enable_lambda_functions_s3_store ? 1 : 0

bucket = aws_s3_bucket.lambda_custom_functions_store[0].id

rule {
apply_server_side_encryption_by_default {
kms_master_key_id = local.infrastructure_kms_encryption ? aws_kms_key.infrastructure[0].arn : null
sse_algorithm = local.infrastructure_kms_encryption ? "aws:kms" : "AES256"
}
}
}
9 changes: 9 additions & 0 deletions lambda-custom-functions-security-group.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
resource "aws_security_group" "custom_lambda" {
for_each = local.infrastructure_vpc_network_enable_public || local.infrastructure_vpc_network_enable_private ? {
for k, custom_lambda in local.custom_lambda_functions : k => custom_lambda if custom_lambda["launch_in_infrastructure_vpc"] == true
} : {}

name = "${local.resource_prefix}-custom-lambda-${each.key}"
description = "${local.resource_prefix} custom lambda ${each.key}"
vpc_id = aws_vpc.infrastructure[0].id
}
90 changes: 90 additions & 0 deletions lambda-custom-functions.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
resource "aws_cloudwatch_log_group" "lambda_custom_functions" {
for_each = local.custom_lambda_functions

name = "/aws/lambda/${local.project_name}-${each.key}-custom-lambda"
kms_key_id = local.infrastructure_kms_encryption ? aws_kms_key.infrastructure[0].arn : null
retention_in_days = each.value["log_retention"]
}

resource "aws_iam_role" "lambda_custom_functions" {
for_each = local.custom_lambda_functions

name = "${local.resource_prefix_hash}-${substr(sha512("${each.key}-custom-lambda"), 0, 6)}"
description = "${local.resource_prefix}-${each.key}-custom-lambda"
assume_role_policy = templatefile(
"${path.root}/policies/assume-roles/service-principle-standard.json.tpl",
{ services = jsonencode(["lambda.amazonaws.com"]) }
)
}

resource "aws_iam_policy" "lambda_custom_functions" {
for_each = local.custom_lambda_functions

name = "${local.resource_prefix}-${each.key}-custom-lambda"
policy = templatefile(
"${path.root}/policies/lambda-default.json.tpl",
{
region = local.aws_region
account_id = local.aws_account_id
function_name = "${local.project_name}-${each.key}-custom-lambda"
}
)
}

resource "aws_iam_role_policy_attachment" "lambda_custom_functions" {
for_each = local.custom_lambda_functions

role = aws_iam_role.lambda_custom_functions[0].name
policy_arn = aws_iam_policy.lambda_custom_functions[0].arn
}

resource "aws_iam_policy" "lambda_custom_functions_additional" {
for_each = {
for k, custom_lambda in local.custom_lambda_functions : k => custom_lambda if custom_lambda["s3_function_store_policy_key"] != null
}

name = "${local.resource_prefix}-${each.key}-custom-lambda-additional"
policy = data.aws_s3_object.lambda_custom_functions_policy[each.key].body
}

resource "aws_iam_role_policy_attachment" "lambda_custom_functions_additional" {
for_each = {
for k, custom_lambda in local.custom_lambda_functions : k => custom_lambda if custom_lambda["s3_function_store_policy_key"] != null
}

role = aws_iam_role.lambda_custom_functions[0].name
policy_arn = aws_iam_policy.lambda_custom_functions_additional[0].arn
}

resource "aws_lambda_function" "custom" {
for_each = local.custom_lambda_functions

s3_bucket = data.aws_s3_object.lambda_custom_functions[each.key].bucket
s3_key = data.aws_s3_object.lambda_custom_functions[each.key].key
s3_object_version = data.aws_s3_object.lambda_custom_functions[each.key].version_id

function_name = "${local.resource_prefix}-custom-${each.key}"
description = "${local.resource_prefix} Custom ${each.key}"
handler = each.value["handler"]
runtime = each.value["runtime"]
role = aws_iam_role.lambda_custom_functions[0].name
memory_size = each.value["memory"]
package_type = "Zip"
timeout = each.value["timeout"]

environment {
variables = each.value["environment_variables"]
}

dynamic "vpc_config" {
for_each = each.value["launch_in_infrastructure_vpc"] == true ? [1] : []
content {
security_group_ids = [aws_security_group.custom_lambda[each.key].id]
subnet_ids = local.infrastructure_vpc_network_enable_private ? [for subnet in aws_subnet.infrastructure_private : subnet.id] : local.infrastructure_vpc_network_enable_public ? [for subnet in aws_subnet.infrastructure_public : subnet.id] : null
}
}

tracing_config {
mode = "Active"
}
}
3 changes: 3 additions & 0 deletions locals.tf
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,9 @@ locals {
enable_cloudformatian_s3_template_store = var.enable_cloudformatian_s3_template_store != null ? var.enable_cloudformatian_s3_template_store : false
custom_cloudformation_stacks = var.custom_cloudformation_stacks

enable_lambda_functions_s3_store = var.enable_lambda_functions_s3_store != null ? var.enable_lambda_functions_s3_store : false
custom_lambda_functions = var.custom_lambda_functions != null ? var.custom_lambda_functions : {}

s3_object_presign = local.enable_cloudformatian_s3_template_store ? toset([
for k, v in local.custom_cloudformation_stacks : "${aws_s3_bucket.cloudformation_custom_stack_template_store[0].id}/${v["s3_template_store_key"]}" if v["s3_template_store_key"] != null
]) : []
Expand Down
23 changes: 22 additions & 1 deletion rds-infrastructure-security-group.tf
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ resource "aws_security_group" "infrastructure_rds" {
vpc_id = aws_vpc.infrastructure[0].id
}

resource "aws_security_group_rule" "infrastructure_rds_ingress_tcp" {
resource "aws_security_group_rule" "infrastructure_rds_ingress_tcp_ecs_instances" {
for_each = local.infrastructure_vpc_network_enable_public || local.infrastructure_vpc_network_enable_private ? local.infrastructure_rds : {}

description = "Allow RDS port tcp ingress from ECS instances if launched, otherwise from the subnet"
Expand All @@ -18,3 +18,24 @@ resource "aws_security_group_rule" "infrastructure_rds_ingress_tcp" {
source_security_group_id = local.enable_infrastructure_ecs_cluster ? aws_security_group.infrastructure_ecs_cluster_container_instances[0].id : null
security_group_id = aws_security_group.infrastructure_rds[each.key].id
}

resource "aws_security_group_rule" "infrastructure_rds_ingress_tcp_custom_lambda" {
for_each = local.infrastructure_vpc_network_enable_public || local.infrastructure_vpc_network_enable_private ? {
for k, v in merge(flatten([for rds_k, rds_v in local.infrastructure_rds :
flatten([
for lambda_k, lambda_v in local.custom_lambda_functions : { "${rds_k}-${lambda_k}" = merge(rds_k, {
lambda_source_security_group = lambda_v["launch_in_infrastructure_vpc"] == true ? aws_security_group.custom_lambda[lambda_k].id : null
}) }
])
])...) : k => v if v["launch_in_infrastructure_vpc"] != null
} : {}

description = "Allow RDS port tcp ingress from Custom Lambdas if launched"
type = "ingress"
from_port = local.rds_ports[each.value["engine"]]
to_port = local.rds_ports[each.value["engine"]]
protocol = "tcp"
cidr_blocks = local.infrastructure_vpc_network_enable_private ? [for subnet in aws_subnet.infrastructure_private : subnet.cidr_block] : local.infrastructure_vpc_network_enable_public ? [for subnet in aws_subnet.infrastructure_public : subnet.cidr_block] : null
source_security_group_id = each.value["launch_in_infrastructure_vpc"]
security_group_id = aws_security_group.infrastructure_rds[each.key].id
}
35 changes: 35 additions & 0 deletions variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -642,3 +642,38 @@ variable "custom_cloudformation_stacks" {
capabilities = optional(list(string), null)
}))
}

variable "enable_lambda_functions_s3_store" {
description = "Creates an S3 bucket to store custom Lambda functions, which can then be referenced in `custom_lambdas`. A user with RW access to the bucket is also created."
type = bool
}

variable "custom_lambda_functions" {
description = <<EOT
Map of Lambda functions to deploy
{
function-name = {
s3_function_store_zip_key: The key of a Zipped Lambda function that is stored within the S3 bucket, created by the `enable_lambda_functions_s3_store`. If a file with the same name, with the `.json` extension is found, this will be used as a policy for the function (eg. `my-function.zip` will use the `my-function.json` as a policy).
s3_function_store_policy_key: The key of a policy (json) stored within the S3 bucket, created by the `enable_lambda_functions_s3_store`. This policy will be attached to the Lambda role.
handler: The function entrypoint in the code
runtime: The function runtime
memory: Amount of memory in MB your Lambda Function can use at runtime.
timeout: Amount of time your Lambda Function has to run in seconds
environment_variables: Map of environment variables that are accessible from the function code during execution.
log_retention: Days to retain logs
launch_in_infrastructure_vpc: Conditionally launch within the infrastructure VPC. This will give access to resources launched within the VPC.
}
}
EOT
type = map(object({
s3_function_store_zip_key = optional(string, null)
s3_function_store_policy_key = optional(string, null)
handler = optional(string, null)
runtime = optional(string, null)
memory = optional(number, null)
timeout = optional(number, null)
environment_variables = optional(map(string), null)
log_retention = optional(number, null)
launch_in_infrastructure_vpc = optional(bool, null)
}))
}

0 comments on commit 96d0745

Please sign in to comment.