Search code examples
loopsterraformconcatenationterraform-provider-aws

concatenate filepath prefix and file name in terraform code


I'm trying to create policies in aws with terraform.

variable "path" {
    type = "string"
}

variable "policies" {
    type = list(object ({
     name = string
     plcyfilename = string
     asmplcyfilename = string
     desc = string
     ownner = string}))
    default = []
}

resource "aws_iam_policy" "policy" {
  count = length(var.policies)
  name =  lookup(var.policies[count.index], "name")
  policy = file(lookup(var.policies[count.index], concat("var.path","plcyfilename")))
  description = "Policy for ${lookup(var.policies[count.index], "desc")}"
}

and this is how my tfvars looks like:

path = "./../t2/scripts/"

policies =  [

{name = "cwpolicy", plcyfilename = "cw.json" , asmplcyfilename ="csasm.json", desc ="vpcflowlogs", ownner ="vpc"},

]

The error that is thrown while I do this is like this:

Error: Invalid function argument

  on main.tf line 13, in resource "aws_iam_policy" "policy":
  13:   policy = file(lookup(var.policies[count.index], "${concat("${var.path}","plcyfilename")}"))

Invalid value for "seqs" parameter: all arguments must be lists or tuples; got
string.

I'm using terraform 0.12.

It works as expected if I change the variable to have complete file path:plcyfilename=./../t2/scripts/cw.json.

However I want to isolate the file path from the file names.

Can someone point me where I am going wrong.


Solution

  • The concat function is for concatenating lists, not for concatenating strings.

    To concatenate strings in Terraform, we use template interpolation syntax:

      policy = file("${var.path}/${var.policies[count.index].policy_filename}")
    

    Since your collection of policies is not a sequence where the ordering is significant, I'd recommend also changing this to use resource for_each, which will ensure that Terraform tracks the policies using the policy name strings rather than using the positions in the list:

    variable "policies" {
      type = map(object({
        policy_filename        = string
        assume_policy_filename = string
        description            = string
        owner                  = string
      }))
      default = {}
    }
    
    resource "aws_iam_policy" "policy" {
      for_each = var.policies
    
      name        = each.key
      policy      = file("${var.path}/${each.value.policy_filename}")
      description = "Policy for ${each.value.description}"
    }
    

    In this case the policies variable is redefined as being a map, so you'd now present the name of each policy as the key within the map rather than as one of the attributes:

      policies = {
        cw = {
          policy_filename        = "cw.json"
          assume_policy_filename = "csasm.json"
          description            = "vpcflowlogs"
          owner                  = "vpc"
        }
        # ...
      }
    

    Because the for_each value is the policies map, each.key inside the resource block is a policy name and each.value is the object representing that policy, making the resulting expressions easier to read and understand.

    By using for_each, we will cause Terraform to create resource instance addresses like aws_iam_policy.policy["cw"] rather than like aws_iam_policy.policy[1], and so adding and removing elements from the map will cause Terraform to add and remove corresponding instances from the resource, rather than try to update instances in-place to respect the list ordering as it would've done with your example.