Skip to content

Commit

Permalink
Create re-usable Lambda Terraform module (#596)
Browse files Browse the repository at this point in the history
  • Loading branch information
austinbyers authored and ryandeivert committed Mar 14, 2018
1 parent ca9c511 commit fc2ed12
Show file tree
Hide file tree
Showing 9 changed files with 422 additions and 2 deletions.
64 changes: 64 additions & 0 deletions terraform/modules/tf_lambda/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Lambda Module
This Terraform module creates a single AWS Lambda function and its related components:

* IAM execution role with basic permissions
* Lambda function
* Production alias
* CloudWatch log group
* CloudWatch metric alarms related to Lambda

All StreamAlert Lambda functions will eventually leverage this module.

The created IAM role has permission to publish CloudWatch logs and metrics. To add function-specific
permissions, attach/inline them to the created IAM role.

## Example
```hcl
module "alert_processor" {
function_name = "alert_processor"
handler = "stream_alert.alert_processor.main.handler"
source_bucket = "SOURCE_BUCKET"
source_object_key = "SOURCE_OBJECT_KEY"
environment_variables = {
LOGGER_LEVEL = "info"
}
// Commonly used optional variables
enabled = true
description = "Function Description"
memory_size_mb = 128
timeout_sec = 60
vpc_subnet_ids = ["abc"]
vpc_security_group_ids = ["id0"]
aliased_version = 1
log_retention_days = 14
alarm_actions = ["SNS_ARN"]
errors_alarm_threshold = 1
enable_iterator_age_alarm = true
}
// Add additional permissions
resource "aws_iam_role_policy" "policy" {
name = "CustomPolicy"
role = "${module.alert_processor.role_id}"
policy = "${data.aws_iam_policy_document.policy.json}"
}
data "aws_iam_policy_document" "policy" {
statement {
effect = "Allow"
actions = ["s3:PutObject"]
resources = ["arn:aws:s3:::..."]
}
}
```

For a complete list of available options and their descriptions, see [`variables.tf`](variables.tf).

## Outputs
If your Lambda function is in a VPC, `function_vpc_arn` is the ARN of the generated Lambda
function. Otherwise, it will be `function_no_vpc_arn`. (This split is a workaround for a
[Terraform bug](https://github.com/terraform-providers/terraform-provider-aws/issues/443)).

This module also exports the `role_arn` and `role_id` for the Lambda execution role.
48 changes: 48 additions & 0 deletions terraform/modules/tf_lambda/iam.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
data "aws_iam_policy_document" "lambda_execution_policy" {
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]

principals {
type = "Service"
identifiers = ["lambda.amazonaws.com"]
}
}
}

// Create the execution role for the Lambda function.
resource "aws_iam_role" "role" {
count = "${var.enabled}"
name = "${var.function_name}_role"
assume_role_policy = "${data.aws_iam_policy_document.lambda_execution_policy.json}"
}

// Base permissions - Allow creating logs and publishing metrics
data "aws_iam_policy_document" "logs_metrics_policy" {
statement {
effect = "Allow"

actions = [
"cloudwatch:PutMetricData",
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
]

resources = ["*"]
}
}

resource "aws_iam_role_policy" "logs_metrics_policy" {
count = "${var.enabled}"
name = "LogsAndMetrics"
role = "${aws_iam_role.role.id}"
policy = "${data.aws_iam_policy_document.logs_metrics_policy.json}"
}

// Attach VPC policy (if applicable)
resource "aws_iam_role_policy_attachment" "vpc_access" {
count = "${var.enabled && local.vpc_enabled ? 1 : 0}"
role = "${aws_iam_role.role.id}"
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole"
}
89 changes: 89 additions & 0 deletions terraform/modules/tf_lambda/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Generic module for any StreamAlert Lambda function.
// TODO - migrate all Lambda functions and Lambda metric alarms to use this module

locals {
vpc_enabled = "${length(var.vpc_subnet_ids) > 0}"
}

// Either the function_vpc or the function_no_vpc resource will be used
resource "aws_lambda_function" "function_vpc" {
count = "${var.enabled && local.vpc_enabled ? 1 : 0}"
function_name = "${var.function_name}"
description = "${var.description}"
runtime = "${var.runtime}"
role = "${aws_iam_role.role.arn}"
handler = "${var.handler}"
memory_size = "${var.memory_size_mb}"
publish = "${var.auto_publish_versions}"
timeout = "${var.timeout_sec}"
s3_bucket = "${var.source_bucket}"
s3_key = "${var.source_object_key}"

environment {
variables = "${var.environment_variables}"
}

// Empty vpc_config lists are theoretically supported, but it actually breaks subsequent deploys:
// https://github.com/terraform-providers/terraform-provider-aws/issues/443
vpc_config {
security_group_ids = "${var.vpc_security_group_ids}"
subnet_ids = "${var.vpc_subnet_ids}"
}

tags {
Name = "${var.name_tag}"
}

// We need VPC access before the function can be created
depends_on = ["aws_iam_role_policy_attachment.vpc_access"]
}

resource "aws_lambda_alias" "production_alias_vpc" {
count = "${var.enabled && local.vpc_enabled ? 1 : 0}"
name = "production"
description = "Production alias for ${var.function_name}"
function_name = "${var.function_name}"
function_version = "${var.aliased_version == "" ? aws_lambda_function.function_vpc.version : var.aliased_version}"
depends_on = ["aws_lambda_function.function_vpc"]
}

resource "aws_lambda_function" "function_no_vpc" {
count = "${var.enabled && !(local.vpc_enabled) ? 1 : 0}"
function_name = "${var.function_name}"
description = "${var.description}"
runtime = "${var.runtime}"
role = "${aws_iam_role.role.arn}"
handler = "${var.handler}"
memory_size = "${var.memory_size_mb}"
publish = "${var.auto_publish_versions}"
timeout = "${var.timeout_sec}"
s3_bucket = "${var.source_bucket}"
s3_key = "${var.source_object_key}"

environment {
variables = "${var.environment_variables}"
}

tags {
Name = "${var.name_tag}"
}
}

resource "aws_lambda_alias" "production_alias_no_vpc" {
count = "${var.enabled && !(local.vpc_enabled) ? 1 : 0}"
name = "production"
description = "Production alias for ${var.function_name}"
function_name = "${var.function_name}"
function_version = "${var.aliased_version == "" ? aws_lambda_function.function_no_vpc.version : var.aliased_version}"
depends_on = ["aws_lambda_function.function_no_vpc"]
}

resource "aws_cloudwatch_log_group" "lambda_log_group" {
count = "${var.enabled}"
name = "/aws/lambda/${var.function_name}"
retention_in_days = "${var.log_retention_days}"

tags {
Name = "${var.name_tag}"
}
}
56 changes: 56 additions & 0 deletions terraform/modules/tf_lambda/metrics.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
resource "aws_cloudwatch_metric_alarm" "lambda_invocation_errors" {
count = "${var.enabled && var.enable_metric_alarms ? 1 : 0}"
alarm_name = "${var.function_name}_invocation_errors"
namespace = "AWS/Lambda"
metric_name = "Errors"
statistic = "Sum"
comparison_operator = "GreaterThanThreshold"
threshold = "${var.errors_alarm_threshold}"
evaluation_periods = "${var.errors_alarm_evaluation_periods}"
period = "${var.errors_alarm_period_secs}"
alarm_description = "StreamAlert Lambda Invocation Errors: ${var.function_name}"
alarm_actions = "${var.alarm_actions}"

dimensions {
FunctionName = "${var.function_name}"
Resource = "${var.function_name}:production"
}
}

resource "aws_cloudwatch_metric_alarm" "lambda_throttles" {
count = "${var.enabled && var.enable_metric_alarms ? 1 : 0}"
alarm_name = "${var.function_name}_throttles"
namespace = "AWS/Lambda"
metric_name = "Throttles"
statistic = "Sum"
comparison_operator = "GreaterThanThreshold"
threshold = "${var.throttles_alarm_threshold}"
evaluation_periods = "${var.throttles_alarm_evaluation_periods}"
period = "${var.throttles_alarm_period_secs}"
alarm_description = "StreamAlert Lambda Throttles: ${var.function_name}"
alarm_actions = "${var.alarm_actions}"

dimensions {
FunctionName = "${var.function_name}"
Resource = "${var.function_name}:production"
}
}

resource "aws_cloudwatch_metric_alarm" "streamalert_lambda_iterator_age" {
count = "${var.enabled && var.enable_metric_alarms && var.enable_iterator_age_alarm ? 1 : 0}"
alarm_name = "${var.function_name}_iterator_age"
namespace = "AWS/Lambda"
metric_name = "IteratorAge"
statistic = "Maximum"
comparison_operator = "GreaterThanThreshold"
threshold = "${var.iterator_age_alarm_threshold_ms}"
evaluation_periods = "${var.iterator_age_alarm_evaluation_periods}"
period = "${var.iterator_age_alarm_period_secs}"
alarm_description = "StreamAlert Lambda High Iterator Age: ${var.function_name}"
alarm_actions = "${var.alarm_actions}"

dimensions {
FunctionName = "${var.function_name}"
Resource = "${var.function_name}:production"
}
}
17 changes: 17 additions & 0 deletions terraform/modules/tf_lambda/output.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Defined only if the Lambda is in a VPC
output "function_vpc_arn" {
value = "${aws_lambda_function.function_vpc.arn}"
}

// Defined only if the Lambda is NOT in a VPC
output "function_no_vpc_arn" {
value = "${aws_lambda_function.function_no_vpc.arn}"
}

output "role_arn" {
value = "${aws_iam_role.role.arn}"
}

output "role_id" {
value = "${aws_iam_role.role.id}"
}
Loading

0 comments on commit fc2ed12

Please sign in to comment.