Upgraded to TF 1.6 and getting "(depends on a resource or a module with changes pending)" when nothing has changed

On Terraform 1.4, the Terraform Plan showed no changes. Having recently upgraded to the latest v1.6 (didn’t change the AWS Provider which was a v4, but issue still exists on the latest v5), the plan shows (single example clipped):

  # module.lambdas["queue_housekeeper"].data.aws_iam_policy_document.lambda_permissions_policy_document will be read during apply
  # (depends on a resource or a module with changes pending)
 <= data "aws_iam_policy_document" "lambda_permissions_policy_document" {
      + id   = (known after apply)
      + json = (known after apply)

      + statement {
          + actions   = [
              + "logs:CreateLogGroup",
              + "logs:CreateLogStream",
              + "xray:PutTelemetryRecords",
              + "xray:PutTraceSegments",
            ]
          + effect    = "Allow"
          + resources = [
              + "*",
            ]
          + sid       = "LambdaMonitoring"
        }
      + statement {
          + actions   = [
              + "logs:DescribeLogGroups",
              + "logs:DescribeLogStreams",
              + "logs:PutLogEvents",
              + "logs:PutRetentionPolicy",
            ]
          + effect    = "Allow"
          + resources = [
              + "arn:aws:logs:*:*:*",
            ]
          + sid       = "Logs"
        }
      + statement {
          + actions   = [
              + "logs:PutLogEvents",
            ]
          + effect    = "Allow"
          + resources = [
              + "arn:aws:logs:*:*:log-group:/aws/lambda-insights:*",
            ]
          + sid       = "LambdaInsights"
        }
      + statement {
          + actions   = [
              + "ec2:CreateNetworkInterface",
              + "ec2:DeleteNetworkInterface",
              + "ec2:DescribeNetworkInterfaces",
            ]
          + effect    = "Allow"
          + resources = [
              + "*",
            ]
          + sid       = "EC2"
        }
      + statement {
          + actions   = [
              + "ecr:Describe*",
              + "ecr:Get*",
              + "ecr:List*",
            ]
          + effect    = "Allow"
          + resources = [
              + "*",
            ]
          + sid       = "ECR"
        }
    }

  # module.lambdas["queue_housekeeper"].aws_iam_policy.lambda_permissions_policy will be updated in-place
  ~ resource "aws_iam_policy" "lambda_permissions_policy" {
        id          = "arn:aws:iam::745249840871:policy/lambda_access_policy_for_queue_housekeeper"
        name        = "lambda_access_policy_for_queue_housekeeper"
      ~ policy      = jsonencode(
            {
              - Statement = [
                  - {
                      - Action   = [
                          - "xray:PutTraceSegments",
                          - "xray:PutTelemetryRecords",
                          - "logs:CreateLogStream",
                          - "logs:CreateLogGroup",
                        ]
                      - Effect   = "Allow"
                      - Resource = "*"
                      - Sid      = "LambdaMonitoring"
                    },
                  - {
                      - Action   = [
                          - "logs:PutRetentionPolicy",
                          - "logs:PutLogEvents",
                          - "logs:DescribeLogStreams",
                          - "logs:DescribeLogGroups",
                        ]
                      - Effect   = "Allow"
                      - Resource = "arn:aws:logs:*:*:*"
                      - Sid      = "Logs"
                    },
                  - {
                      - Action   = "logs:PutLogEvents"
                      - Effect   = "Allow"
                      - Resource = "arn:aws:logs:*:*:log-group:/aws/lambda-insights:*"
                      - Sid      = "LambdaInsights"
                    },
                  - {
                      - Action   = [
                          - "ec2:DescribeNetworkInterfaces",
                          - "ec2:DeleteNetworkInterface",
                          - "ec2:CreateNetworkInterface",
                        ]
                      - Effect   = "Allow"
                      - Resource = "*"
                      - Sid      = "EC2"
                    },
                  - {
                      - Action   = [
                          - "ecr:List*",
                          - "ecr:Get*",
                          - "ecr:Describe*",
                        ]
                      - Effect   = "Allow"
                      - Resource = "*"
                      - Sid      = "ECR"
                    },
                ]
              - Version   = "2012-10-17"
            }
        ) -> (known after apply)
        tags        = {
            "Name" = "queue_housekeeper"
        }
        # (5 unchanged attributes hidden)
    }

  # module.lambdas["queue_housekeeper"].aws_iam_role.lambda_role will be updated in-place
  ~ resource "aws_iam_role" "lambda_role" {
      ~ assume_role_policy    = jsonencode(
            {
              - Statement = [
                  - {
                      - Action    = "sts:AssumeRole"
                      - Effect    = "Allow"
                      - Principal = {
                          - Service = "lambda.amazonaws.com"
                        }
                      - Sid       = "LambdaAssumeRole"
                    },
                ]
              - Version   = "2012-10-17"
            }
        ) -> (known after apply)
        id                    = "lambda_role_for_queue_housekeeper"
        name                  = "lambda_role_for_queue_housekeeper"
        tags                  = {
            "Name" = "queue_housekeeper"
        }
        # (8 unchanged attributes hidden)
    }

