Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unable to use the terraform plan to get the relation between different resources before run terraform apply #32012

Open
moelasmar opened this issue Oct 14, 2022 · 2 comments
Labels
enhancement new new issue not yet triaged

Comments

@moelasmar
Copy link

Terraform Version

Terraform v1.2.6
on darwin_amd64
+ provider registry.terraform.io/hashicorp/aws v4.34.0

Your version of Terraform is out of date! The latest version
is 1.3.2. You can update by downloading from https://www.terraform.io/downloads.html

Use Cases

Our team is working on a feature that allow terraform customers to locally test their lambda resources using the SAM CLI tool.
Our logic is to use the terraform plan output to get the Lambda, API Gateway, and StepFunction resources defined in the terraform project and initiate some local docker containers that customer can invoke to simulate a lambda function call locally.

We found some limitations in finding the relation between a Lambda function and lambda layers if the customer does not execute terraform apply as the layers attribute of the lambda function in the planned_values section will be always null as the layers are not created yet.

The use cases we found:

Multiple instances of the resources definition

If the customer defines multiple instances of a lambda function and also a lambda layer using count or for_each and use the count.index or the for_each key to link between these instances, we could not determine which layer instance should be linked to which function instance. See the example below:

resource "aws_lambda_layer_version" "layer" {
  count = 2
  filename   = var.layers_source_code[count.index]
  layer_name = "lambda_layer${count.index}"

  compatible_runtimes = ["python3.8"]
}

resource "aws_lambda_function" "function" {
    count = 2
    filename = "/Volumes/workplace/tf_poc/investigations/fixtures/get_country_languages.zip"
    handler = "index.lambda_handler"
    runtime = "python3.8"
    function_name = "get_country_languages${count.index}"
    role = aws_iam_role.iam_for_lambda.arn
    layers = [aws_lambda_layer_version.layer[count.index].arn]
}

we should be able to know that there is a relation between function aws_lambda_function.function[0] and aws_lambda_layer_version.layer[0] and also between aws_lambda_function.function[1] and aws_lambda_layer_version.layer[1] but actually we could not get this information from the terraform plan.

The layers property in the functions instances is null as expected, as the layers are not created yet and there is no ARN generated for them.

{
 "planned_values": {
    "root_module": {
      "resources": [
        {
          "address": "aws_lambda_function.function[0]",
          ..
          "values": {
            "filename": "/Volumes/workplace/tf_poc/investigations/fixtures/get_country_languages.zip",
            "function_name": "get_country_languages0",
            "handler": "index.lambda_handler",
            ...
          },
          "sensitive_values": {
            "layers": [],
            ...
          }
        },
        {
          "address": "aws_lambda_function.function[1]",
          ...
          "values": {
            "filename": "/Volumes/workplace/tf_poc/investigations/fixtures/get_country_languages.zip",
            "function_name": "get_country_languages1",
            "handler": "index.lambda_handler",
            ...
          },
          "sensitive_values": {
            "layers": [],
            ...
          }
        },
        {
          "address": "aws_lambda_layer_version.layer[0]",
          ...
          "values": {
            "compatible_runtimes": ["python3.8"],
            "filename": "/Volumes/workplace/tf_poc/investigations/fixtures/layer1.zip",
            "layer_name": "lambda_layer0",
            ...
          }
        },
        {
          "address": "aws_lambda_layer_version.layer[1]",
          ...
          "values": {
            "compatible_runtimes": ["python3.8"],
            "filename": "/Volumes/workplace/tf_poc/investigations/fixtures/layer2.zip",
            "layer_name": "lambda_layer1",
            ....
          },
        }
      ]
    }
  }
}

We could not use the configuration section as it only contains the resource definition, but does not contain each instance. Also, the layers attribute refer to the layer resource configuration and count.index

{
  "configuration": {
    ...
    "root_module": {
      "resources": [
        {
          "address": "aws_lambda_function.function",
          ...
          "expressions": {
           ...
            "layers": {
              "references": [
                "aws_lambda_layer_version.layer",
                "count.index"
              ]
            },
          },
          "schema_version": 0,
          "count_expression": {
            "constant_value": 2
          }
        },
        {
          "address": "aws_lambda_layer_version.layer",
          "expressions": {...}
        }
      ],
    }
  },
}

