Data lookup being considered a variable

For some reason a data lookup is being considered a variable and I get an error message that “variables may not be used here”

variables.tf

variable "permission_sets" {
  description = "Map containing Permission Set names as keys."
    default = ""
}

main.tf

module "sso" {
  source = "../../../modules/permission-sets"
  permission_sets = var.permission_sets
}

data.tf

data "aws_iam_policy_document" "default_nonprod_inline_policy" {
  statement { #do not modify this statement
    sid = "inlineadditional"
    ###delete permissions below allow roleback for CDK deployments###
    actions = [
        "iam:AttachRolePolicy",
        "iam:DetachRolePolicy",
        "iam:CreatePolicy",
        "iam:CreateRole",
        "iam:DeleteRole",
        "iam:CreatePolicyVersion",
        "iam:DeletePolicyVersion",
        "iam:CreateRolePolicy",
        "iam:DeleteRolePolicy",
        "iam:PutRolePolicy",
        "iam:TagRole",
        "iam:TagPolicy",
        "iam:PassRole",
    ]
    resources = [
        "*"
    ]
  }
  statement {
    sid = "denyIdentityCenter"
    effect = "Deny"
    actions = [ "sso:*", "sso-directory:*" ]
    resources = ["*"]
  }
}

test.auto.tfvars

permission_sets = {
    architecture-nonprod = {
        description      = "Provides read and write access to nonprod accounts for the domain.",
        session_duration = "PT12H",
        managed_policies = ["arn:aws:iam::aws:policy/PowerUserAccess", "arn:aws:iam::aws:policy/ReadOnlyAccess"]
        inline_policy    = data.aws_iam_policy_document.default_nonprod_inline_policy.json
        principal_name   = "App-AWS_SSO-SolutionArchitecture-nonprod"
        principal_type   = "GROUP"
        account_names    = ["removed"]
    },
}

error

I currently have the variable set as above but i get the same when I have each strictly defined as below:

variable "permission_sets" {
  description = "Map containing Permission Set names as keys."
    type = map(object({
      description      = string
      session_duration = string
      managed_policies = list(string)
      inline_policy    = any 
      principal_name   = string
      principal_type   = string
      account_names    = list(string)
  }))
}

I am not trying to pass a variable but this is the message I get. I have tried putting the data source in “${data_source}” but that didn’t work either.
I have not format errors in my linting. I have browsed the forum but others have had similar issues when actually passing a variable into their variable. I am not trying to do this (that I am aware of). I am probably overlooking something simple but can’t see it at the moment. Would appreciate any help.

Hi @kevin.held_hashicorp,

This is a more general sense of the word “variable” than when used to describe the language feature “input variables”, which is what you’d be declaring with a variable block.

In this case the message is that this value must be entirely constant, without referring to any other objects at all. That’s correct for a .tfvars file because those are for passing data into the root module from outside, and so there aren’t any other objects in scope there to refer to: the data block you referred to here is presumably inside the root module and so you can refer to it only from the root module.

Thank you. I think I understand. So is there a recommended way to do what I am trying to do. Do I have to drop the tfvars file? Can I add a data lookup to a tfvars?

You would need to do that data lookup in your actual code (ie a .tf file).

Hi @kevin.held_hashicorp,

I think I understand your requirement as allowing each “permission set” to select from one of the predefined policies.

If that’s true then one way to do that is to gather all of the predefined policies into a local value map, like this:

locals {
  predefined_policies = tomap({
    nonprod_default = data.aws_iam_policy_document.default_nonprod_inline_policy.json
  })
}

Then in your input variable you’d specify a key from that map to choose which one to use:

variable "permission_sets" {
  description = "Map containing Permission Set names as keys."
  type = map(object({
    description        = string
    session_duration   = string
    managed_policies   = list(string)
    inline_policy_name = string
    principal_name     = string
    principal_type     = string
    account_names      = list(string)
  }))
}

In your .tfvars file you can specify the key from the map:

permission_sets = {
    architecture-nonprod = {
        description      = "Provides read and write access to nonprod accounts for the domain.",
        session_duration = "PT12H",
        managed_policies = ["arn:aws:iam::aws:policy/PowerUserAccess", "arn:aws:iam::aws:policy/ReadOnlyAccess"]
        inline_policy    = "nonprod_default"
        principal_name   = "App-AWS_SSO-SolutionArchitecture-nonprod"
        principal_type   = "GROUP"
        account_names    = ["removed"]
    }
}