This is occurring for every lambda function and on every deployment.

The volume of them is now essentially hiding anything really worth looking at due to the number of lines output.

If we run the plan after making zero changes after a deployment (which includes the same overload of issues), the same output, so something isn’t quite right here.

We have a “Terraform Plan Summary” which is a jq processing of the plan. The output in v1.4 of Terraform was as expected, empty, as nothing had changed. In v1.5 we see:

Plan Summary
============

Resource address                                                                                                                 Read  Deleted  Created  Updated  Trigger
----------------                                                                                                                 ----  -------  -------  -------  -------
module.lambdas["email_notification"].data.aws_iam_policy_document.lambda_assume_role_policy_document                              *                                
module.lambdas["email_notification"].data.aws_iam_policy_document.lambda_permissions_policy_document                              *                                
module.lambdas["email_notification"].aws_iam_policy.lambda_permissions_policy                                                                               *      
module.lambdas["email_notification"].aws_iam_role.lambda_role                                                                                               *      
module.lambdas["qr_code_generator"].data.aws_iam_policy_document.lambda_assume_role_policy_document                               *                                
module.lambdas["qr_code_generator"].data.aws_iam_policy_document.lambda_permissions_policy_document                               *                                
module.lambdas["qr_code_generator"].aws_iam_policy.lambda_permissions_policy                                                                                *      
module.lambdas["qr_code_generator"].aws_iam_role.lambda_role                                                                                                *      
module.lambdas["queue"].data.aws_iam_policy_document.lambda_assume_role_policy_document                                           *                                
module.lambdas["queue"].data.aws_iam_policy_document.lambda_permissions_policy_document                                           *                                
module.lambdas["queue"].aws_iam_policy.lambda_permissions_policy                                                                                            *      
module.lambdas["queue"].aws_iam_role.lambda_role                                                                                                            *      
module.lambdas["queue_admin"].data.aws_iam_policy_document.lambda_assume_role_policy_document                                     *                                
module.lambdas["queue_admin"].data.aws_iam_policy_document.lambda_permissions_policy_document                                     *                                
module.lambdas["queue_admin"].aws_iam_policy.lambda_permissions_policy                                                                                      *      
module.lambdas["queue_admin"].aws_iam_role.lambda_role                                                                                                      *      
module.lambdas["queue_housekeeper"].data.aws_iam_policy_document.lambda_assume_role_policy_document                               *                                
module.lambdas["queue_housekeeper"].data.aws_iam_policy_document.lambda_permissions_policy_document                               *                                
module.lambdas["queue_housekeeper"].aws_iam_policy.lambda_permissions_policy                                                                                *      
module.lambdas["queue_housekeeper"].aws_iam_role.lambda_role                                                                                                *      

Completed

None of these things have changed. Nothing that they are dependent upon (names of resources mainly) have changed.

Nothing is using depends_on.

The plan summary output is there to help guide expectation. Upon reading the actual plan summary, nothing in it is helping.

If there IS a dependency, where in the plan is that information accessible? If we can extract that from the JSON data then we can compartmentalise the Plan Summary in some way (things we expect to change and things we know about that have not really changed unless they are actually present in the first set). By having a known “trigger” (as we call it) for the changes we currently see, then we can add additional processing to help render the list in a more meaningful way.

Some of the policies are hard-coded.

As this is only present in TF 1.6 and not dependent upon the AWS provider (a version v4 or a v5) … we are very stuck on what to do.

The “noise” being generated repeatedly is not useful.

So, as an exercise, I changed the aws_iam_role setup from:

data "aws_iam_policy_document" "lambda_assume_role_policy_document" {
  statement {
    sid     = "LambdaAssumeRole"
    effect  = "Allow"
    actions = ["sts:AssumeRole"]
    principals {
      identifiers = ["lambda.amazonaws.com"]
      type        = "Service"
    }
  }
}

resource "aws_iam_role" "lambda_role" {
  name               = format("lambda_role_for_%s", var.name)
  assume_role_policy = data.aws_iam_policy_document.lambda_assume_role_policy_document.json
  tags = {
    Name = var.name
  }
}

to

resource "aws_iam_role" "lambda_role" {
  name = format("lambda_role_for_%s", var.name)
  assume_role_policy = jsonencode({
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "lambda.amazonaws.com"
        }
        Sid = "LambdaAssumeRole"
      },
    ]
    Version = "2012-10-17"
  })
  tags = {
    Name = var.name
  }
}

and that solved the problem. The downside is that any issue with the the content of the JSON (a typo in a property name for example) is not going to be known until apply and, hopefully, AWS reject the policy. But this just feels wrong and would seem the whole point of a data "aws_iam_policy_document" "lambda_assume_role_policy_document".

Any insight would be REALLY good!!!

EDIT: I have looked into the JSON and found "action_reason": "read_because_dependency_pending". Added this to the Plan Summary and now get:

Plan Summary
============

Resource address                                                                                                                 Read  Deleted  Created  Updated  Trigger  Reason
----------------                                                                                                                 ----  -------  -------  -------  -------  ------
module.lambdas["email_notification"].data.aws_iam_policy_document.lambda_assume_role_policy_document                              *                                        read_because_dependency_pending
module.lambdas["email_notification"].data.aws_iam_policy_document.lambda_permissions_policy_document                              *                                        read_because_dependency_pending
module.lambdas["email_notification"].aws_iam_policy.lambda_permissions_policy                                                                               *               
module.lambdas["email_notification"].aws_iam_role.lambda_role                                                                                               *               
module.lambdas["qr_code_generator"].data.aws_iam_policy_document.lambda_assume_role_policy_document                               *                                        read_because_dependency_pending
module.lambdas["qr_code_generator"].data.aws_iam_policy_document.lambda_permissions_policy_document                               *                                        read_because_dependency_pending
module.lambdas["qr_code_generator"].aws_iam_policy.lambda_permissions_policy                                                                                *               
module.lambdas["qr_code_generator"].aws_iam_role.lambda_role                                                                                                *               
module.lambdas["queue"].data.aws_iam_policy_document.lambda_assume_role_policy_document                                           *                                        read_because_dependency_pending
module.lambdas["queue"].data.aws_iam_policy_document.lambda_permissions_policy_document                                           *                                        read_because_dependency_pending
module.lambdas["queue"].aws_iam_policy.lambda_permissions_policy                                                                                            *               
module.lambdas["queue"].aws_iam_role.lambda_role                                                                                                            *               
module.lambdas["queue_admin"].data.aws_iam_policy_document.lambda_assume_role_policy_document                                     *                                        read_because_dependency_pending
module.lambdas["queue_admin"].data.aws_iam_policy_document.lambda_permissions_policy_document                                     *                                        read_because_dependency_pending
module.lambdas["queue_admin"].aws_iam_policy.lambda_permissions_policy                                                                                      *               
module.lambdas["queue_admin"].aws_iam_role.lambda_role                                                                                                      *               
module.lambdas["queue_housekeeper"].data.aws_iam_policy_document.lambda_assume_role_policy_document                               *                                        read_because_dependency_pending
module.lambdas["queue_housekeeper"].data.aws_iam_policy_document.lambda_permissions_policy_document                               *                                        read_because_dependency_pending
module.lambdas["queue_housekeeper"].aws_iam_policy.lambda_permissions_policy                                                                                *               
module.lambdas["queue_housekeeper"].aws_iam_role.lambda_role                                                                                                *               

Completed

