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
8.06k stars 979 forks source link

Remote state (local) path not applying to dependant groups #2179

Open aamm19 opened 2 years ago

aamm19 commented 2 years ago

Hi,

I'm currently having an issue in a setup I'm using for a project with terragrunt.

I have a set of modules that I'm creating (sns, networking, secret manager and security groups). However, security groups is dependant on the networking module.

The command I'm using to run terragrunt currently is: terragrunt run-all apply --terragrunt-include-dir ./networking --terragrunt-include-dir ./secret_manager --terragrunt-include-dir ./sns --terragrunt-include-dir ./security_groups

And the groups are to be executed as follows:

Group 1
- Module [path]/networking
- Module [path]/secret_manager
- Module [path]/sns

Group 2
- Module [path]/security_groups

in my root terragrunt.hcl I have declared the remote_state with the backend="local" option like so:

remote_state {
  backend = "local"
  config = {
    path = "${path_relative_to_include()}/output/terraform.tfstate"
  }
}

And I have imported it on all the modules themselves:

include "root" {
  path   = find_in_parent_folders()
  expose = true
}

#Tested remote_state locally as well on security_group module but no success
#remote_state {
#  backend = "local"
#  config = {
# 
#    path= "./output/terraform.tfstate"
#  }
#}

All of the base modules (sns, networking and secret manager) work as expected, showing the folder structure I paste on the below tree. When the execution for the security group module happens, it saves the statefile in the same level as the terragrunt.hcl file.

Is anyone else facing this issue?

Directory tree:

terragrunt
  |-terragrunt.hcl
  |-networking
  |  |-main.tf
  |  |-output
  |  |  |-terraform.tfstate
  |  |-outputs.tf
  |  |-terraform.tfvars
  |  |-terragrunt.hcl
  |  |-variables.tf
  |-secret_manager
  |  |-main.tf
  |  |-output
  |  |  |-terraform.tfstate
  |  |-outputs.tf
  |  |-terraform.tfvars
  |  |-terragrunt.hcl
  |  |-variables.tf
  |-security_groups
  |  |-main.tf
  |  |-outputs.tf
  |  |-terraform.tfvars
  |  |-terraform.tfstate <- statefile that is being generated by the security_groups module
  |  |-terragrunt.hcl
  |  |-variables.tf
  |  |-versions.tf
  |-sns
  |  |-main.tf
  |  |-output
  |  |  |-terraform.tfstate
  |  |-terraform.tfvars
  |  |-terragrunt.hcl
  |  |-variables.tf
yorinasub17 commented 2 years ago

Hi, it will be hard to debug this without access to the code. Is there any way you can create a minimum reproducible gist?

Alternatively, you can try running with --terragrunt-log-level debug. Since you are not using the generate attribute of remote_state, you should be able to see the backend configuration calls in the logs tacked on to calls to terragrunt init. That might give you insight into whether that is happening because terragrunt isn't configuring the state correctly.

You can also use render-json to introspect what the combined config is. My suspicion is that you have a bug in the terragrunt.hcl for the security_groups folder that is either overriding the remote_state or is not getting the right parent terragrunt.hcl file.

aamm19 commented 2 years ago

Hi Yano, thanks for the reply.

Here's a basic summary of the files: terragrunt.hcl (parent folder)

#The Terragrunt root file includes the configuration of all modules
# Locals are named constants that are reusable within the configuration.
# Loading the common and env variables

remote_state {
  backend = "local"
  config = {
    path = "${path_relative_to_include()}/output/terraform.tfstate"
  }
}

locals {
  aws_region = "REGION"
  tags = {
    Terraform   = "True"
    Environment = "ENVIRONMENT"
  }
  aws_account_id = get_aws_account_id()
}

terraform {
  #source = "../"
  # Force Terraform to keep trying to acquire a lock for
  # up to 20 minutes if someone else already has the lock
  extra_arguments "retry_lock" {
    commands  = get_terraform_commands_that_need_locking()
    arguments = ["-lock-timeout=20m"]
  }
  # Pass custom var files to Terraform
  extra_arguments "custom_vars" {
    commands = [
      "apply",
      "plan",
      "import",
      "push",
      "refresh"
    ]
 }
}

terragrunt.hcl (networking module)

include "root" {
  path   = find_in_parent_folders()
  expose = true
}