Then finally where you need to use the policy that was selected, you can look up the entry in local.predefined_policies using the value of the inline_policy attribute inside each var.permission_sets element.

Thank you. This does solve for my basic scenario. It still leaves me trying to figure out the complex scenario. I have about 30 data lookups being dynamically generated from a for_each:

data "aws_iam_policy_document" "default_prod_inline_policy" {
  for_each = {for k, v in var.permission_sets : k => v if length(regexall("\\w*-prod$", k)) > 0}
  #poweruser statements
  statement { #do not modify this statement except the condition
    sid = "poweruseraccess1"
    not_actions = [
        "iam:*",
        "organizations:*",
        "account:*"
    ]
    resources = [
        "*"
    ]
    condition {
        test = "StringEquals"
        variable = "aws:PrincipalAccount"
        values = [for account in local.all_accounts_map : account.id if contains(regex("gl-\\w*-*\\w*-dev", each.key.account_names), account.name)]
    }
  }
  statement { #do not modify this statement except the condition
    sid = "poweruseraccess2"
    actions = [
        "iam:CreateServiceLinkedRole",
        "iam:DeleteServiceLinkedRole",
        "iam:ListRoles",
        "organizations:DescribeOrganization",
        "account:ListRegions"
    ]
    resources = [
        "*"
    ]
    condition {
        test = "StringEquals"
        variable = "aws:PrincipalAccount"
        values = [for account in local.all_accounts_map : account.id if contains(regex("gl-\\w*-*\\w*-dev", each.key.account_names), account.name)]
    }
  }
  #additional statements below  
  statement {
    sid = "inlineadditional"
    ###delete permissions below allow roleback for CDK deployments###
    actions = [
        "iam:AttachRolePolicy",
        "iam:DetachRolePolicy",
        "iam:CreatePolicy",
        "iam:CreateRole",
        "iam:DeleteRole",
        "iam:CreatePolicyVersion",
        "iam:DeletePolicyVersion",
        "iam:CreateRolePolicy",
        "iam:DeleteRolePolicy",
        "iam:PutRolePolicy",
        "iam:TagRole",
        "iam:TagPolicy",
        "iam:PassRole",
    ]
    resources = [
        "*"
    ]
    condition {
        test = "StringEquals"
        variable = "aws:PrincipalAccount"
        values = [for account in local.all_accounts_map : account.id if contains(regex("gl-\\w*-*\\w*-dev", each.key.account_names), account.name)]
    }
  }
  statement {
    sid = "denyIdentityCenter"
    effect = "Deny"
    actions = [ "sso:*", "sso-directory:*" ]
    resources = ["*"]
  }
}

The condition statement above is unique based on the account names that I pass from the original variable.
Your solution works but I believe I would have to list out every iteration like this

locals {
  predefined_policies = tomap({
    nonprod_default = data.aws_iam_policy_document.default_nonprod_inline_policy.json
    app1_prod = data.aws_iam_policy_document.default_prod_inline_policy["app1"].json
    app2_prod = data.aws_iam_policy_document.default_prod_inline_policy["app2"].json
    appN_prod = data.aws_iam_policy_document.default_prod_inline_policy["appN"].json
  })
}

This solution may work but isn’t as dynamic as i could hope. I may be able to separate out the prod and noprod accounts. That may provide a solution as well.

Was also looking for a way to trim the " from the beginning of a string or not. For example I pass in inline_policy = “data.lookup” in the input variable and then when I call it in the resource use trim(var.inline_policy, “\”")
But this didn’t seem to work either. Trying to be creative.

It looks like the relationship between the instance keys of data.aws_iam_policy_document.default_prod_inline_policy canmap systematically to the keys inside predefined_policies, so you can potentially explain to Terraform how to calculate that set of keys, rather than hand-writing it.

For example:

locals {
  predefined_policies = tomap(merge(
    {
      nonprod_default = data.aws_iam_policy_document.default_nonprod_inline_policy.json
    },
    {
      for k, p in data.aws_iam_policy_document.default_prod_inline_policy :
      "${k}_prod" => p.json
    },
  }))
}

Here I used the merge function to combine a hand-written entry with a bunch of systematically-generated entries. The rule of adding the _prod suffix to the keys ensures that the dynamically-generated ones can never collide with the hand-written one.

You are AMAZING! Yes this worked. I was able to get it to dynamically assign and read those data sources. WOW! I really appreciate your help on this.
Can’t thank you enough!
Kevin