The generator of this is:

  TF_PLAN_JSON=test-reports/tf-plan.json
  terraform show -json terraform.tfplan > $TF_PLAN_JSON
  jq -r '
    # Create an array of object from the following logic
    [
      # Delete any resource_change object that have a change action of "no-op".
      del(.resource_changes[]?|select(.change.actions[0]|startswith("no-op")))
      |
      # Extract just the resource_change that remain
      .resource_changes[]?
      |
      # Extract the required elements from each resource_change into a simpler object
      {
        "Resource address":.address,
        "Read":(if .change.actions | contains(["read"]) then " *" else " " end),
        # As a resource can be created and deleted, determine the position of the delete
        "Deleted":(if .change.actions | contains(["delete"]) then
            if .change.actions | length == 1 then
                "   *"
            elif .change.actions[0] == "delete" then
                "  1st"
            else
                "  2nd"
            end
        else
            " "
        end),
        # As a resource can be created and deleted, determine the position of the create
        "Created":(if .change.actions | contains(["create"]) then
            if .change.actions | length == 1 then
                "   *"
            elif .change.actions[0] == "create" then
                "  1st"
            else
                "  2nd"
            end
        else
            " "
        end),
        "Updated":(if .change.actions | contains(["update"]) then "   *" else " " end),
        "Trigger":(if .change.replace_paths?[0][0] then .change.replace_paths[0][0] else " " end),
        "Reason":(if .action_reason then .action_reason else " " end)
      }
    ]
    |
    if (.[0] | length) == 0 then
      "No changes"
    else
      # Generate the column headings for the resultant table.
      (
        # Use the first object
        .[0]
        |
        # Get the keys in their defined order
        keys_unsorted
        |
        # For each key, create an underline and have these underlines in a new array (so, keys, then underlines).
        (
          .,
          map(length*"-")
        )
      ),
      # Join on the objects to the set of column headings.
      .[]
      |
      # Get just the values.
      map(.)
      |
      # Output everything using tab separation which is the picked up by the column command that follows to make a nice
      # tabular output.
      @tsv
    end
  ' $TF_PLAN_JSON | column -ts $'\t'

Hi @rquadling,

Are you certain that the module "lambdas" block does not use depends_on? If the data source does not refer to a managed resource, and the module does not use depends_on I’m not sure how you are seeing that message

There is a depends_on related to the load balancer.

# Grant access to the lambda function from the load balance.
resource "aws_lambda_permission" "lambda" {
  for_each      = var.lb_collection
  statement_id  = format("AllowExecutionFromLb-%s", each.key)
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.lambda.arn
  qualifier     = aws_lambda_alias.lambda_alias.name
  principal     = "elasticloadbalancing.amazonaws.com"
  source_arn    = aws_lb_target_group.lambda[each.key].arn
}

# Attach the lambda function to the target group.
resource "aws_lb_target_group_attachment" "lambda" {
  depends_on       = [aws_lambda_permission.lambda]
  for_each         = var.lb_collection
  target_group_arn = aws_lb_target_group.lambda[each.key].arn
  target_id        = aws_lambda_alias.lambda_alias.arn
}

but this is not IAM policy related.

But … the call to the module for the lambdas:

module "lambdas" {
  depends_on                          = [module.ec2_asg["wildcards"]]
  source                              = "./modules/lambda_resources"
  for_each                            = local.lambda_configurations
  app_ids                             = local.app_ids
  description                         = each.value.description
  ecr_image_uri                       = data.terraform_remote_state.devops.outputs.shared_hello_world_container_url
  events                              = each.value.events
  lb_collection                       = each.value.lb_collection
  lb_listener_paths                   = each.value.lb_listener_paths
  lb_listener_rule_priority           = each.value.lb_listener_rule_priority
  name                                = each.key
  required_parameter_store_namespaces = each.value.required_parameter_store_namespaces
  required_s3_buckets_paths_access    = each.value.required_s3_buckets_paths_access
  subnet_ids                          = [for az_info in module.vpc.aws_az_subnets : az_info.private_subnet_id]
  security_group_ids                  = [aws_security_group.lambda.id]
  timeout                             = 10
  vpc_id                              = module.vpc.vpc_id
  env_vars                            = each.value.env_vars
}

Now, upon seeing that … I’m not exactly sure what/why there is a depends there.

So, as the ALB needs to exist for the lambda to be attached. But that does seem to be Terraform’s role in sorting things like that out. Only references to existing resources are used.

And lo and bloody behold …

ARGH!!!

Removed the depends_on and it is magically all working as if there had never been anything wrong.

I’ll very very quietly rewrite the master branch history and hide my mistake.

THANK YOU so much for tolerating this question!

Sigh!!!

(P.S. end of a LONG day, so fix this tomorrow).