Terraform-provider-aws: Using Lambda function modules with an empty environment

Created on 11 Jul 2017  ·  13Comments  ·  Source: hashicorp/terraform-provider-aws

Terraform Version

0.9.11

Affected Resource(s)

  • aws_lambda_function

Terraform Configuration Files

main.tf
module "edge" {
  source = "./modules/function"
  # ...
}
modules/function/main.tf
variable "environment" {
  type = "map"
  default = {}
}

resource "aws_lambda_function" "main" {
  function_name = "hello-world"
  # ...
  environment = {
    variables = "${var.environment}"
  }
}

Debug Output

  • module.edge.aws_lambda_function.main: 1 error(s) occurred:
  • aws_lambda_function.main: At least one field is expected inside environment

Expected Behavior

It should allow an empty object for the function environment variables, specifically because Lambda@Edge doesn't allow environment variables. Breaks all the function modules that want to be repurposed for edge functions.

Actual Behavior

It doesn't allow the empty object. If I try to go one step up as seen below, it produces a different error:

resource "aws_lambda_function" "main" {
  function_name = "hello-world"
  # ...
  environment = "${var.environment}"
}
  • module.edge.aws_lambda_function.main: environment: should be a list

Obviously environment is supposed to be a map, but it seems to expect a list. I can't find a workaround to providing a default empty object for the environment, aside from creating a seperate module specifically for edge functions, which would omit environment entirely.

bug terraform-0.12 upstream-terraform

Most helpful comment

yes, this is problematic in my case as well
I have a lambda module and I pass environment variables to it only if I need to

Terraform v. 0.10.7

All 13 comments

Also, according to the docs, aws_lambda_function.environment.variables is optional. But if you omit it, you get the At least one field is expected inside environment error, making it required.

I can also confirm this issue on my end and know of no workarounds to the problem.

yes, this is problematic in my case as well
I have a lambda module and I pass environment variables to it only if I need to

Terraform v. 0.10.7

Is there any developments on this?

The configuration language of Terraform (HCL) currently has some limitations when it comes to typing and lacks the ability to pass an "empty"/null value. The good news is that this is likely being addressed in the next major version of Terraform: https://www.hashicorp.com/blog/terraform-0-1-2-preview

In this situation, the following configuration will be available:

# Potential Terraform 0.12 configuration - implementation may change during development
resource "aws_lambda_function" "main" {
  # ... other configuration ...
  environment = var.environment
}

When var.environment is assigned to a new, special null value, this will trigger the provider to see the resource configuration as:

# Potential Terraform 0.12 equivalent after processing null value for environment argument
resource "aws_lambda_function" "main" {
  # ... other configuration ...
}

There are some upstream issues in Terraform core for tracking this feature prior to its release:

For what it's worth we found a workaround for this.

We created a local variable that checks the length of our environment_variables map.

locals {
  enable_environment_variables = "${length(var.environment_variables) > 0 ? "true" : "false"}"
}

We then created two resources that checked the enable_environment_variables flag. This will either create the lambda with or without environment variables.

# -------------------------------------------------------------------------------------
# Create Lambda Function with Environment Variables
# -------------------------------------------------------------------------------------
resource "aws_lambda_function" "lambda_function" {
  count             = "${local.enable_environment_variables == "true" ? 1 : 0}"
  function_name     = "${var.name}"
  ...

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

# -------------------------------------------------------------------------------------
# Create Lambda Function with NO Environment Variables
# -------------------------------------------------------------------------------------
resource "aws_lambda_function" "lambda_function_no_environment_variables" {
  count             = "${local.enable_environment_variables == "false" ? 1 : 0}"
  function_name     = "${var.name}"
  ...
}

I had the same problem and I solved it by passing a conditional:
 count = "$ {var.has_environment ? 1: 0}"
and I add the variable:

variable "has_environment" {    type = "string"    description = "true or false"    default = "true" }

In Terraform 12 you can use NULL as the value.
IE:
resource "aws_lambda_function" "lambda_handler" {
...
environment {
variables = var.environment
}
}

where the default value of var.environment is null

My solution actually doesn't work. It only works the first time. The 2nd time it is run, terraform will try and add
"+ environment {}"
Which will fail with the same error.

Hi folks 👋 It seems my note above was only partially correct, however I can confirm this is resolved in Terraform 0.12, which supports new functionality in the configuration language aimed at solving this issue. The new dynamic block syntax can be used to dynamically generate configuration blocks and their arguments. These can be combined with the new null value, which can be used to omit arguments as if they were not defined in the configuration at all.

Given this configuration:

terraform {
  required_providers {
    aws = "2.20.0"
  }
  required_version = "0.12.5"
}

provider "aws" {
  region = "us-east-1"
}

variable "test1" {
  type = list(object({
    variables = map(string)
  }))
  default = []
}

variable "test2" {
  type = list(object({
    variables = map(string)
  }))
  default = [{
    variables = {
      key1 = "value1"
      key2 = "value2"
    }
  }]
}

resource "aws_iam_role" "test" {
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = "sts:AssumeRole"
      Principal = {
        Service = "lambda.amazonaws.com"
      }
      Effect = "Allow"
      Sid    = ""
    }]
  })
}