locals {
  aws_region         = include.root.locals.aws_region
  tags               = include.root.locals.tags
  local_tags         = {
    Services = "Networking"
  }
}

inputs = {
  aws_region          = local.aws_region
  tags                = merge(local.tags, local.local_tags)
}

terragrunt.hcl (security_groups module)

include "root" {
  path   = find_in_parent_folders()
  expose = true
}

dependencies {
  paths = ["../networking"]
}

dependency "networking" {
  config_path = "../networking"
  mock_outputs = {
    vpc_id = "vpc-00a11b2c"
  }
  #skip_outputs = true # means use mocks all the time if mock_outputs are set.
  #mock_outputs_allowed_terraform_commands = ["init", "validate", "plan" ] 
  #partial_mock_outputs = true # Set true if would merge the actual outputs with the mocked ones
  #mock_outputs_merge_with_state = true #  force terragrunt to merge mock_outputs with the dependency output that was fetched from state.
  #mock_outputs_merge_strategy_with_state = "shallow"
}

locals {
  aws_region         = include.root.locals.aws_region
  tags               = include.root.locals.tags
  local_tags         = {
    Services = "Security_group"
  }
}

inputs = {
  aws_region             = local.aws_region
  use_name_prefix        = local.use_name_prefix
  revoke_rules_on_delete = local.revoke_rules_on_delete
  tags                   = merge(local.tags, local.local_tags)
  vpc_id                 = dependency.networking.outputs.vpc_id
}

I used the render-json to print out the json, and this is the configuration block for the parent json: "remote_state":{"backend":"local","config":{"path":"./output/terraform.tfstate"},"disable_dependency_optimization":false,"disable_init":false,"generate":null}, networking json: "remote_state":{"backend":"local","config":{"path":"networking/output/terraform.tfstate"},"disable_dependency_optimization":false,"disable_init":false,"generate":null} security groups json: "remote_state":{"backend":"local","config":{"path":"security_groups/output/terraform.tfstate"},"disable_dependency_optimization":false,"disable_init":false,"generate":null}

I would've thought it might have something to do with the path_relative_to_include() and how it is interpreted, but then again it wouldn't show only on the security group module.

yorinasub17 commented 2 years ago

Thanks for sharing the code. I see a few potential issues that may or may not be related:

  1. Since you aren't using the generate attribute, the backend configuration in the TF code would have precedence. So it's possible that you misconfigured the terraform block in the main.tf for the security_groups module, which is overriding the path config.

    • I recommend double checking the terraform block in the modules to make sure the backend block is empty without any path field set.
    • Alternatively, consider switching to using the generate pattern for configuring the backend instead of remote_state. There is no advantage to using remote_state block unless you are using the GCS or S3 backend.
  2. Relative paths can be tricky to understand in terragrunt because the folder where Terragrunt calls Terraform is highly dependent on the terraform source. It's advised to generally prefer to use absolute paths when dealing with file paths in the code. For your case, the following should do the trick:

path = "${get_parent_terragrunt_dir()}/${path_relative_to_include()}/output/terraform.tfstate"
aamm19 commented 2 years ago

Thanks for the tips Yano.

I was able to solve the issue implementing the generate backend pattern similar to the documentation and generating the absolute path, as suggested.

terragrunt.hcl (parent folder)

generate "backend" {
  path = "backend.tf"
  if_exists = "overwrite_terragrunt"
  contents = <<EOF
terraform {
  backend "local" {
    path = "${get_parent_terragrunt_dir()}/${path_relative_to_include()}/output/terraform.tfstate"
  }
}
EOF
}

[omitted rest of file for brevity]

Having said that, during the testing I did find that the dependant module (security_groups) gets re-initialized everytime I run "terragrunt run-all apply", even if it was initialized before.

I'm not sure if during the initialization the remote_state block gets overwritten during execution, but if it does, I'm guessing this might be related to the unexpected behavior.

JBallin commented 1 year ago

remote_state works too

remote_state {
  backend = "local"
  config = {
    path = "${get_parent_terragrunt_dir()}/${path_relative_to_include()}/terraform.tfstate"
  }

  generate = {
    path = "backend.tf"
    if_exists = "overwrite"
  }
}

IMO this (local backends) should be documented.

Note: I linked to this issue in a SO answer.