gruntwork-io / terragrunt

Terragrunt is a flexible orchestration tool that allows Infrastructure as Code written in OpenTofu/Terraform to scale.
https://terragrunt.gruntwork.io/
MIT License
7.94k stars 965 forks source link

Add support for passing in locals to generate provider block #2726

Open jmosswcg opened 12 months ago

jmosswcg commented 12 months ago

Describe the solution you'd like Add support for passing in locals into the generate block. For example

locals {
  account_id                            = get_env("foo")
  api_key                                  = get_env("bar")
  environment_vars                 = read_terragrunt_config(find_in_parent_folders("env.hcl"))
  git_branch                             = local.environment_vars.locals.git_branch
}

generate "provider" {
  path      = "provider.tf"
  if_exists = "overwrite_terragrunt"
  contents  = <<EOF
provider "newrelic" {
  account_id = local.account_id
  api_key = local.api_key
  region = "US"                    # Valid regions are US and EU
}
EOF
}

Describe alternatives you've considered For now, I'm setting environment variables in my wrapper script when running terragrunt in CI.

Additional context Add any other context or screenshots about the feature request here.

evsl commented 12 months ago

You can already do this - just escape from your contents string using HCL's string templating syntax. (i.e. ${<some expr here>}.

Referencing your example:

locals {
  account_id                            = get_env("foo")
  api_key                                  = get_env("bar")
  environment_vars                 = read_terragrunt_config(find_in_parent_folders("env.hcl"))
  git_branch                             = local.environment_vars.locals.git_branch
}

generate "provider" {
  path      = "provider.tf"
  if_exists = "overwrite_terragrunt"
  contents  = <<EOF
provider "newrelic" {
  account_id = ${local.account_id}
  api_key = ${local.api_key}
  region = "US"                    # Valid regions are US and EU
}
EOF
}
jmosswcg commented 12 months ago

You can already do this - just escape from your contents string using HCL's string templating syntax. (i.e. ${<some expr here>}.

Referencing your example:

locals {
  account_id                            = get_env("foo")
  api_key                                  = get_env("bar")
  environment_vars                 = read_terragrunt_config(find_in_parent_folders("env.hcl"))
  git_branch                             = local.environment_vars.locals.git_branch
}

generate "provider" {
  path      = "provider.tf"
  if_exists = "overwrite_terragrunt"
  contents  = <<EOF
provider "newrelic" {
  account_id = ${local.account_id}
  api_key = ${local.api_key}
  region = "US"                    # Valid regions are US and EU
}
EOF
}

Thanks! I'll give this a shot. Didn't see this in any documentation so I'll give this a shot.

kubegurus commented 9 months ago

@evsl what if you want to setup the generate block in a file as follow:

provider.hcl

generate "provider" {
  path      = "aws_provider.tf"
  if_exists = "overwrite"
  contents  = <<EOF
provider "aws" {
  region              = "${local.aws_region}"
  allowed_account_ids = ["${local.account_id}"]
  profile             = "${local.aws_profile}"
  default_tags {
    tags = {
      Env        = "${local.aws_profile}"
      AccountID  = "${local.account_id}"
      Region     = "${local.aws_region}"
    }
  }
}
EOF
}

Calling it in the terragrunt.hcl file

locals {
  gen_provider   = read_terragrunt_config("${get_repo_root()}/providers/provider.hcl")
  account_vars   = read_terragrunt_config(find_in_parent_folders("account.hcl"))
  region_vars    = read_terragrunt_config(find_in_parent_folders("region.hcl"))
  environment_vars = read_terragrunt_config(find_in_parent_folders("env.hcl"))

  account_name = local.account_vars.locals.account_name
  account_id   = local.account_vars.locals.aws_account_id
  aws_profile  = local.account_vars.locals.aws_profile
  aws_region   = local.region_vars.locals.aws_region
  env              = local.environment_vars.locals.environment
}
generate = local.gen_provider.generate