resource "aws_iam_role_policy_attachment" "test" {
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
  role       = aws_iam_role.test.name
}

resource "aws_lambda_function" "test1" {
  depends_on = [aws_iam_role_policy_attachment.test]

  filename      = "test.zip"
  function_name = "test1"
  role          = aws_iam_role.test.arn
  handler       = "exports.example"
  runtime       = "nodejs8.10"

  dynamic "environment" {
    for_each = var.test1

    content {
      variables = environment.value.variables
    }
  }
}

resource "aws_lambda_function" "test2" {
  depends_on = [aws_iam_role_policy_attachment.test]

  filename      = "test.zip"
  function_name = "test2"
  role          = aws_iam_role.test.arn
  handler       = "exports.example"
  runtime       = "nodejs8.10"

  dynamic "environment" {
    for_each = var.test2

    content {
      variables = environment.value.variables
    }
  }
}

Produces the following apply output:

$ terraform apply

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_iam_role.test will be created
  + resource "aws_iam_role" "test" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "lambda.amazonaws.com"
                        }
                      + Sid       = ""
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + create_date           = (known after apply)
      + force_detach_policies = false
      + id                    = (known after apply)
      + max_session_duration  = 3600
      + name                  = (known after apply)
      + path                  = "/"
      + unique_id             = (known after apply)
    }

  # aws_iam_role_policy_attachment.test will be created
  + resource "aws_iam_role_policy_attachment" "test" {
      + id         = (known after apply)
      + policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
      + role       = (known after apply)
    }

  # aws_lambda_function.test1 will be created
  + resource "aws_lambda_function" "test1" {
      + arn                            = (known after apply)
      + filename                       = "test.zip"
      + function_name                  = "test1"
      + handler                        = "exports.example"
      + id                             = (known after apply)
      + invoke_arn                     = (known after apply)
      + last_modified                  = (known after apply)
      + memory_size                    = 128
      + publish                        = false
      + qualified_arn                  = (known after apply)
      + reserved_concurrent_executions = -1
      + role                           = (known after apply)
      + runtime                        = "nodejs8.10"
      + source_code_hash               = (known after apply)
      + source_code_size               = (known after apply)
      + timeout                        = 3
      + version                        = (known after apply)

      + tracing_config {
          + mode = (known after apply)
        }
    }

  # aws_lambda_function.test2 will be created
  + resource "aws_lambda_function" "test2" {
      + arn                            = (known after apply)
      + filename                       = "test.zip"
      + function_name                  = "test2"
      + handler                        = "exports.example"
      + id                             = (known after apply)
      + invoke_arn                     = (known after apply)
      + last_modified                  = (known after apply)
      + memory_size                    = 128
      + publish                        = false
      + qualified_arn                  = (known after apply)
      + reserved_concurrent_executions = -1
      + role                           = (known after apply)
      + runtime                        = "nodejs8.10"
      + source_code_hash               = (known after apply)
      + source_code_size               = (known after apply)
      + timeout                        = 3
      + version                        = (known after apply)

      + environment {
          + variables = {
              + "key1" = "value1"
              + "key2" = "value2"
            }
        }

      + tracing_config {
          + mode = (known after apply)
        }
    }

Plan: 4 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

aws_iam_role.test: Creating...
aws_iam_role.test: Creation complete after 0s [id=terraform-20190724062422187100000001]
aws_iam_role_policy_attachment.test: Creating...
aws_iam_role_policy_attachment.test: Creation complete after 0s [id=terraform-20190724062422187100000001-20190724062422605200000002]
aws_lambda_function.test2: Creating...
aws_lambda_function.test1: Creating...
aws_lambda_function.test2: Still creating... [10s elapsed]
aws_lambda_function.test1: Still creating... [10s elapsed]
aws_lambda_function.test1: Creation complete after 16s [id=test1]
aws_lambda_function.test2: Creation complete after 17s [id=test2]

Apply complete! Resources: 4 added, 0 changed, 0 destroyed.

There may be ways to simplify this setup, but from a functionality standpoint, the relevant pieces are in place. Enjoy! 🚀

Building on @bflad's example I'm using the following in a module which avoids the weird array of maps of maps variable.

variable "environment_vars" {
  type    = map(string)
  default = null
}

locals {
  environment_map = var.environment_vars == null ? [] : [var.environment_vars]
}

resource "aws_lambda_function" "lambda" {
  # ... omitted

  dynamic "environment" {
    for_each = local.environment_map
    content {
      variables = environment.value
    }
  }
}

Snippet from @blefevre didn't work for me - a slight modification was needed.

variable "environment_vars" {
  type    = map(string)
  default = null
}

locals {
  environment_map = var.environment_vars[*]
}

resource "aws_lambda_function" "lambda" {
  # ... omitted

  dynamic "environment" {
    for_each = local.environment_map
    content {
      variables = environment.value
    }
  }
}

I'm going to lock this issue because it has been closed for _30 days_ ⏳. This helps our maintainers find and focus on the active issues.

If you feel this issue should be reopened, we encourage creating a new issue linking back to this one for added context. Thanks!

Was this page helpful?
0 / 5 - 0 ratings