diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index cc105ab..a99cffa 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@master - uses: hashicorp/setup-terraform@v1 with: - terraform_version: 1.3.0 + terraform_version: 1.7.4 - id: Init run: terraform init -no-color - id: Fmt diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..a41ff70 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +terraform 1.7.4 diff --git a/README.md b/README.md index a1df14c..52ed7ee 100644 --- a/README.md +++ b/README.md @@ -32,52 +32,48 @@ No requirements. ## Modules -No modules. +| Name | Source | Version | +|------|--------|---------| +| [lambda\_function](#module\_lambda\_function) | terraform-aws-modules/lambda/aws | 7.2.6 | ## Resources | Name | Type | |------|------| -| [aws_cloudwatch_log_group.lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | -| [aws_cloudwatch_metric_alarm.lambda_errors](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_metric_alarm) | resource | -| [aws_iam_role_policy_attachment.lambda_basic_execution](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | -| [aws_iam_role_policy_attachment.lambda_insights](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | -| [aws_iam_role_policy_attachment.lambda_networking](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | -| [aws_iam_role_policy_attachment.lambda_xray](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | -| [aws_lambda_function.lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function) | resource | -| [aws_s3_bucket_object.lambda_deploy_object](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_object) | resource | -| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | +| [aws_sns_topic.notifications](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/sns_topic) | data source | +| [aws_ssm_parameter.account_name](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | | [aws_ssm_parameter.deployment_bucket_id](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [aws_ssm_parameter.global_account_id](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | +| [aws_ssm_parameter.kms_key_arn](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | data source | ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [deployment\_bucket\_id](#input\_deployment\_bucket\_id) | ID of S3 bucket that should store our deployment artifacts. Will use the /account/DEPLOYMENT\_BUCKET\_ID value from SSM unless specified otherwise. | `string` | `null` | no | -| [description](#input\_description) | Description of the Lambda Function | `string` | `null` | no | -| [environment](#input\_environment) | Environment variables to be passed to the function | `map(string)` | `{}` | no | -| [error\_rate\_alarm\_threshold](#input\_error\_rate\_alarm\_threshold) | Error rate (in percent, 1-100) at which to trigger an alarm notification | `number` | `25` | no | -| [git\_sha](#input\_git\_sha) | Hash generated by `git hash-object` in source repo and used to determine whether a lambda needs to be updated | `string` | `null` | no | -| [handler](#input\_handler) | Name of the handler function inside the artifact (https://docs.aws.amazon.com/lambda/latest/dg/configuration-console.html) | `string` | n/a | yes | -| [layer\_arns](#input\_layer\_arns) | List of ARNs for layers to use with the function | `list(string)` | `[]` | no | -| [log\_retention\_in\_days](#input\_log\_retention\_in\_days) | Number of days to keep function logs in Cloudwatch | `number` | `365` | no | -| [memory\_size](#input\_memory\_size) | Amount of memory (in MB) to allocate to the function | `number` | `128` | no | -| [name](#input\_name) | Name for the function | `string` | n/a | yes | -| [notifications\_topic\_arn](#input\_notifications\_topic\_arn) | SNS topic to send error notifications | `string` | n/a | yes | -| [path](#input\_path) | Local path to a zipped artifact containing the function code | `string` | n/a | yes | -| [reserved\_concurrent\_executions](#input\_reserved\_concurrent\_executions) | Reserved concurrent executions (none by default) | `number` | `null` | no | -| [role\_name](#input\_role\_name) | Name of the execution role for the function. It does not need to include logging/networking permissions - those policies will be added automatically. | `string` | n/a | yes | -| [runtime](#input\_runtime) | Language runtime for the function (https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html) | `string` | n/a | yes | -| [security\_group\_ids](#input\_security\_group\_ids) | Security groups for the function (if run in a VPC) | `list(string)` | `[]` | no | -| [subnet\_ids](#input\_subnet\_ids) | Subnets for the function (if run in a VPC) | `list(string)` | `[]` | no | -| [tags](#input\_tags) | Tags to apply to created resources | `map(any)` | `{}` | no | -| [timeout](#input\_timeout) | Function timeout in seconds | `number` | `15` | no | +| [allowed\_triggers](#input\_allowed\_triggers) | n/a | `map(any)` | `{}` | no | +| [description](#input\_description) | n/a | `string` | `null` | no | +| [ecr\_address](#input\_ecr\_address) | n/a | `string` | n/a | yes | +| [ecr\_arn](#input\_ecr\_arn) | n/a | `string` | n/a | yes | +| [engine\_name](#input\_engine\_name) | n/a | `string` | n/a | yes | +| [environment\_variables](#input\_environment\_variables) | n/a | `map(any)` | `{}` | no | +| [event\_source\_mapping](#input\_event\_source\_mapping) | Map of event source mappings | `map(any)` | `{}` | no | +| [function\_name](#input\_function\_name) | n/a | `string` | n/a | yes | +| [handler](#input\_handler) | n/a | `string` | `null` | no | +| [image\_tag](#input\_image\_tag) | n/a | `string` | `"latest"` | no | +| [memory\_size](#input\_memory\_size) | n/a | `number` | `1024` | no | +| [policy\_statements](#input\_policy\_statements) | n/a | `map(any)` | `{}` | no | +| [security\_group\_ids](#input\_security\_group\_ids) | n/a | `list(string)` | `[]` | no | +| [subnet\_ids](#input\_subnet\_ids) | n/a | `list(string)` | `[]` | no | +| [timeout](#input\_timeout) | n/a | `string` | `"60"` | no | ## Outputs | Name | Description | |------|-------------| -| [function\_arn](#output\_function\_arn) | ARN of the created/updated Lambda function | -| [function\_invoke\_arn](#output\_function\_invoke\_arn) | ARN for invoking the created Lambda function | -| [function\_name](#output\_function\_name) | Name of the created Lambda function | -| [function\_version](#output\_function\_version) | Version of the created/updated Lambda function | +| [function\_arn](#output\_function\_arn) | n/a | +| [function\_name](#output\_function\_name) | n/a | +| [function\_version](#output\_function\_version) | n/a | +| [lambda\_role\_arn](#output\_lambda\_role\_arn) | n/a | +| [lambda\_role\_name](#output\_lambda\_role\_name) | n/a | +| [qualified\_function\_arn](#output\_qualified\_function\_arn) | n/a | diff --git a/data.tf b/data.tf index 4c2b538..5178b52 100644 --- a/data.tf +++ b/data.tf @@ -1,5 +1,21 @@ -data "aws_caller_identity" "current" {} +data "aws_region" "current" {} + +data "aws_ssm_parameter" "account_name" { + name = "/account/name" +} + +data "aws_sns_topic" "notifications" { + name = "account-notifications" +} data "aws_ssm_parameter" "deployment_bucket_id" { name = "/account/DEPLOYMENT_BUCKET_ID" } + +data "aws_ssm_parameter" "kms_key_arn" { + name = "/account/KMS_KEY_ARN" +} + +data "aws_ssm_parameter" "global_account_id" { + name = "/global/account_id" +} diff --git a/main.tf b/main.tf index fc0cc6a..1eda524 100644 --- a/main.tf +++ b/main.tf @@ -23,166 +23,73 @@ */ locals { - deploy_artifact_key = "deploy.zip" - deployment_bucket_id = coalesce(var.deployment_bucket_id, data.aws_ssm_parameter.deployment_bucket_id.value) - source_hash = coalesce(var.git_sha, try(filebase64sha256(var.path), null)) - role_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/${var.role_name}" + description = coalesce(var.description, "Highwing Engine - ${title(var.engine_name)}") + handler = coalesce(var.handler, "engines/${var.engine_name}/lambda.handler") + + environment = data.aws_ssm_parameter.account_name.value + environment_variables = ( + startswith(local.environment, "production") + ? var.environment_variables + : merge(var.environment_variables, { + DEV_MODE_ENABLED = true + }) + ) } -# Configure default role permissions -resource "aws_iam_role_policy_attachment" "lambda_basic_execution" { - role = var.role_name - policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" -} - -resource "aws_iam_role_policy_attachment" "lambda_networking" { - role = var.role_name - policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole" -} - -resource "aws_iam_role_policy_attachment" "lambda_xray" { - role = var.role_name - policy_arn = "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess" -} - -resource "aws_iam_role_policy_attachment" "lambda_insights" { - role = var.role_name - policy_arn = "arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy" -} - -# S3 object to hold the deployed artifact -resource "aws_s3_bucket_object" "lambda_deploy_object" { - count = var.image_uri == null ? 1 : 0 - bucket = local.deployment_bucket_id - key = "${var.name}/${local.deploy_artifact_key}" - source = var.path - source_hash = md5(local.source_hash) - tags = merge(var.tags, { - GitSHA = var.git_sha - }) -} - -# The Lambda function itself -resource "aws_lambda_function" "lambda" { - function_name = var.name - description = var.description - handler = var.handler - layers = var.image_uri == null ? concat( - var.layer_arns, - ["arn:aws:lambda:us-east-1:580247275435:layer:LambdaInsightsExtension:14"] # ARN for us-east-1 - ) : null - memory_size = var.memory_size - publish = true - reserved_concurrent_executions = var.reserved_concurrent_executions - role = local.role_arn - runtime = var.runtime - package_type = var.image_uri == null ? "Zip" : "Image" - s3_bucket = var.image_uri == null ? local.deployment_bucket_id : null - s3_key = var.image_uri == null ? aws_s3_bucket_object.lambda_deploy_object[0].key : null - s3_object_version = var.image_uri == null ? aws_s3_bucket_object.lambda_deploy_object[0].version_id : null - image_uri = var.image_uri - tags = var.tags - timeout = var.timeout - - dynamic "vpc_config" { - for_each = length(var.subnet_ids) > 0 ? [1] : [] - content { - subnet_ids = var.subnet_ids - security_group_ids = var.security_group_ids - } - } - - dynamic "environment" { - for_each = length(var.environment) > 0 ? [1] : [] - content { - variables = var.environment - } - } - - tracing_config { - mode = "Active" - } - - lifecycle { - precondition { - condition = (var.image_uri != null && var.path == null) || (var.image_uri == null && var.path != null) - error_message = "Cannot specify image_uri AND path" +module "lambda_function" { + source = "terraform-aws-modules/lambda/aws" + version = "7.2.6" + + function_name = var.function_name + description = local.description + handler = local.handler + memory_size = var.memory_size + publish = true + timeout = var.timeout + + vpc_subnet_ids = var.subnet_ids + vpc_security_group_ids = var.security_group_ids + + attach_cloudwatch_logs_policy = true + attach_dead_letter_policy = true + attach_network_policy = true + attach_tracing_policy = true + + allowed_triggers = var.allowed_triggers + + number_of_policies = 2 + policies = [ + "arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy", + "arn:aws:iam::aws:policy/service-role/AWSLambdaSQSQueueExecutionRole" + ] + attach_policies = true + + attach_policy_statements = length(var.policy_statements) > 0 ? true : false + policy_statements = merge(var.policy_statements, { + ecr = { + effect = "Allow" + actions = [ + "ecr:BatchGetImage", + "ecr:GetDownloadUrlForLayer" + ] + resources = [var.ecr_arn] } - precondition { - condition = (var.image_uri != null && var.handler == null) || (var.image_uri == null && var.handler != null) - error_message = "Cannot specify image_uri AND handler" - } - precondition { - condition = (var.image_uri != null && length(var.layer_arns) == 0) || (var.image_uri == null) - error_message = "Cannot specify image_uri AND layer_arns" - } - precondition { - condition = (var.image_uri != null && var.path == null) || (var.image_uri == null && var.path != null) - error_message = "Cannot specify image_uri AND path" - } - precondition { - condition = (var.image_uri != null && var.runtime == null) || (var.image_uri == null && var.path != null) - error_message = "Cannot specify image_uri AND runtime" - } - } - -} - -# An alarm to notify of function errors -resource "aws_cloudwatch_metric_alarm" "lambda_errors" { - alarm_actions = [var.notifications_topic_arn] - alarm_description = "This metric monitors the error rate on the ${var.name} lambda" - alarm_name = "${var.name} - High Error Rate" - comparison_operator = "GreaterThanOrEqualToThreshold" - evaluation_periods = "2" - threshold = var.error_rate_alarm_threshold - treat_missing_data = "notBreaching" - - metric_query { - id = "error_rate" - expression = "errors/invocations*100" - label = "Error Rate" - return_data = "true" - } - - metric_query { - id = "errors" - - metric { - metric_name = "Errors" - namespace = "AWS/Lambda" - period = "600" - stat = "Sum" - unit = "Count" - - dimensions = { - FunctionName = aws_lambda_function.lambda.function_name - } - } - } + }) - metric_query { - id = "invocations" + cloudwatch_logs_retention_in_days = 1096 + dead_letter_target_arn = data.aws_sns_topic.notifications.arn + kms_key_arn = data.aws_ssm_parameter.kms_key_arn.value + tracing_mode = "Active" - metric { - metric_name = "Invocations" - namespace = "AWS/Lambda" - period = "600" - stat = "Sum" - unit = "Count" + create_package = false + image_uri = "${var.ecr_address}:lambda_${var.image_tag}" + package_type = "Image" + architectures = ["x86_64"] - dimensions = { - FunctionName = aws_lambda_function.lambda.function_name - } - } - } + event_source_mapping = var.event_source_mapping - tags = var.tags + environment_variables = merge(local.environment_variables, { + DD_SERVICE = replace(var.function_name, "-", "_") + }) } -# Configure logging in Cloudwatch -resource "aws_cloudwatch_log_group" "lambda" { - name = "/aws/lambda/${var.name}" - retention_in_days = var.log_retention_in_days - tags = var.tags -} diff --git a/outputs.tf b/outputs.tf index 95b0fd5..8cde8f4 100644 --- a/outputs.tf +++ b/outputs.tf @@ -1,19 +1,23 @@ -output "function_name" { - description = "Name of the created Lambda function" - value = aws_lambda_function.lambda.function_name +output "function_arn" { + value = module.lambda_function.lambda_function_arn } -output "function_arn" { - description = "ARN of the created/updated Lambda function" - value = aws_lambda_function.lambda.arn +output "qualified_function_arn" { + value = module.lambda_function.lambda_function_qualified_arn +} + +output "function_name" { + value = module.lambda_function.lambda_function_name } output "function_version" { - description = "Version of the created/updated Lambda function" - value = aws_lambda_function.lambda.version + value = module.lambda_function.lambda_function_version +} + +output "lambda_role_arn" { + value = module.lambda_function.lambda_role_arn } -output "function_invoke_arn" { - description = "ARN for invoking the created Lambda function" - value = aws_lambda_function.lambda.invoke_arn +output "lambda_role_name" { + value = module.lambda_function.lambda_role_name } diff --git a/variables.tf b/variables.tf new file mode 100644 index 0000000..f6d46b3 --- /dev/null +++ b/variables.tf @@ -0,0 +1,71 @@ +variable "allowed_triggers" { + type = map(any) + default = {} +} + +variable "engine_name" { + type = string +} + +variable "function_name" { + type = string +} + +variable "description" { + type = string + default = null +} + +variable "ecr_address" { + type = string +} + +variable "ecr_arn" { + type = string +} + +variable "environment_variables" { + type = map(any) + default = {} +} + +variable "event_source_mapping" { + description = "Map of event source mappings" + type = map(any) + default = {} +} + +variable "handler" { + type = string + default = null +} + +variable "image_tag" { + type = string + default = "latest" +} + +variable "memory_size" { + type = number + default = 1024 +} + +variable "policy_statements" { + type = map(any) + default = {} +} + +variable "subnet_ids" { + type = list(string) + default = [] +} + +variable "security_group_ids" { + type = list(string) + default = [] +} + +variable "timeout" { + type = string + default = "60" +} diff --git a/vars.tf b/vars.tf deleted file mode 100644 index 2494ad9..0000000 --- a/vars.tf +++ /dev/null @@ -1,116 +0,0 @@ -variable "description" { - default = null - description = "Description of the Lambda Function" - type = string -} - -variable "image_uri" { - default = null - description = "Image URI of the ECR container image for the Lambda Function. Overrides the use of path variable & s3 deployment object" - type = string -} - -variable "deployment_bucket_id" { - default = null - description = "ID of S3 bucket that should store our deployment artifacts. Will use the /account/DEPLOYMENT_BUCKET_ID value from SSM unless specified otherwise." - type = string -} - -variable "environment" { - default = {} - description = "Environment variables to be passed to the function" - type = map(string) -} - -variable "error_rate_alarm_threshold" { - default = 25 - description = "Error rate (in percent, 1-100) at which to trigger an alarm notification" - type = number -} - -variable "git_sha" { - default = null - description = "Hash generated by `git hash-object` in source repo and used to determine whether a lambda needs to be updated" - type = string -} - -variable "handler" { - default = null - description = "Name of the handler function inside the artifact (https://docs.aws.amazon.com/lambda/latest/dg/configuration-console.html)" - type = string -} - -variable "layer_arns" { - default = [] - description = "List of ARNs for layers to use with the function" - type = list(string) -} - -variable "log_retention_in_days" { - description = "Number of days to keep function logs in Cloudwatch" - type = number - default = 1096 -} - -variable "memory_size" { - description = "Amount of memory (in MB) to allocate to the function" - type = number - default = 128 -} - -variable "name" { - description = "Name for the function" - type = string -} - -variable "notifications_topic_arn" { - description = "SNS topic to send error notifications" - type = string -} - -variable "path" { - default = null - description = "Local path to a zipped artifact containing the function code" - type = string -} - -variable "reserved_concurrent_executions" { - type = number - description = "Reserved concurrent executions (none by default)" - default = null -} - -variable "role_name" { - description = "Name of the execution role for the function. It does not need to include logging/networking permissions - those policies will be added automatically." - type = string -} - -variable "runtime" { - default = null - type = string - description = "Language runtime for the function (https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html)" -} - -variable "security_group_ids" { - type = list(string) - description = "Security groups for the function (if run in a VPC)" - default = [] -} - -variable "subnet_ids" { - type = list(string) - description = "Subnets for the function (if run in a VPC)" - default = [] -} - -variable "tags" { - type = map(any) - description = "Tags to apply to created resources" - default = {} -} - -variable "timeout" { - type = number - description = "Function timeout in seconds" - default = 15 -}