diff --git a/infrastructure/modules/lambda/README.md b/infrastructure/modules/lambda/README.md index 44c1fdc..d09e623 100644 --- a/infrastructure/modules/lambda/README.md +++ b/infrastructure/modules/lambda/README.md @@ -22,14 +22,17 @@ | [filter\_pattern](#input\_filter\_pattern) | Filter pattern to use for the log subscription filter | `string` | `""` | no | | [force\_lambda\_code\_deploy](#input\_force\_lambda\_code\_deploy) | If the lambda package in s3 has the same commit id tag as the terraform build branch, the lambda will not update automatically. Set to True if making changes to Lambda code from on the same commit for example during development | `bool` | `false` | no | | [function\_code\_base\_path](#input\_function\_code\_base\_path) | The base path to the sourcecode directories needed for this lambda | `string` | `"./"` | no | -| [function\_code\_dir](#input\_function\_code\_dir) | The directory for this lambda | `string` | n/a | yes | +| [function\_code\_dir](#input\_function\_code\_dir) | The directory for this lambda | `string` | `null` | no | | [function\_include\_common](#input\_function\_include\_common) | Include the 'common' lambda module with this lambda | `bool` | `true` | no | | [function\_module\_name](#input\_function\_module\_name) | The name of the function module as used by the lambda handler, e.g. index or exports | `string` | `"index"` | no | | [function\_name](#input\_function\_name) | Base name of this lambda | `string` | n/a | yes | -| [function\_s3\_bucket](#input\_function\_s3\_bucket) | The bucket to upload Lambda packages to | `string` | n/a | yes | +| [function\_s3\_bucket](#input\_function\_s3\_bucket) | The bucket to upload Lambda packages to | `string` | `null` | no | | [group](#input\_group) | The name of the tfscaffold group | `string` | `null` | no | | [handler\_function\_name](#input\_handler\_function\_name) | The name of the lambda handler function (passed directly to the Lambda's handler option) | `string` | `"handler"` | no | | [iam\_policy\_document](#input\_iam\_policy\_document) | n/a |
object({
body = string
})
| `null` | no | +| [image\_config](#input\_image\_config) | Optional image configuration for Image-based Lambda |
object({
entry_point = optional(list(string))
command = optional(list(string))
working_directory = optional(string)
})
| `null` | no | +| [image\_repository\_names](#input\_image\_repository\_names) | ECR repository names allowed for Image-based Lambda | `list(string)` | `[]` | no | +| [image\_uri](#input\_image\_uri) | ECR image URI for Image-based Lambda | `string` | `null` | no | | [kms\_key\_arn](#input\_kms\_key\_arn) | KMS key arn to use for this function | `string` | n/a | yes | | [lambda\_at\_edge](#input\_lambda\_at\_edge) | Whether this Lambda is a Lambda@Edge function | `bool` | `false` | no | | [lambda\_dlq\_message\_retention\_seconds](#input\_lambda\_dlq\_message\_retention\_seconds) | The number of seconds to retain messages in the Lambda DLQ SQS queue | `number` | `1209600` | no | @@ -41,10 +44,11 @@ | [log\_subscription\_lambda\_create\_permission](#input\_log\_subscription\_lambda\_create\_permission) | Whether to create a permission for the log forwarder. Set to false if using a generic one. | `bool` | `true` | no | | [log\_subscription\_role\_arn](#input\_log\_subscription\_role\_arn) | The ARN of the IAM role to use for the log subscription filter | `string` | `""` | no | | [memory](#input\_memory) | The amount of memory to apply to the created Lambda | `number` | n/a | yes | +| [package\_type](#input\_package\_type) | Lambda package type: Zip or Image | `string` | `"Zip"` | no | | [permission\_statements](#input\_permission\_statements) | Statements giving an external source permission to invoke the Lambda function |
list(object({
action = optional(string)
principal = string
source_arn = optional(string)
source_account = optional(string)
statement_id = string
}))
| `[]` | no | | [project](#input\_project) | The name of the tfscaffold project | `string` | n/a | yes | | [region](#input\_region) | The AWS Region | `string` | n/a | yes | -| [runtime](#input\_runtime) | The runtime to use for the lambda function | `string` | n/a | yes | +| [runtime](#input\_runtime) | The runtime to use for the lambda function | `string` | `null` | no | | [schedule](#input\_schedule) | The fully qualified Cloudwatch Events schedule for when to run the lambda function, e.g. rate(1 day) or a cron() expression. Default disables all events resources | `string` | `""` | no | | [send\_to\_firehose](#input\_send\_to\_firehose) | Enable sending logs to firehose | `bool` | `true` | no | | [sns\_destination](#input\_sns\_destination) | SNS Topic ARN to be used for on-failure Lambda invocation records | `string` | `null` | no | diff --git a/infrastructure/modules/lambda/data_archive_file_lambda.tf b/infrastructure/modules/lambda/data_archive_file_lambda.tf index a7ad069..762512a 100644 --- a/infrastructure/modules/lambda/data_archive_file_lambda.tf +++ b/infrastructure/modules/lambda/data_archive_file_lambda.tf @@ -1,4 +1,5 @@ data "archive_file" "lambda" { + count = local.package_type == "zip" ? 1 : 0 type = "zip" source_dir = "${path.root}/${var.function_code_base_path}/${var.function_code_dir}" diff --git a/infrastructure/modules/lambda/data_iam_policy_document_ecr.tf b/infrastructure/modules/lambda/data_iam_policy_document_ecr.tf new file mode 100644 index 0000000..875c1a1 --- /dev/null +++ b/infrastructure/modules/lambda/data_iam_policy_document_ecr.tf @@ -0,0 +1,26 @@ +data "aws_iam_policy_document" "ecr" { + statement { + effect = "Allow" + + actions = [ + "ecr:GetAuthorizationToken", + ] + + resources = ["*"] + } + + statement { + effect = "Allow" + + actions = [ + "ecr:BatchGetImage", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchCheckLayerAvailability", + ] + + resources = [ + for repo_name in var.image_repository_names : + "arn:aws:ecr:${var.region}:${var.aws_account_id}:repository/${repo_name}" + ] + } +} diff --git a/infrastructure/modules/lambda/iam_role_policy_ecr.tf b/infrastructure/modules/lambda/iam_role_policy_ecr.tf new file mode 100644 index 0000000..3da8378 --- /dev/null +++ b/infrastructure/modules/lambda/iam_role_policy_ecr.tf @@ -0,0 +1,6 @@ +resource "aws_iam_role_policy" "ecr" { + count = local.package_type == "image" ? 1 : 0 + name = "${local.csi}-ecr" + role = aws_iam_role.main.id + policy = data.aws_iam_policy_document.ecr.json +} diff --git a/infrastructure/modules/lambda/lambda_function.tf b/infrastructure/modules/lambda/lambda_function.tf index d088acd..f67849a 100644 --- a/infrastructure/modules/lambda/lambda_function.tf +++ b/infrastructure/modules/lambda/lambda_function.tf @@ -2,15 +2,27 @@ resource "aws_lambda_function" "main" { description = var.description function_name = local.csi role = aws_iam_role.main.arn - handler = "${var.function_module_name}.${var.handler_function_name}" - runtime = var.runtime + handler = local.package_type == "zip" ? "${var.function_module_name}.${var.handler_function_name}" : null + runtime = local.package_type == "zip" ? var.runtime : null + package_type = title(local.package_type) publish = true memory_size = var.memory timeout = var.timeout - s3_bucket = aws_s3_object.lambda.bucket - s3_key = aws_s3_object.lambda.key - s3_object_version = aws_s3_object.lambda.version_id + s3_bucket = local.package_type == "zip" ? aws_s3_object.lambda[0].bucket : null + s3_key = local.package_type == "zip" ? aws_s3_object.lambda[0].key : null + s3_object_version = local.package_type == "zip" ? aws_s3_object.lambda[0].version_id : null + + image_uri = local.package_type == "image" ? var.image_uri : null + + dynamic "image_config" { + for_each = local.package_type == "image" && var.image_config != null ? [1] : [] + content { + entry_point = try(var.image_config.entry_point, null) + command = try(var.image_config.command, null) + working_directory = try(var.image_config.working_directory, null) + } + } logging_config { application_log_level = var.application_log_level @@ -19,12 +31,12 @@ resource "aws_lambda_function" "main" { system_log_level = var.system_log_level } - layers = compact(concat( + layers = local.package_type == "zip" ? compact(concat( var.layers, [ var.enable_lambda_insights && var.lambda_at_edge == false ? "arn:aws:lambda:${var.region}:580247275435:layer:LambdaInsightsExtension:53" : null ] - )) + )) : [] environment { variables = var.lambda_env_vars diff --git a/infrastructure/modules/lambda/locals.tf b/infrastructure/modules/lambda/locals.tf index 04398ea..bd112a0 100644 --- a/infrastructure/modules/lambda/locals.tf +++ b/infrastructure/modules/lambda/locals.tf @@ -1,6 +1,8 @@ locals { module = "lambda" + package_type = lower(var.package_type) + # Compound Scope Identifier csi = replace( format( diff --git a/infrastructure/modules/lambda/s3_object_lambda.tf b/infrastructure/modules/lambda/s3_object_lambda.tf index f65d11a..65f619b 100644 --- a/infrastructure/modules/lambda/s3_object_lambda.tf +++ b/infrastructure/modules/lambda/s3_object_lambda.tf @@ -1,12 +1,13 @@ resource "aws_s3_object" "lambda" { + count = local.package_type == "zip" ? 1 : 0 bucket = var.function_s3_bucket key = "${local.csi}.zip" - source = data.archive_file.lambda.output_path + source = data.archive_file.lambda[0].output_path - source_hash = var.force_lambda_code_deploy ? data.archive_file.lambda.output_base64sha256 : null + source_hash = var.force_lambda_code_deploy ? data.archive_file.lambda[0].output_base64sha256 : null metadata = { - hash = data.archive_file.lambda.output_base64sha256 + hash = data.archive_file.lambda[0].output_base64sha256 function = local.csi commit = try(data.external.git_commit.result["sha"], "null") } diff --git a/infrastructure/modules/lambda/variables.tf b/infrastructure/modules/lambda/variables.tf index 33dbe13..ed335f9 100644 --- a/infrastructure/modules/lambda/variables.tf +++ b/infrastructure/modules/lambda/variables.tf @@ -92,6 +92,55 @@ variable "log_retention_in_days" { variable "runtime" { type = string description = "The runtime to use for the lambda function" + default = null + + validation { + condition = lower(var.package_type) != "zip" || (var.runtime != null && var.runtime != "") + error_message = "runtime must be set when package_type is Zip." + } +} + +variable "package_type" { + type = string + description = "Lambda package type: Zip or Image" + default = "Zip" + + validation { + condition = contains(["zip", "image"], lower(var.package_type)) + error_message = "package_type must be either Zip or Image." + } +} + +variable "image_uri" { + type = string + description = "ECR image URI for Image-based Lambda" + default = null + + validation { + condition = lower(var.package_type) != "image" || (var.image_uri != null && var.image_uri != "") + error_message = "image_uri must be set when package_type is Image." + } +} + +variable "image_config" { + type = object({ + entry_point = optional(list(string)) + command = optional(list(string)) + working_directory = optional(string) + }) + description = "Optional image configuration for Image-based Lambda" + default = null +} + +variable "image_repository_names" { + type = list(string) + description = "ECR repository names allowed for Image-based Lambda" + default = [] + + validation { + condition = lower(var.package_type) != "image" || length(var.image_repository_names) > 0 + error_message = "image_repository_names must include at least one repository name when package_type is Image." + } } variable "schedule" { @@ -122,11 +171,23 @@ variable "function_code_base_path" { variable "function_code_dir" { type = string description = "The directory for this lambda" + default = null + + validation { + condition = lower(var.package_type) != "zip" || (var.function_code_dir != null && var.function_code_dir != "") + error_message = "function_code_dir must be set when package_type is Zip." + } } variable "function_s3_bucket" { type = string description = "The bucket to upload Lambda packages to" + default = null + + validation { + condition = lower(var.package_type) != "zip" || (var.function_s3_bucket != null && var.function_s3_bucket != "") + error_message = "function_s3_bucket must be set when package_type is Zip." + } } variable "function_include_common" {