Even using terraform graph command, we found that each lambda function instance has a relation to both layers instances, although it is not correct.

"[root] aws_lambda_function.function[0]" -> "[root] aws_lambda_layer_version.layer[0]"
"[root] aws_lambda_function.function[0]" -> "[root] aws_lambda_layer_version.layer[1]"
...
"[root] aws_lambda_function.function[1]" -> "[root] aws_lambda_layer_version.layer[0]"
"[root] aws_lambda_function.function[1]" -> "[root] aws_lambda_layer_version.layer[1]"

Define the relation conditionally

If the customer chooses between multiple lambda layers conditionally to set a lambda function resource layers. We could not determine which layer should be linked to this function. See the example below:

resource "aws_lambda_layer_version" "layer1" {
  filename   = var.layers_source_code[0]
  layer_name = "lambda_layer0"
  compatible_runtimes = ["python3.8"]
}

resource "aws_lambda_layer_version" "layer2" {
  filename   = var.layers_source_code[1]
  layer_name = "lambda_layer2"
  compatible_runtimes = ["python3.8"]
}

resource "aws_lambda_function" "function" {
    filename = "/Volumes/workplace/tf_poc/investigations/fixtures/get_country_languages.zip"
    handler = "index.lambda_handler"
    runtime = "python3.8"
    function_name = "function"
    role = aws_iam_role.iam_for_lambda.arn
    layers = var.env=="prod"? [aws_lambda_layer_version.layer1.arn]:[aws_lambda_layer_version.layer2.arn]
}

In case if the var.env is prod, we should be able to know that there is a relation between function aws_lambda_function.function and aws_lambda_layer_version.layer1 but actually we could not get this information from the terraform plan or terraform graph.

The layers property in the functions instances is null as expected, as the layers are not created yet and there is no ARN generated for them.

{
  "planned_values": {
    "root_module": {
      "resources": [
        {
          "address": "aws_lambda_function.function",
          "values": {
            "filename": "/Volumes/workplace/tf_poc/investigations/fixtures/get_country_languages.zip",
            "function_name": "function",
            "handler": "index.lambda_handler",
            ...
          },
          "sensitive_values": {
            "layers": [],
            ...
          }
        },
        {
          "address": "aws_lambda_layer_version.layer1",
          "values": {
            "compatible_runtimes": ["python3.8"],
            "filename": "/Volumes/workplace/tf_poc/investigations/fixtures/layer1.zip",
            "layer_name": "lambda_layer0",
            ...
          },
        },
        {
          "address": "aws_lambda_layer_version.layer2",
          "values": {
            "compatible_runtimes": ["python3.8"],
            "filename": "/Volumes/workplace/tf_poc/investigations/fixtures/layer2.zip",
            "layer_name": "lambda_layer2",
          },
        }
      ]
    }
  },
}

And in the configuration section, the Layers attribute is linked to both layers resources which is not correct

{
  "configuration": {
    "root_module": {
      "resources": [
        {
          "address": "aws_lambda_function.function",
          ...
          "expressions": {
            ...
            "layers": {
              "references": [
                "var.env",
                "aws_lambda_layer_version.layer1.arn",
                "aws_lambda_layer_version.layer1",
                "aws_lambda_layer_version.layer2.arn",
                "aws_lambda_layer_version.layer2"
              ]
            },
          },
          "schema_version": 0
        },
        {
          "address": "aws_lambda_layer_version.layer1",
          "expressions": {...},
          "schema_version": 0
        },
        {
          "address": "aws_lambda_layer_version.layer2",
          "expressions": {...},
          "schema_version": 0
        }
      ],
    }
  },
}

Also using the terraform graph, the function is linked to both layers:

"[root] aws_lambda_function.function" -> "[root] aws_iam_role.iam_for_lambda"
"[root] aws_lambda_function.function" -> "[root] aws_lambda_function.function (expand)"
"[root] aws_lambda_function.function" -> "[root] aws_lambda_layer_version.layer1"
"[root] aws_lambda_function.function" -> "[root] aws_lambda_layer_version.layer2"
"[root] aws_lambda_function.function" -> "[root] var.env"

Attempted Solutions

We could not find a way to solve these limitations.

Proposal