From my testing it failed because he is not aware to the locals. Do you have solution for it?

evsl commented 9 months ago

kubegurus

Based on your code, and assuming your structure looks something like this:

root
| `teragrunt.hcl`
| providers
  | provider.hcl

You're seeing this error since your generate block in provider.hcl is trying to access locals defined in terragrunt.hcl - the only way you could do that is if your provider.hcl looked something like this

locals {
    root = read_terragrunt_config(find_in_parent_folders()).locals
}

generate "provider" {
  path      = "aws_provider.tf"
  if_exists = "overwrite"
  contents  = <<EOF
provider "aws" {
  region              = "${local.root.aws_region}"
  allowed_account_ids = ["${local.root.account_id}"]
  profile             = "${local.root.aws_profile}"
  default_tags {
    tags = {
      Env        = "${local.root.aws_profile}"
      AccountID  = "${local.root.account_id}"
      Region     = "${local.root.aws_region}"
    }
  }
}
EOF
}

However, since you're reading the config from provider.tf, you couldn't do that as terraform would (currently - maybe it'll support lazily evaluating this / stopping when a cycle is detected one day) cause a cycle and endlessly process. There's ways to avoid this and do exactly what you want here, but not without either rethinking your folder/read_terragrunt_config structure so that either the root terragrunt.hcl wouldn't itself need to read the provider.tf file, or reading those variables themselves from the provider.tf file and accessing them within their locals (a possible DRY violation of sorts - depends on how exactly you're using those variables of course).

To alleviate this, you can define terraform variables within your generate block. This way you can provide the values from the root by passing the variables as inputs, which you can do from your root terragrunt.hcl. You'd need to define the variables in your modules if they don't already have them - if it's a hassle to do that you can optionally add any required variables to the generated file.

provider.hcl


generate "provider" {
  path      = "aws_provider.tf"
  if_exists = "overwrite"
  contents  = <<EOF

# example variable in generate block -- I personally would avoid this if the module
# defines or uses these variables or related ones elsewhere, but if they're only used in generated files
# then it's fine

variable "aws_region" {
  type = "string"
}

# ... the other variables

provider "aws" {
  region              = var.aws_region
  allowed_account_ids = [ var.aws_account_id ]
  profile             = "var.aws_profile"
  default_tags {
    tags = {
      Env        = var.aws_profile
      AccountID  = var.account_id
      Region     = var.aws_region
    }
  }
}
EOF
}

terragrunt.hcl

locals {
  gen_provider   = read_terragrunt_config("${get_repo_root()}/providers/provider.hcl")
  account_vars   = read_terragrunt_config(find_in_parent_folders("account.hcl"))
  region_vars    = read_terragrunt_config(find_in_parent_folders("region.hcl"))
  environment_vars = read_terragrunt_config(find_in_parent_folders("env.hcl"))

  account_name = local.account_vars.locals.account_name
  account_id   = local.account_vars.locals.aws_account_id
  aws_profile  = local.account_vars.locals.aws_profile
  aws_region   = local.region_vars.locals.aws_region
  env              = local.environment_vars.locals.environment
}
generate = local.gen_provider.generate
inputs = {
  aws_region = locals.aws_region
  aws_account_id = locals.account_id
  aws_profile = locals.aws_profile
}

Hope that helps!


PSA for future reference: If you're in a scenario where you need to expand something (e.g. a variable used in a function like format()) inside of a generated block, then you can use the double leading character syntax to reference it without causing the generate block itself to try to expand the contents. e.g. generate block contents:

variable "foo" {
  type = "string"
}

resource "some_resource" "example" {
  name = "$${foo}-resource"
}
kubegurus commented 9 months ago

@evsl thanks for the detailed answer! I would setup as you suggested

andre177 commented 1 month ago

@evsl is it possible to use random_password's resource output in inputs block when using "generate" block?