diff --git a/.gitignore b/.gitignore index 576c848..8a1499d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ +.terraform* /_test/.terraform /_test/.terraform.lock.hcl diff --git a/CHANGELOG.md b/CHANGELOG.md index f99d3c5..4cdcf4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## v1.3.0 + +- [Add VPC support](https://github.com/babbel/terraform-aws-lambda-with-inline-code/pull/19) + ## v1.2.1 - [Empty environment is handled correctly](https://github.com/babbel/terraform-aws-lambda-with-inline-code/pull/15) diff --git a/main.tf b/main.tf index f6ecbf4..ee24a6c 100644 --- a/main.tf +++ b/main.tf @@ -12,6 +12,15 @@ resource "aws_lambda_function" "this" { role = aws_iam_role.this.arn + dynamic "vpc_config" { + for_each = local.vpc_configs + + content { + subnet_ids = vpc_config.value.subnets[*].id + security_group_ids = values(aws_security_group.this)[*].id + } + } + dynamic "environment" { // local.environments is built using a merge, and merges always result in a map // so we can safely assume we're dealing with a map here. @@ -41,6 +50,8 @@ data "archive_file" "this" { output_path = ".terraform/tmp/lambda/${var.function_name}.zip" } +# IAM role + resource "aws_iam_role" "this" { name = "lambda-${var.function_name}" @@ -60,6 +71,8 @@ data "aws_iam_policy_document" "lambda-assume-role" { } } +# CloudWatch Logs group + resource "aws_iam_role_policy" "cloudwatch-log-group" { role = aws_iam_role.this.name name = "cloudwatch-log-group" @@ -86,6 +99,104 @@ data "aws_iam_policy_document" "cloudwatch-log-group" { } } +# VPC config + +locals { + # convert `var.vpc_config` into a `for_each`-compatible local + vpc_config_key = "lambda" + vpc_configs = var.vpc_config != null ? { (local.vpc_config_key) = var.vpc_config } : {} +} + +resource "aws_security_group" "this" { + for_each = local.vpc_configs + + name = "lambda-${var.function_name}" + description = "Lambda: ${var.function_name}" + vpc_id = each.value.vpc.id + + tags = merge({ + Name = "Lambda: ${var.function_name}" + }, var.tags) + + lifecycle { + create_before_destroy = true + } +} + +resource "aws_security_group_rule" "egress" { + for_each = aws_security_group.this + + security_group_id = each.value.id + + type = "egress" + cidr_blocks = ["0.0.0.0/0"] + protocol = "-1" + from_port = 0 + to_port = 0 + + lifecycle { + create_before_destroy = true + } +} + +data "aws_iam_policy_document" "vpc" { + for_each = local.vpc_configs + + statement { + actions = ["ec2:CreateNetworkInterface"] + + resources = flatten([ + "arn:${data.aws_partition.current[local.vpc_config_key].partition}:ec2:${data.aws_region.current[local.vpc_config_key].name}:${data.aws_caller_identity.current[local.vpc_config_key].account_id}:network-interface/*", + each.value.subnets[*].arn, + values(aws_security_group.this)[*].arn + ]) + } + + statement { + actions = ["ec2:DescribeNetworkInterfaces"] + resources = ["*"] + + condition { + variable = "ec2:Region" + test = "StringEquals" + values = [data.aws_region.current[local.vpc_config_key].name] + } + } + + statement { + # It is not possible to restrict this permissions because the Lambda runtime + # is making a DryRun call to this action without any request parameters + # before actually creating the Lambda function. + actions = ["ec2:DeleteNetworkInterface"] + resources = ["*"] + + condition { + variable = "ec2:Region" + test = "StringEquals" + values = [data.aws_region.current[local.vpc_config_key].name] + } + } +} + +resource "aws_iam_role_policy" "vpc" { + for_each = data.aws_iam_policy_document.vpc + + role = aws_iam_role.this.name + name = "vpc" + policy = each.value.json +} + +data "aws_partition" "current" { + for_each = local.vpc_configs +} + +data "aws_region" "current" { + for_each = local.vpc_configs +} + +data "aws_caller_identity" "current" { + for_each = local.vpc_configs +} # Secret environment variables diff --git a/outputs.tf b/outputs.tf index 1ce9901..c5c37f7 100644 --- a/outputs.tf +++ b/outputs.tf @@ -15,3 +15,9 @@ output "iam_role" { description = "The IAM role the Lambda function will assume." } + +output "security_group" { + value = lookup(aws_security_group.this, local.vpc_config_key, null) + + description = "The VPC security group the Lambda function will use if `var.vpc_config` is specified; `null` otherwise." +} diff --git a/variables.tf b/variables.tf index 6deb4a6..eca2c0f 100644 --- a/variables.tf +++ b/variables.tf @@ -97,3 +97,29 @@ variable "timeout" { description = "The amount of time (in seconds) per execution before stopping it." } + +variable "vpc_config" { + type = object({ + subnets = list( + object({ + arn = string + id = string + }) + ) + + vpc = object({ + id = string + }) + }) + + default = null + + description = <