Search code examples
amazon-s3terraformterraform-provider-awsstring-interpolationterragrunt

Terragrunt string interpolation on generate block


Having a hard time trying to make this work.

I'm trying to use the generate block to add a for_each and run the module multiple times depending on how much entries I have in the variable s3, but having lots of trouble making it right.

This are the important parts of the terragrunt.hcl file

locals {
  s3 = {
    "companies-data-service" = {
      enable_cloudfront        = true
      cloudfront_allowed_paths = ["logos/*"]
      cors_rules = {
        "local" = {
          methods = ["POST", "HEAD", "PUT", "GET"]
          origins = ["http://localhost:*", "https://*.test.io"]
        }
      }
    }
  }
}

generate "cdn" {
  path      = "cdn.tf"
  if_exists = "overwrite"
  contents  = <<EOF
module "cdn" {
  source = "git::[email protected]:my-org/terraform-modules.git//s3?ref=v0.0.21"
  for_each = { for $${k}, $${v} in "${local.s3}" : $${k} => $${v} }

  name                     = "${local.s3}" != {} ? $${each.value} : ""
  environment              = "${local.parent.inputs.core_env_name}"
  cors_rules               = try($${each.value.cors_rules}, {})
  enable_cloudfront        = try($${each.value.enable_cloudfront}, false)
  enable_versioning        = try($${each.value.enable_versioning}, false)
  cloudfront_allowed_paths = try($${each.value.cloudfront_allowed_paths}, [])
  acm_certificate_arn      = "${dependency.acm.outputs.acm_certificate_arn}"
  aliases                  = ["$${each.key}-static.test.com"]

  enable_lifecycle_rules = try($${each.value.enable_lifecycle_rules}, false)
  s3_lifecycle_rules     = try($${each.value.lifecycle_rules}, [])
  bucket_policies        = try($${each.value.bucket_policies}, {})
}
EOF
}

Having these errors

local.s3 as object with 1 attribute "companies-data-service" 
ERRO[0010] .
                                           
ERRO[0010] Cannot include the given value in a string template: string required.
 
ERRO[0010] Error: Invalid template interpolation value

Also tried the local.s3 variable like this:

s3 = {
    companies = {
      name = "companies"
      enable_cloudfront        = true
      cloudfront_allowed_paths = ["logos/*"]
      cors_rules = {
        "local" = {
          methods = ["POST", "HEAD", "PUT", "GET"]
          origins = ["http://localhost:*", "https://*.test.io"]
        }
      }
    },
    companies2 = {
      name = "companies-2"
      enable_cloudfront        = true
      cloudfront_allowed_paths = ["logos/*"]
      cors_rules = {
        "local" = {
          methods = ["POST", "HEAD", "PUT", "GET"]
          origins = ["http://localhost:*", "https://*.test.io"]
        }
      }
    }
  }

And block generate like this:

generate "cdn" {
  path      = "cdn.tf"
  if_exists = "overwrite"
  contents  = <<EOF
module "cdn" {
  source = "git::[email protected]:my-org/terraform-modules.git//s3?ref=v0.0.21"
  for_each = "${local.s3}"

  name                     = $${each.value.name}
  environment              = "${local.parent.inputs.core_env_name}"
  cors_rules               = try($${each.value.cors_rules}, {})
  enable_cloudfront        = try($${each.value.enable_cloudfront}, false)
  enable_versioning        = try($${each.value.enable_versioning}, false)
  cloudfront_allowed_paths = try($${each.value.cloudfront_allowed_paths}, [])
  acm_certificate_arn      = "${dependency.acm.outputs.acm_certificate_arn}"
  aliases                  = ["$${each.value.name}-static.test.com"]

  enable_lifecycle_rules = try($${each.value.enable_lifecycle_rules}, false)
  s3_lifecycle_rules     = try($${each.value.lifecycle_rules}, [])
  bucket_policies        = try($${each.value.bucket_policies}, {})
}
EOF
}

To validate my changes I tried to pass the locals inside the generate block like pure terraform code, that way it works.

generate "cdn" {
  path      = "cdn.tf"
  if_exists = "overwrite"
  contents  = <<EOF
locals {
  s3 = {
    companies = {
      name = "companies"
      enable_cloudfront        = true
      cloudfront_allowed_paths = ["logos/*"]
      cors_rules = {
        "local" = {
          methods = ["POST", "HEAD", "PUT", "GET"]
          origins = ["http://localhost:*", "https://*.test.io"]
        }
      }
    },
    companies2 = {
      name = "companies-2"
      enable_cloudfront        = true
      cloudfront_allowed_paths = ["logos/*"]
      cors_rules = {
        "local" = {
          methods = ["POST", "HEAD", "PUT", "GET"]
          origins = ["http://localhost:*", "https://*.test.io"]
        }
      }
    }
  }
}
module "cdn" {
  source = "git::[email protected]:my-org/terraform-modules.git//s3?ref=v0.0.21"
  for_each = local.s3

  name                     = each.value.name
  environment              = "${local.parent.inputs.core_env_name}"
  cors_rules               = try(each.value.cors_rules, {})
  enable_cloudfront        = try(each.value.enable_cloudfront, false)
  enable_versioning        = try(each.value.enable_versioning, false)
  cloudfront_allowed_paths = try(each.value.cloudfront_allowed_paths, [])
  acm_certificate_arn      = "${dependency.acm.outputs.acm_certificate_arn}"
  aliases                  = ["$${each.value.name}-static.test.com"]
  enable_lifecycle_rules = try(each.value.enable_lifecycle_rules, false)
  s3_lifecycle_rules     = try(each.value.lifecycle_rules, [])
  bucket_policies        = try(each.value.bucket_policies, {})
}
EOF
}
```

So the issue is clearly with how terragrunt is passing the variable to the generate block, but I don't what is happening because terragrunt don't output anything I can use to debug that variable.

Any help will be much appreciated

Solution

  • This is a terragrunt file, therefore locals is consumed inside terragrunt, and you cannot pass it to terraform. Terragrunt only generates the tf files and execute the terraform command. you need to pass the locals to terraform and you can use

    inputs = {
        s3 = local.s3
    }
    
    generate "vars" {
        path      = "vars.tf"
        if_exists = "overwrite"
        contents  = <<EOF
    variable "s3" {
      type = object({})
    }
    EOF
    }
    

    and then the cdn block can be written in

    generate "cdn" {
      path      = "cdn.tf"
      if_exists = "overwrite"
      contents  = <<EOF
    module "cdn" {
      source = "git::[email protected]:my-org/terraform-modules.git//s3?ref=v0.0.21"
      for_each = { for k, v in s3 : k => v }
    
      name                     = s3 != {} ? each.value : ""
      environment              = "${local.parent.inputs.core_env_name}"
      cors_rules               = try(each.value.cors_rules, {})
      enable_cloudfront        = try(each.value.enable_cloudfront, false)
      enable_versioning        = try(each.value.enable_versioning, false)
      cloudfront_allowed_paths = try(each.value.cloudfront_allowed_paths, [])
      acm_certificate_arn      = "${dependency.acm.outputs.acm_certificate_arn}"
      aliases                  = ["$${each.key}-static.test.com"]
    
      enable_lifecycle_rules = try(each.value.enable_lifecycle_rules, false)
      s3_lifecycle_rules     = try(each.value.lifecycle_rules, [])
      bucket_policies        = try(each.value.bucket_policies, {})
    }
    EOF
    }
    

    I have replaced "${dependency.acm.outputs.acm_certificate_arn}" and "${local.parent.inputs.core_env_name}" with static values (I didn't have the values) and source = "vancluever/module/null", and it worked.