Is it possible to update the terraform plan command to update the plan by following one of the following solutions. To avoid any issues for current users we can make these updates to be enabled by some flag.

Solution 1

Is it possible to update the planned value to make the layers attribute to refer to the correct layer instance arn as expression instead of string. It can be something like this:

{
 "planned_values": {
    "root_module": {
      "resources": [
        {
          "address": "aws_lambda_function.function[0]",
          ..
          "values": {
            "filename": "/Volumes/workplace/tf_poc/investigations/fixtures/get_country_languages.zip",
            "function_name": "get_country_languages0",
            "handler": "index.lambda_handler",
            ...
          },
          "expressions":{
            "layers": {
              "references": [
                "aws_lambda_layer_version.layer[0]"
              ]
            },
          },
        },
        {
          "address": "aws_lambda_function.function[1]",
          ...
          "values": {
            "filename": "/Volumes/workplace/tf_poc/investigations/fixtures/get_country_languages.zip",
            "function_name": "get_country_languages1",
            "handler": "index.lambda_handler",
            ...
          },
          "expressions":{
            "layers": {
              "references": [
                "aws_lambda_layer_version.layer[1]"
              ]
            },
        },
        {
          "address": "aws_lambda_layer_version.layer[0]",
          ...
          "values": {
            "compatible_runtimes": ["python3.8"],
            "filename": "/Volumes/workplace/tf_poc/investigations/fixtures/layer1.zip",
            "layer_name": "lambda_layer0",
            ...
          }
        },
        {
          "address": "aws_lambda_layer_version.layer[1]",
          ...
          "values": {
            "compatible_runtimes": ["python3.8"],
            "filename": "/Volumes/workplace/tf_poc/investigations/fixtures/layer2.zip",
            "layer_name": "lambda_layer1",
            ....
          },
        }
      ]
    }
  }
}

Solution 2:

Can you change the configuration section to contain each instance of the resource, and to set the references after doing any required calculations. It can be something like this

{
  "configuration": {
    ...
    "root_module": {
      "resources": [
        {
          "address": "aws_lambda_function.function[0]",
          ...
          "expressions": {
           ...
            "layers": {
              "references": [
                "aws_lambda_layer_version.layer[0]"
              ]
            },
          },
          "schema_version": 0
        },
        {
          "address": "aws_lambda_function.function[1]",
          ...
          "expressions": {
           ...
            "layers": {
              "references": [
                "aws_lambda_layer_version.layer[1]"
              ]
            },
          },
          "schema_version": 0
        },
        {
          "address": "aws_lambda_layer_version.layer[0]",
          "expressions": {...}
        },
        {
          "address": "aws_lambda_layer_version.layer[1]",
          "expressions": {...}
        }
      ],
    }
  },
}

If the previous solutions can not be implemented, is there any library that can help us to parse the terraform project to get all the modules, resources defined in the project and a way to process the expressions and functions.

References

No response

@crw
Copy link
Collaborator

crw commented Oct 18, 2022

Thanks for this request!

@apparentlymart
Copy link
Member

Hi @moelasmar!

Unfortunately Terraform does not itself track the information you are hoping for here: for dependency resolution purposes, dependencies are between whole resource blocks rather than individual instances. Terraform evaluates all of the instances of the first resource first, and only then begins evaluating instances of the second resource that refers to the first. Terraform has no way to know which instances depend on which in the general case because the index key can itself be dynamic.

However, I think there is a potential solution to this specific case if the ARN syntax for Lambda layer versions is itself predictable before they are created. I'm not familiar with the way Lambda generates ARNs, but I trust that you'll be more familiar with that than I am and can confirm whether what I'm about to suggest is feasible.

Terraform's execution model allows providers to pre-populate attributes during the planning phase if and only if the provider has enough information to predict exactly what the final value will be after apply. If it's possible to predict the exact content of the ARN for a lambda version before that object is created then Terraform's AWS provider could pre-populate the arn attribute during the planning step and then your plan output would have the exact ARN of the target object, without the need for any inference through Terraform's expressions.

This solution would not be feasible if the ARN for a Lambda layer version contains any information that will be decided only during the create step, because in that case the provider can of course not guess what the final ARN will be until the object has already been created.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement new new issue not yet triaged
Projects
None yet
Development

No branches or pull requests

3 participants