Repository: gruntwork-io/terragrunt

Issue:
Automatically disable dependency optimization if extra_arguments or hooks for init are configured
Prior to 0.23.35 when I ran a `terragunt init`, the dependencies would also run a `terragunt init` using the same flags (like `-upgrade=true`) as the main command that are defined in an extra_arguments block in my terragunt config file. 

The new dependency optimizations broke that, now the `terragrunt init` run on dependencies has no flags. This a big problem on my CI machine which tries to define a plugin-dir. 

Discussion:
@yorinasub17: Thanks for reporting the bug. Working on a stop gap solution for now, which is to disable the dependency optimization by default and have a feature flag to enable it.@yorinasub17: @unixninja92 https://github.com/gruntwork-io/terragrunt/releases/tag/v0.23.37 now has a feature flag to disable dependency optimization. We ended up flipping to on by default (see https://github.com/gruntwork-io/terragrunt/pull/1317#issuecomment-682041007 for thoughts on why).

You will need to set `disable_dependency_optimization = true` on your `remote_state` block to get the previous behavior.

Will keep this ticket open as the plan is to work on autodetection features to automatically disable dependency optimization if `extra_arguments` or `hooks` are configured for `init`.@yorinasub17: Actually, thinking through this a bit more, I just realized that this may not be as big an issue as originally thought. The way the dependency optimization works is that it creates a temporary folder and generates the `backend.tf` file to have an empty terraform module that only contains the s3 backend configuration. It then runs `terraform init && terraform output` in there to pull down the remote state.

Since the temporary folder is an empty module, there should be no provider configuration or module sources it needs to download/init, which effectively means that the plugin directories should be left untouched (since there is no plugin to download).

Can you see if this is the case in your environment?@unixninja92: Your change in v0.23.38 fixes my use case by disabling plugin downloads on dependencies. It also speeds up terragrunt commands! @github-actions: This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for raising this issue.

Solution patch:
diff --git a/config/config.go b/config/config.go
index 2b8324be..1e23378d 100644
--- a/config/config.go
+++ b/config/config.go
@@ -80,10 +80,11 @@ type terragruntLocal struct {
 
 // Configuration for Terraform remote state as parsed from a terragrunt.hcl config file
 type remoteStateConfigFile struct {
-	Backend     string                     `hcl:"backend,attr"`
-	DisableInit *bool                      `hcl:"disable_init,attr"`
-	Generate    *remoteStateConfigGenerate `hcl:"generate,attr"`
-	Config      cty.Value                  `hcl:"config,attr"`
+	Backend                       string                     `hcl:"backend,attr"`
+	DisableInit                   *bool                      `hcl:"disable_init,attr"`
+	DisableDependencyOptimization *bool                      `hcl:"disable_dependency_optimization,attr"`
+	Generate                      *remoteStateConfigGenerate `hcl:"generate,attr"`
+	Config                        cty.Value                  `hcl:"config,attr"`
 }
 
 func (remoteState *remoteStateConfigFile) String() string {
@@ -111,6 +112,9 @@ func (remoteState *remoteStateConfigFile) toConfig() (*remote.RemoteState, error
 	if remoteState.DisableInit != nil {
 		config.DisableInit = *remoteState.DisableInit
 	}
+	if remoteState.DisableDependencyOptimization != nil {
+		config.DisableDependencyOptimization = *remoteState.DisableDependencyOptimization
+	}
 
 	config.FillDefaults()
 	if err := config.Validate(); err != nil {
diff --git a/config/config_as_cty.go b/config/config_as_cty.go
index 251435a6..3a0ec188 100644
--- a/config/config_as_cty.go
+++ b/config/config_as_cty.go
@@ -50,7 +50,7 @@ func terragruntConfigAsCty(config *TerragruntConfig) (cty.Value, error) {
 	if dependenciesCty != cty.NilVal {
 		output["dependencies"] = dependenciesCty
 	}
-	
+
 	if config.PreventDestroy != nil {
 		output["prevent_destroy"] = goboolToCty(*config.PreventDestroy)
 	}
@@ -135,6 +135,7 @@ func remoteStateAsCty(remoteState *remote.RemoteState) (cty.Value, error) {
 	output := map[string]cty.Value{}
 	output["backend"] = gostringToCty(remoteState.Backend)
 	output["disable_init"] = goboolToCty(remoteState.DisableInit)
+	output["disable_dependency_optimization"] = goboolToCty(remoteState.DisableDependencyOptimization)
 
 	generateCty, err := gostructToCty(remoteState.Generate)
 	if err != nil {
diff --git a/config/dependency.go b/config/dependency.go
index 9172a817..44d91bc2 100644
--- a/config/dependency.go
+++ b/config/dependency.go
@@ -20,6 +20,7 @@ import (
 	"golang.org/x/sync/errgroup"
 
 	"github.com/gruntwork-io/terragrunt/aws_helper"
+	"github.com/gruntwork-io/terragrunt/codegen"
 	"github.com/gruntwork-io/terragrunt/errors"
 	"github.com/gruntwork-io/terragrunt/options"
 	"github.com/gruntwork-io/terragrunt/remote"
@@ -390,7 +391,6 @@ func cloneTerragruntOptionsForDependencyOutput(terragruntOptions *options.Terrag
 // Retrieve the outputs from the terraform state in the target configuration. This attempts to optimize the output
 // retrieval if the following conditions are true:
 // - State backends are managed with a `remote_state` block.
-// - The `remote_state` block is using the generator pattern (the `generate` attribute is set).
 // - The `remote_state` block does not depend on any `dependency` outputs.
 // If these conditions are met, terragrunt can optimize the retrieval to avoid recursively retrieving dependency outputs
 // by directly pulling down the state file. Otherwise, terragrunt will fallback to running `terragrunt output` on the
@@ -412,18 +412,12 @@ func getTerragruntOutputJson(terragruntOptions *options.TerragruntOptions, targe
 		terragruntOptions.Logger.Printf("WARNING: Falling back to terragrunt output.")
 		return runTerragruntOutputJson(targetTGOptions, targetConfig)
 	}
-
-	// Fetch directly. If the target config has an IAM role directive and it was not set on the command line, set it to
-	// the one we retrieved from the config.
-	if remoteStateTGConfig.IamRole != "" && targetTGOptions.IamRole == "" {
-		targetTGOptions.IamRole = remoteStateTGConfig.IamRole
-	}
-	return getTerragruntOutputJsonFromRemoteState(targetTGOptions, targetConfig, remoteStateTGConfig.RemoteState)
+	return getTerragruntOutputJsonFromRemoteState(targetTGOptions, targetConfig, remoteStateTGConfig.RemoteState, remoteStateTGConfig.IamRole)
 }
 
-// canGetRemoteState returns true if the remote state block is not nil and has the generate block configured.
+// canGetRemoteState returns true if the remote state block is not nil and dependency optimization is not disabled
 func canGetRemoteState(remoteState *remote.RemoteState) bool {
-	return remoteState != nil && remoteState.Generate != nil
+	return remoteState != nil && !remoteState.DisableDependencyOptimization
 }
 
 // getTerragruntOutputJsonFromRemoteState will retrieve the outputs directly by using just the remote state block. This
@@ -434,9 +428,14 @@ func canGetRemoteState(remoteState *remote.RemoteState) bool {
 // - Generate the backend.tf file with the backend configuration from the remote_state block
 // - Run terraform init and terraform output
 // - Clean up folder once json file is generated
-// NOTE: targetTGOptions should be in the context of the targetConfig.
-func getTerragruntOutputJsonFromRemoteState(targetTGOptions *options.TerragruntOptions, targetConfig string, remoteState *remote.RemoteState) ([]byte, error) {
-	util.Debugf(targetTGOptions.Logger, "Detected remote state block with generate config. Resolving dependency by pulling remote state.")
+// NOTE: terragruntOptions should be in the context of the targetConfig already.
+func getTerragruntOutputJsonFromRemoteState(
+	terragruntOptions *options.TerragruntOptions,
+	targetConfig string,
+	remoteState *remote.RemoteState,
+	iamRole string,
+) ([]byte, error) {
+	util.Debugf(terragruntOptions.Logger, "Detected remote state block with generate config. Resolving dependency by pulling remote state.")
 
 	// Create working directory where we will run terraform in. Make sure it is cleaned up before the function returns.
 	tempWorkDir, err := ioutil.TempDir("", "")
@@ -445,17 +444,33 @@ func getTerragruntOutputJsonFromRemoteState(targetTGOptions *options.TerragruntO
 	}
 	defer os.RemoveAll(tempWorkDir)
 
+	// Here we clone the terragrunt options again since we need to make further modifications to it to allow fetching
+	// remote state.
 	// Set the terraform working dir to the tempdir, and set stdout writer to ioutil.Discard so that output content is
 	// not logged.
+	targetTGOptions := terragruntOptions.Clone(targetConfig)
 	targetTGOptions.WorkingDir = tempWorkDir
 	targetTGOptions.Writer = ioutil.Discard
 
+	// If the target config has an IAM role directive and it was not set on the command line, set it to
+	// the one we retrieved from the config.
+	if iamRole != "" && targetTGOptions.IamRole == "" {
+		targetTGOptions.IamRole = iamRole
+	}
+
 	// Make sure to assume any roles set by TERRAGRUNT_IAM_ROLE
 	if err := aws_helper.AssumeRoleAndUpdateEnvIfNecessary(targetTGOptions); err != nil {
 		return nil, err
 	}
 
-	// Generate the backend configuration in the working dir
+	// Generate the backend configuration in the working dir. If no generate config is set on the remote state block,
+	// set a temporary generate config so we can generate the backend code.
+	if remoteState.Generate == nil {
+		remoteState.Generate = &remote.RemoteStateGenerate{
+			Path:     "backend.tf",
+			IfExists: codegen.ExistsOverwriteTerragruntStr,
+		}
+	}
 	if err := remoteState.GenerateTerraformCode(targetTGOptions); err != nil {
 		return nil, err
 	}
diff --git a/docs/_docs/04_reference/config-blocks-and-attributes.md b/docs/_docs/04_reference/config-blocks-and-attributes.md
index 83deb64c..18530ac1 100644
--- a/docs/_docs/04_reference/config-blocks-and-attributes.md
+++ b/docs/_docs/04_reference/config-blocks-and-attributes.md
@@ -184,6 +184,9 @@ The `remote_state` block supports the following arguments:
   have support in Terragrunt to be automatically created if the storage does not exist. Currently `s3` and `gcs` are the
   two backends with support for automatic creation. Defaults to `false`.
 
+- `disable_dependency_optimization` (attribute): When `true`, disable optimized dependency fetching for terragrunt
+  modules using this `remote_state` block. See the documentation for [dependency block](#dependency) for more details.
+
 - `generate` (attribute): Configure Terragrunt to automatically generate a `.tf` file that configures the remote state
   backend. This is a map that expects two properties:
     - `path`: The path where the generated file should be written. If a relative path, it'll be relative to the Terragrunt
@@ -451,8 +454,11 @@ However, terragrunt includes an optimization to only fetch the lowest level outp
 example) provided that the following conditions are met in the immediate dependencies:
 
 - The remote state is managed using `remote_state` blocks.
-- The `remote_state` block is in generate mode (the `generate` attribute is set).
+- The dependency optimization feature flag is enabled (`disable_dependency_optimization = false`, which is the default).
 - The `remote_state` block itself does not depend on any `dependency` outputs (`locals` and `include` are ok).
+- You are not relying on `before_hook`, `after_hook`, or `extra_arguments` to the `terraform init` call. NOTE:
+  terragrunt will not automatically detect this and you will need to explicitly opt out of the dependency optimization
+  flag.
 
 If these conditions are met, terragrunt will only parse out the `remote_state` blocks and use that to pull down the
 state for the target module without parsing the `dependency` blocks, avoiding the recursive dependency retrieval.
diff --git a/remote/remote_state.go b/remote/remote_state.go
index 533352db..c0e50d93 100644
--- a/remote/remote_state.go
+++ b/remote/remote_state.go
@@ -10,16 +10,17 @@ import (
 )
 
 // Configuration for Terraform remote state
-// NOTE: If any attributes are added here, be sure to add it to ctyRemoteState in config/config_as_cty.go
+// NOTE: If any attributes are added here, be sure to add it to remoteStateAsCty in config/config_as_cty.go
 type RemoteState struct {
-	Backend     string
-	DisableInit bool
-	Generate    *RemoteStateGenerate
-	Config      map[string]interface{}
+	Backend                       string
+	DisableInit                   bool
+	DisableDependencyOptimization bool
+	Generate                      *RemoteStateGenerate
+	Config                        map[string]interface{}
 }
 
 func (remoteState *RemoteState) String() string {
-	return fmt.Sprintf("RemoteState{Backend = %v, DisableInit = %v, Generate = %v, Config = %v}", remoteState.Backend, remoteState.DisableInit, remoteState.Generate, remoteState.Config)
+	return fmt.Sprintf("RemoteState{Backend = %v, DisableInit = %v, DisableDependencyOptimization = %v, Generate = %v, Config = %v}", remoteState.Backend, remoteState.DisableInit, remoteState.DisableDependencyOptimization, remoteState.Generate, remoteState.Config)
 }
 
 // Code gen configuration for Terraform remote state



Test patch:
diff --git a/config/config_as_cty_test.go b/config/config_as_cty_test.go
index 0029743e..81425fa5 100644
--- a/config/config_as_cty_test.go
+++ b/config/config_as_cty_test.go
@@ -48,8 +48,9 @@ func TestTerragruntConfigAsCtyDrift(t *testing.T) {
 		TerraformVersionConstraint:  "= 0.12.20",
 		TerragruntVersionConstraint: "= 0.23.18",
 		RemoteState: &remote.RemoteState{
-			Backend:     "foo",
-			DisableInit: true,
+			Backend:                       "foo",
+			DisableInit:                   true,
+			DisableDependencyOptimization: true,
 			Config: map[string]interface{}{
 				"bar": "baz",
 			},
@@ -116,8 +117,9 @@ func TestTerragruntConfigAsCtyDrift(t *testing.T) {
 // This test makes sure that all the fields in RemoteState are converted to cty
 func TestRemoteStateAsCtyDrift(t *testing.T) {
 	testConfig := remote.RemoteState{
-		Backend:     "foo",
-		DisableInit: true,
+		Backend:                       "foo",
+		DisableInit:                   true,
+		DisableDependencyOptimization: true,
 		Generate: &remote.RemoteStateGenerate{
 			Path:     "foo",
 			IfExists: "overwrite_terragrunt",
@@ -208,6 +210,8 @@ func remoteStateStructFieldToMapKey(t *testing.T, fieldName string) (string, boo
 		return "backend", true
 	case "DisableInit":
 		return "disable_init", true
+	case "DisableDependencyOptimization":
+		return "disable_dependency_optimization", true
 	case "Generate":
 		return "generate", true
 	case "Config":
diff --git a/test/fixture-get-output/localstate/live/terragrunt.hcl b/test/fixture-get-output/localstate/live/terragrunt.hcl
index c1df67e4..83abffba 100644
--- a/test/fixture-get-output/localstate/live/terragrunt.hcl
+++ b/test/fixture-get-output/localstate/live/terragrunt.hcl
@@ -1,6 +1,6 @@
 remote_state {
   backend = "local"
   config = {
-    path = "${path_relative_to_include()}/terraform.tfstate"
+    path = "${get_terragrunt_dir()}/${path_relative_to_include()}/terraform.tfstate"
   }
 }
diff --git a/test/fixture-get-output/nested-optimization-disable/.gitignore b/test/fixture-get-output/nested-optimization-disable/.gitignore
new file mode 100644
index 00000000..79655ca1
--- /dev/null
+++ b/test/fixture-get-output/nested-optimization-disable/.gitignore
@@ -0,0 +1 @@
+backend.tf
diff --git a/test/fixture-get-output/nested-optimization-disable/deepdep/main.tf b/test/fixture-get-output/nested-optimization-disable/deepdep/main.tf
new file mode 100644
index 00000000..cf92c131
--- /dev/null
+++ b/test/fixture-get-output/nested-optimization-disable/deepdep/main.tf
@@ -0,0 +1,3 @@
+output "output" {
+  value = "The answer is 42"
+}
diff --git a/test/fixture-get-output/nested-optimization-disable/deepdep/terragrunt.hcl b/test/fixture-get-output/nested-optimization-disable/deepdep/terragrunt.hcl
new file mode 100644
index 00000000..97f3ba80
--- /dev/null
+++ b/test/fixture-get-output/nested-optimization-disable/deepdep/terragrunt.hcl
@@ -0,0 +1 @@
+# Intentionally empty.
diff --git a/test/fixture-get-output/nested-optimization-disable/dep/main.tf b/test/fixture-get-output/nested-optimization-disable/dep/main.tf
new file mode 100644
index 00000000..fd008c9a
--- /dev/null
+++ b/test/fixture-get-output/nested-optimization-disable/dep/main.tf
@@ -0,0 +1,4 @@
+variable "input" {}
+output "output" {
+  value = "No, ${var.input}"
+}
diff --git a/test/fixture-get-output/nested-optimization-disable/dep/terragrunt.hcl b/test/fixture-get-output/nested-optimization-disable/dep/terragrunt.hcl
new file mode 100644
index 00000000..ea795078
--- /dev/null
+++ b/test/fixture-get-output/nested-optimization-disable/dep/terragrunt.hcl
@@ -0,0 +1,12 @@
+include {
+  path = find_in_parent_folders()
+}
+
+# Retrieve a dependency. In the test, we will destroy this state and verify we can still get the output.
+dependency "deepdep" {
+  config_path = "../deepdep"
+}
+
+inputs = {
+  input = dependency.deepdep.outputs.output
+}
diff --git a/test/fixture-get-output/nested-optimization-disable/live/main.tf b/test/fixture-get-output/nested-optimization-disable/live/main.tf
new file mode 100644
index 00000000..8932fbbd
--- /dev/null
+++ b/test/fixture-get-output/nested-optimization-disable/live/main.tf
@@ -0,0 +1,4 @@
+variable "input" {}
+output "output" {
+  value = "They said, \"${var.input}\""
+}
diff --git a/test/fixture-get-output/nested-optimization-disable/live/terragrunt.hcl b/test/fixture-get-output/nested-optimization-disable/live/terragrunt.hcl
new file mode 100644
index 00000000..3de76183
--- /dev/null
+++ b/test/fixture-get-output/nested-optimization-disable/live/terragrunt.hcl
@@ -0,0 +1,8 @@
+# Retrieve a dependency. In the test, we will destroy this state and verify we can still get the output.
+dependency "dep" {
+  config_path = "../dep"
+}
+
+inputs = {
+  input = dependency.dep.outputs.output
+}
diff --git a/test/fixture-get-output/nested-optimization-disable/terragrunt.hcl b/test/fixture-get-output/nested-optimization-disable/terragrunt.hcl
new file mode 100644
index 00000000..00a2b117
--- /dev/null
+++ b/test/fixture-get-output/nested-optimization-disable/terragrunt.hcl
@@ -0,0 +1,17 @@
+# Configure Terragrunt to automatically store tfstate files in an S3 bucket
+remote_state {
+  disable_dependency_optimization = true
+  backend = "s3"
+  config = {
+    encrypt = true
+    bucket = "__FILL_IN_BUCKET_NAME__"
+    key = "${path_relative_to_include()}/terraform.tfstate"
+    region = "us-west-2"
+    dynamodb_table = "__FILL_IN_LOCK_TABLE_NAME__"
+    enable_lock_table_ssencryption = true
+  }
+  generate = {
+    path      = "backend.tf"
+    if_exists = "overwrite"
+  }
+}
diff --git a/test/fixture-get-output/nested-optimization-nogen/.gitignore b/test/fixture-get-output/nested-optimization-nogen/.gitignore
new file mode 100644
index 00000000..79655ca1
--- /dev/null
+++ b/test/fixture-get-output/nested-optimization-nogen/.gitignore
@@ -0,0 +1 @@
+backend.tf
diff --git a/test/fixture-get-output/nested-optimization-nogen/deepdep/main.tf b/test/fixture-get-output/nested-optimization-nogen/deepdep/main.tf
new file mode 100644
index 00000000..cf92c131
--- /dev/null
+++ b/test/fixture-get-output/nested-optimization-nogen/deepdep/main.tf
@@ -0,0 +1,3 @@
+output "output" {
+  value = "The answer is 42"
+}
diff --git a/test/fixture-get-output/nested-optimization-nogen/deepdep/terragrunt.hcl b/test/fixture-get-output/nested-optimization-nogen/deepdep/terragrunt.hcl
new file mode 100644
index 00000000..97f3ba80
--- /dev/null
+++ b/test/fixture-get-output/nested-optimization-nogen/deepdep/terragrunt.hcl
@@ -0,0 +1 @@
+# Intentionally empty.
diff --git a/test/fixture-get-output/nested-optimization-nogen/dep/main.tf b/test/fixture-get-output/nested-optimization-nogen/dep/main.tf
new file mode 100644
index 00000000..bc2915c7
--- /dev/null
+++ b/test/fixture-get-output/nested-optimization-nogen/dep/main.tf
@@ -0,0 +1,8 @@
+terraform {
+  backend "s3" {}
+}
+
+variable "input" {}
+output "output" {
+  value = "No, ${var.input}"
+}
diff --git a/test/fixture-get-output/nested-optimization-nogen/dep/terragrunt.hcl b/test/fixture-get-output/nested-optimization-nogen/dep/terragrunt.hcl
new file mode 100644
index 00000000..ea795078
--- /dev/null
+++ b/test/fixture-get-output/nested-optimization-nogen/dep/terragrunt.hcl
@@ -0,0 +1,12 @@
+include {
+  path = find_in_parent_folders()
+}
+
+# Retrieve a dependency. In the test, we will destroy this state and verify we can still get the output.
+dependency "deepdep" {
+  config_path = "../deepdep"
+}
+
+inputs = {
+  input = dependency.deepdep.outputs.output
+}
diff --git a/test/fixture-get-output/nested-optimization-nogen/live/main.tf b/test/fixture-get-output/nested-optimization-nogen/live/main.tf
new file mode 100644
index 00000000..8932fbbd
--- /dev/null
+++ b/test/fixture-get-output/nested-optimization-nogen/live/main.tf
@@ -0,0 +1,4 @@
+variable "input" {}
+output "output" {
+  value = "They said, \"${var.input}\""
+}
diff --git a/test/fixture-get-output/nested-optimization-nogen/live/terragrunt.hcl b/test/fixture-get-output/nested-optimization-nogen/live/terragrunt.hcl
new file mode 100644
index 00000000..3de76183
--- /dev/null
+++ b/test/fixture-get-output/nested-optimization-nogen/live/terragrunt.hcl
@@ -0,0 +1,8 @@
+# Retrieve a dependency. In the test, we will destroy this state and verify we can still get the output.
+dependency "dep" {
+  config_path = "../dep"
+}
+
+inputs = {
+  input = dependency.dep.outputs.output
+}
diff --git a/test/fixture-get-output/nested-optimization-nogen/terragrunt.hcl b/test/fixture-get-output/nested-optimization-nogen/terragrunt.hcl
new file mode 100644
index 00000000..79b2d560
--- /dev/null
+++ b/test/fixture-get-output/nested-optimization-nogen/terragrunt.hcl
@@ -0,0 +1,12 @@
+# Configure Terragrunt to automatically store tfstate files in an S3 bucket
+remote_state {
+  backend = "s3"
+  config = {
+    encrypt = true
+    bucket = "__FILL_IN_BUCKET_NAME__"
+    key = "${path_relative_to_include()}/terraform.tfstate"
+    region = "us-west-2"
+    dynamodb_table = "__FILL_IN_LOCK_TABLE_NAME__"
+    enable_lock_table_ssencryption = true
+  }
+}
diff --git a/test/integration_test.go b/test/integration_test.go
index 323c62a4..2848cbd2 100644
--- a/test/integration_test.go
+++ b/test/integration_test.go
@@ -2037,6 +2037,14 @@ func TestYamlDecodeRegressions(t *testing.T) {
 // If output optimization is working, we should still get the same correct output even though the state of the upmost
 // module has been destroyed.
 func TestDependencyOutputOptimization(t *testing.T) {
+	dependencyOutputOptimizationTest(t, "nested-optimization")
+}
+
+func TestDependencyOutputOptimizationNoGenerate(t *testing.T) {
+	dependencyOutputOptimizationTest(t, "nested-optimization-nogen")
+}
+
+func dependencyOutputOptimizationTest(t *testing.T, moduleName string) {
 	t.Parallel()
 
 	expectedOutput := `They said, "No, The answer is 42"`
@@ -2044,7 +2052,7 @@ func TestDependencyOutputOptimization(t *testing.T) {
 
 	cleanupTerraformFolder(t, TEST_FIXTURE_GET_OUTPUT)
 	tmpEnvPath := copyEnvironment(t, TEST_FIXTURE_GET_OUTPUT)
-	rootPath := filepath.Join(tmpEnvPath, TEST_FIXTURE_GET_OUTPUT, "nested-optimization")
+	rootPath := filepath.Join(tmpEnvPath, TEST_FIXTURE_GET_OUTPUT, moduleName)
 	rootTerragruntConfigPath := filepath.Join(rootPath, config.DefaultTerragruntConfigPath)
 	livePath := filepath.Join(rootPath, "live")
 	deepDepPath := filepath.Join(rootPath, "deepdep")
@@ -2057,6 +2065,10 @@ func TestDependencyOutputOptimization(t *testing.T) {
 
 	runTerragrunt(t, fmt.Sprintf("terragrunt apply-all --terragrunt-non-interactive --terragrunt-working-dir %s", rootPath))
 
+	// We need to bust the output cache that stores the dependency outputs so that the second run pulls the outputs.
+	// This is only a problem during testing, where the process is shared across terragrunt runs.
+	config.ClearOutputCache()
+
 	// verify expected output
 	stdout, _, err := runTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt output -no-color -json --terragrunt-non-interactive --terragrunt-working-dir %s", livePath))
 	require.NoError(t, err)
@@ -2065,7 +2077,8 @@ func TestDependencyOutputOptimization(t *testing.T) {
 	require.NoError(t, json.Unmarshal([]byte(stdout), &outputs))
 	assert.Equal(t, expectedOutput, outputs["output"].Value)
 
-	// Now delete the deepdep state and verify still works
+	// Now delete the deepdep state and verify still works (note we need to bust the cache again)
+	config.ClearOutputCache()
 	require.NoError(t, os.Remove(filepath.Join(deepDepPath, "terraform.tfstate")))
 	reout, _, err := runTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt output -no-color -json --terragrunt-non-interactive --terragrunt-working-dir %s", livePath))
 	require.NoError(t, err)
@@ -2074,6 +2087,47 @@ func TestDependencyOutputOptimization(t *testing.T) {
 	assert.Equal(t, expectedOutput, outputs["output"].Value)
 }
 
+func TestDependencyOutputOptimizationDisableTest(t *testing.T) {
+	t.Parallel()
+
+	expectedOutput := `They said, "No, The answer is 42"`
+	generatedUniqueId := uniqueId()
+
+	cleanupTerraformFolder(t, TEST_FIXTURE_GET_OUTPUT)
+	tmpEnvPath := copyEnvironment(t, TEST_FIXTURE_GET_OUTPUT)
+	rootPath := filepath.Join(tmpEnvPath, TEST_FIXTURE_GET_OUTPUT, "nested-optimization-disable")
+	rootTerragruntConfigPath := filepath.Join(rootPath, config.DefaultTerragruntConfigPath)
+	livePath := filepath.Join(rootPath, "live")
+	deepDepPath := filepath.Join(rootPath, "deepdep")
+
+	s3BucketName := fmt.Sprintf("terragrunt-test-bucket-%s", strings.ToLower(generatedUniqueId))
+	lockTableName := fmt.Sprintf("terragrunt-test-locks-%s", strings.ToLower(generatedUniqueId))
+	defer deleteS3Bucket(t, TERRAFORM_REMOTE_STATE_S3_REGION, s3BucketName)
+	defer cleanupTableForTest(t, lockTableName, TERRAFORM_REMOTE_STATE_S3_REGION)
+	copyTerragruntConfigAndFillPlaceholders(t, rootTerragruntConfigPath, rootTerragruntConfigPath, s3BucketName, lockTableName, TERRAFORM_REMOTE_STATE_S3_REGION)
+
+	runTerragrunt(t, fmt.Sprintf("terragrunt apply-all --terragrunt-non-interactive --terragrunt-working-dir %s", rootPath))
+
+	// We need to bust the output cache that stores the dependency outputs so that the second run pulls the outputs.
+	// This is only a problem during testing, where the process is shared across terragrunt runs.
+	config.ClearOutputCache()
+
+	// verify expected output
+	stdout, _, err := runTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt output -no-color -json --terragrunt-non-interactive --terragrunt-working-dir %s", livePath))
+	require.NoError(t, err)
+
+	outputs := map[string]TerraformOutput{}
+	require.NoError(t, json.Unmarshal([]byte(stdout), &outputs))
+	assert.Equal(t, expectedOutput, outputs["output"].Value)
+
+	// Now delete the deepdep state and verify it no longer works, because it tries to fetch the deepdep dependency
+	config.ClearOutputCache()
+	require.NoError(t, os.Remove(filepath.Join(deepDepPath, "terraform.tfstate")))
+	require.NoError(t, os.RemoveAll(filepath.Join(deepDepPath, ".terraform")))
+	_, _, err = runTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt output -no-color -json --terragrunt-non-interactive --terragrunt-working-dir %s", livePath))
+	require.Error(t, err)
+}
+
 func TestDependencyOutput(t *testing.T) {
 	t.Parallel()
 
@@ -2783,10 +2837,11 @@ func TestReadTerragruntConfigFull(t *testing.T) {
 		t,
 		remoteStateOut,
 		map[string]interface{}{
-			"backend":      "local",
-			"disable_init": false,
-			"generate":     map[string]interface{}{"path": "backend.tf", "if_exists": "overwrite_terragrunt"},
-			"config":       map[string]interface{}{"path": "foo.tfstate"},
+			"backend":                         "local",
+			"disable_init":                    false,
+			"disable_dependency_optimization": false,
+			"generate":                        map[string]interface{}{"path": "backend.tf", "if_exists": "overwrite_terragrunt"},
+			"config":                          map[string]interface{}{"path": "foo.tfstate"},
 		},
 	)
 	terraformOut := map[string]interface{}{}
@@ -3159,6 +3214,8 @@ func runTerragruntCommandWithOutput(t *testing.T, command string) (string, strin
 	if err != nil {
 		return "", "", err
 	}
+	logBufferContentsLineByLine(t, stdout, "stdout")
+	logBufferContentsLineByLine(t, stderr, "stderr")
 	return stdout.String(), stderr.String(), nil
 }