Repository: gruntwork-io/terragrunt

Issue:
Compatibility with Terraform 1.10
Hi,

## Describe the enhancement

Terraform 1.10 was released two hours ago and Terragrunt is not yet compatible with this version !! 

Two hours !! What are you doing, guys ?! 🤣 

Thanks, ;)

David 

Hints from issue comments:
@denis256: Hi,
the new Terraform 1.10 introduces changes in input/plan handling and S3 configuration behavior, which have caused multiple Terragrunt tests to fail.

If you'd like to contribute fixes or insights, we’d greatly appreciate your support. Contributions from the community play a significant role in making updates faster and smoother.

Example failing tests:
TestAutoRetryGetDefaultErrors
TestTerragruntWorksWithIncludeDeepMerge
TestTerragruntWorksWithMultipleInclude
TestSopsDecryptedCorrectly
TestSopsDecryptedCorrectlyRunAll
TestInputsPassedThroughCorrectly
TestDependencyMockOutputMergeStrategyWithStateCompatTrue
TestDependencyMockOutputMergeStrategyWithStateCompatConflict
TestDependencyMockOutputMergeStrategyWithStateShallow
TestDependencyMockOutputMergeStrategyWithStateDeepMapOnly
TestDependencyOutputTypeConversion
TestReadTerragruntConfigWithDependency
TestStorePlanFilesRunAllPlanApply
TestStorePlanFilesRunAllPlanApplyRelativePath
TestTerragruntRunAllPlanAndShow
TestTerragruntWorksWithMultipleInclude/deep-merge-nonoverlapping
TestTerragruntWorksWithMultipleInclude/deep-merge-overlapping
TestTerragruntWorksWithMultipleInclude/expose
TestTerragruntWorksWithMultipleInclude/has-bare-include
TestTerragruntWorksWithMultipleInclude/json
TestAwsSopsDecryptedKMSCorrectly


Reference PR
https://github.com/gruntwork-io/terragrunt/pull/3605@ricardoleal: For now i'm getting the following

```shell
  ╷
  │ Error: Can't change variable when applying a saved plan
  │ 
  │ The variable asg_capacity cannot be set using the -var and -var-file
  │ options when applying a saved plan file, because a saved plan includes the
  │ variable values that were set when it was created. The saved plan specifies
  │ "{\"desired_capacity\":1,\"max_size\":1,\"min_size\":1}" as the value
  │ whereas during apply the value object with 3 attributes was set by an
  │ environment variable. To declare an ephemeral variable which is not saved
  │ in the plan file, use ephemeral = true.
  ╵
```

```shell
➜  terragrunt --version
terragrunt version v0.69.2
➜  terraform --version 
Terraform v1.10.0
on darwin_arm64
+ provider registry.terraform.io/hashicorp/aws v5.78.0
+ provider registry.terraform.io/hashicorp/cloudinit v2.3.5
```

Update with `terraform` and `terragrunt` versions

Issue on hashicorp for reference : https://github.com/hashicorp/terraform/issues/36106@joshbsemperis: Experiencing the same, seems like quite a serious issue, its a breaking change@ricardoleal: > For now i'm getting the following
> 
> ```shell
>   ╷
>   │ Error: Can't change variable when applying a saved plan
>   │ 
>   │ The variable asg_capacity cannot be set using the -var and -var-file
>   │ options when applying a saved plan file, because a saved plan includes the
>   │ variable values that were set when it was created. The saved plan specifies
>   │ "{\"desired_capacity\":1,\"max_size\":1,\"min_size\":1}" as the value
>   │ whereas during apply the value object with 3 attributes was set by an
>   │ environment variable. To declare an ephemeral variable which is not saved
>   │ in the plan file, use ephemeral = true.
>   ╵
> ```
> 
> ```shell
> ➜  terragrunt --version
> terragrunt version v0.69.2
> ➜  terraform --version 
> Terraform v1.10.0
> on darwin_arm64
> + provider registry.terraform.io/hashicorp/aws v5.78.0
> + provider registry.terraform.io/hashicorp/cloudinit v2.3.5
> ```
> 
> Update with `terraform` and `terragrunt` versions
> 
> Issue on hashicorp for reference : [hashicorp/terraform#36106](https://github.com/hashicorp/terraform/issues/36106)

Issue solved with https://github.com/hashicorp/terraform/releases/tag/v1.10.1

Tested new backend with s3 state lock and working.

```hcl 
remote_state {
  backend = "s3"
  config = {
    bucket         = "xxxxx-bucket"
    key            = format("%s/terraform.tfstate", get_path_from_repo_root())
    region         = "eu-west-3"
    encrypt        = true
    dynamodb_table = "xxxxxx-db-table"
    use_lockfile   = true
  }
}
```

result
```json
aws s3 cp s3://xxxxx-bucket/terraform.tfstate.tflock - | jq .
{
  "ID": "ee0b3c54-de6a-d4ca-d0a6-52f5298ed7c9",
  "Operation": "OperationTypeApply",
  "Info": "",
  "Who": "xxxx@device",
  "Version": "1.10.1",
  "Created": "2024-12-04T16:54:05.570709Z",
  "Path": "xxxxx-bucket/terraform.tfstate"
}
```@liangrog: Terraform has released 1.10.1 for the fix. Could we please bump the version up to the same for Terragrunt docker container? Thanks. @yhakbar: Hey folks,

We're still seeing failures in our CI testing the integration with 1.10.1 related to applying stored plans (the three failing tests are `TestTerragruntRunAllPlanAndShow`, `TestStorePlanFilesRunAllPlanApplyRelativePath` and `TestStorePlanFilesRunAllPlanApply` if interested).

We won't bump the officially supported version until it passes CI.@yhakbar: The recent release of 1.10.2 seems to have a release note related to fixing applying plans, so I'm re-running integration tests on that version.@yhakbar: Still seeing failures in CI:

TestStorePlanFilesRunAllPlanApply:
```
* Failed to execute "terraform apply -auto-approve -input=false /tmp/TestStorePlanFilesRunAllPlanApply3764107339/001/app/tfplan.tfplan" in /tmp/terragrunt-test857549231/fixtures/out-dir/app
Error: Can't change variable when applying a saved plan
The variable input_value cannot be set using the -var and -var-file options
when applying a saved plan file, because a saved plan includes the variable
values that were set when it was created. The saved plan specifies "42" as
the value whereas during apply the value "46521694" was set by an
environment variable. To declare an ephemeral variable which is not saved
in the plan file, use ephemeral = true.
```@alandreasyans: Hi all,
Testing now the new feature of the native state locking with a terragrunt, however, getting issues.
Terraform version -1.10.5
Terragrunt version - v0.73.0-beta2025020601(was same with a 0.72.0 version)

My backend configuration.

remote_state {
  generate = {
    path      = "backend.tf"
    if_exists = "overwrite"
  }
  backend = "s3"
  config = {
    encrypt          = true
    bucket           = "my-bucket-${get_aws_account_id()}"
    key                = local.key
    region            = "eu-west-2"
    use_lockfile   = true
  }
}

Tried to reconfigure the state or migrate state arguments, but getting the same issue.
`error: Backend initialization required, please run "terraform init"
│ 
│ Reason: Initial configuration of the requested backend "s3"
│ 
│ The "backend" is the interface that Terraform uses to store state,
│ perform operations, etc. If this message is showing up, it means that the
│ Terraform configuration you're using is using a custom configuration for
│ the Terraform backend.
│ 
│ Changes to backend configurations require reinitialization. This allows
│ Terraform to set up the new configuration, copy existing state, etc. Please
│ run
│ "terraform init" with either the "-reconfigure" or "-migrate-state" flags
│ to
│ use the current configuration.
│ 
│ If the change reason above is incorrect, please verify your configuration
│ hasn't changed and try again. At this point, no changes to your existing
│ configuration or state have been made.`


@tropnikovvl: Hi, this functionality would be nice@sassdavid: In the meantime Terraform 1.11.0 has been release, we hope it could help to move ahead with the compatibility of the last two versions.@djacquensf9: > In the meantime Terraform 1.11.0 has been release, we hope it could help to move ahead with the compatibility of the last two versions.

Hi everyone, 😉 

Thank you very much everyone for your great feedbacks.

As @sassdavid pointed out, 1.11.0 has been released. 
Could someone create a new issue for 1.11.0 so I can close this issue? 

Thanks 🌻 

David @denis256: Added support for Terraform 1.10 in https://github.com/gruntwork-io/terragrunt/releases/tag/v0.74.0@wanis-fahmy: Hey folks, thank you for the update! does it take time till the release is reflected in brew? https://formulae.brew.sh/api/formula/terragrunt.json still points to the previous release@yhakbar: @wanis-fahmy , yup! Once this is merged, homebrew will reference `v0.74.0`:
https://github.com/Homebrew/homebrew-core/pull/209572

Solution patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 4afc6016..9eeee9eb 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -9,7 +9,7 @@ env: &env
     GRUNTWORK_INSTALLER_VERSION: v0.0.39
     MODULE_CI_VERSION: v0.57.0
     OPENTOFU_VERSION: "1.9.0"
-    TERRAFORM_VERSION: "1.9.7"
+    TERRAFORM_VERSION: "1.10.5"
     TFLINT_VERSION: "0.47.0"
     TOFU_ENGINE_VERSION: "v0.0.11"
 
diff --git a/docs/_docs/04_reference/07-supported-versions.md b/docs/_docs/04_reference/07-supported-versions.md
index 1984401b..c4426ad2 100644
--- a/docs/_docs/04_reference/07-supported-versions.md
+++ b/docs/_docs/04_reference/07-supported-versions.md
@@ -28,6 +28,7 @@ The officially supported versions are:
 
 | Terraform Version | Terragrunt Version                                                                                                                                    |
 |-------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
+| 1.10.x            | >= [0.74.0](https://github.com/gruntwork-io/terragrunt/releases/tag/v0.74.0)                                                                          |
 | 1.9.x             | >= [0.60.0](https://github.com/gruntwork-io/terragrunt/releases/tag/v0.60.0)                                                                          |
 | 1.8.x             | >= [0.57.0](https://github.com/gruntwork-io/terragrunt/releases/tag/v0.57.0)                                                                          |
 | 1.7.x             | >= [0.56.0](https://github.com/gruntwork-io/terragrunt/releases/tag/v0.56.0)                                                                          |



Test patch:
diff --git a/test/fixtures/ephemeral-inputs/main.tf b/test/fixtures/ephemeral-inputs/main.tf
new file mode 100644
index 00000000..a19c7492
--- /dev/null
+++ b/test/fixtures/ephemeral-inputs/main.tf
@@ -0,0 +1,14 @@
+variable "test" {
+  type      = string
+  default   = "test_value"
+  ephemeral = true
+}
+
+output "output" {
+  value = ephemeralasnull(var.test)
+}
+
+resource "local_file" "file" {
+  content  = "test"
+  filename = "${path.module}/test.txt"
+}
diff --git a/test/fixtures/ephemeral-inputs/terragrunt.hcl b/test/fixtures/ephemeral-inputs/terragrunt.hcl
new file mode 100644
index 00000000..4e8ec1a0
--- /dev/null
+++ b/test/fixtures/ephemeral-inputs/terragrunt.hcl
@@ -0,0 +1,4 @@
+
+inputs = {
+  test = "test input 46521694"
+}
diff --git a/test/helpers/package.go b/test/helpers/package.go
index baf08c70..d1778ed4 100644
--- a/test/helpers/package.go
+++ b/test/helpers/package.go
@@ -658,10 +658,35 @@ func ExpectedWrongCommandErr(command string) error {
 	return run.WrongTerraformCommand(command)
 }
 
+// IsTerraform checks if the wrapped binary currently in use is the Terraform binary.
 func IsTerraform() bool {
 	return WrappedBinary() == TerraformBinary
 }
 
+// IsTerraform110OrHigher checks if the installed Terraform binary is version 1.10.0 or higher.
+func IsTerraform110OrHigher() bool {
+	const (
+		requiredMajor = 1
+		requiredMinor = 10
+	)
+
+	if !IsTerraform() {
+		return false
+	}
+
+	output, err := exec.Command(WrappedBinary(), "-version").Output()
+	if err != nil {
+		return false
+	}
+
+	matches := regexp.MustCompile(`Terraform v(\d+)\.(\d+)\.`).FindStringSubmatch(string(output))
+
+	major, _ := strconv.Atoi(matches[1])
+	minor, _ := strconv.Atoi(matches[2])
+
+	return major > requiredMajor || (major == requiredMajor && minor >= requiredMinor)
+}
+
 func FindFilesWithExtension(dir string, ext string) ([]string, error) {
 	var files []string
 
diff --git a/test/integration_destroy_test.go b/test/integration_destroy_test.go
index e2d2d75e..28d87959 100644
--- a/test/integration_destroy_test.go
+++ b/test/integration_destroy_test.go
@@ -313,6 +313,9 @@ func TestStorePlanFilesRunAllDestroy(t *testing.T) {
 	helpers.CleanupTerraformFolder(t, tmpEnvPath)
 	testPath := util.JoinPath(tmpEnvPath, testFixtureOutDir)
 
+	dependencyPath := util.JoinPath(tmpEnvPath, testFixtureOutDir, "dependency")
+	helpers.RunTerragrunt(t, fmt.Sprintf("terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-log-level trace --terragrunt-working-dir %s --terragrunt-out-dir %s", dependencyPath, tmpDir))
+
 	// plan and apply
 	_, _, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt run-all plan --terragrunt-non-interactive --terragrunt-log-level trace --terragrunt-working-dir %s --terragrunt-out-dir %s", testPath, tmpDir))
 	require.NoError(t, err)
diff --git a/test/integration_test.go b/test/integration_test.go
index b5b551ce..532c29f4 100644
--- a/test/integration_test.go
+++ b/test/integration_test.go
@@ -110,6 +110,7 @@ const (
 	textFixtureDisjointSymlinks               = "fixtures/stack/disjoint-symlinks"
 	testFixtureLogStreaming                   = "fixtures/streaming"
 	testFixtureCLIFlagHints                   = "fixtures/cli-flag-hints"
+	testFixtureEphemeralInputs                = "fixtures/ephemeral-inputs"
 
 	terraformFolder = ".terraform"
 
@@ -528,12 +529,12 @@ func TestLogStdoutLevel(t *testing.T) {
 	tmpEnvPath := helpers.CopyEnvironment(t, testFixtureLogStdoutLevel)
 	rootPath := util.JoinPath(tmpEnvPath, testFixtureLogStdoutLevel)
 
-	stdout, _, err := helpers.RunTerragruntCommandWithOutput(t, "terragrunt apply -auto-approve --terragrunt-log-level trace --terragrunt-non-interactive -no-color --terragrunt-no-color --terragrunt-log-format=pretty  --terragrunt-working-dir "+rootPath)
+	stdout, _, err := helpers.RunTerragruntCommandWithOutput(t, "terragrunt apply -auto-approve --terragrunt-non-interactive -no-color --terragrunt-no-color --terragrunt-log-format=pretty  --terragrunt-working-dir "+rootPath)
 	require.NoError(t, err)
 
 	assert.Contains(t, stdout, "STDOUT "+wrappedBinary()+": Changes to Outputs")
 
-	stdout, _, err = helpers.RunTerragruntCommandWithOutput(t, "terragrunt destroy -auto-approve --terragrunt-log-level trace --terragrunt-non-interactive -no-color --terragrunt-no-color --terragrunt-log-format=pretty  --terragrunt-working-dir "+rootPath)
+	stdout, _, err = helpers.RunTerragruntCommandWithOutput(t, "terragrunt destroy -auto-approve --terragrunt-non-interactive -no-color --terragrunt-no-color --terragrunt-log-format=pretty  --terragrunt-working-dir "+rootPath)
 	require.NoError(t, err)
 
 	assert.Contains(t, stdout, "STDOUT "+wrappedBinary()+": Changes to Outputs")
@@ -3763,6 +3764,9 @@ func TestStorePlanFilesRunAllPlanApply(t *testing.T) {
 	tmpEnvPath := helpers.CopyEnvironment(t, testFixtureOutDir)
 	helpers.CleanupTerraformFolder(t, tmpEnvPath)
 	testPath := util.JoinPath(tmpEnvPath, testFixtureOutDir)
+	dependencyPath := util.JoinPath(tmpEnvPath, testFixtureOutDir, "dependency")
+
+	helpers.RunTerragrunt(t, fmt.Sprintf("terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-log-level trace --terragrunt-working-dir %s --terragrunt-out-dir %s", dependencyPath, tmpDir))
 
 	// run plan with output directory
 	_, output, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt run-all plan --terragrunt-non-interactive --terragrunt-log-level trace --terragrunt-working-dir %s --terragrunt-out-dir %s", testPath, tmpDir))
@@ -3789,6 +3793,9 @@ func TestStorePlanFilesRunAllPlanApplyRelativePath(t *testing.T) {
 	helpers.CleanupTerraformFolder(t, tmpEnvPath)
 	testPath := util.JoinPath(tmpEnvPath, testFixtureOutDir)
 
+	dependencyPath := util.JoinPath(tmpEnvPath, testFixtureOutDir, "dependency")
+	helpers.RunTerragrunt(t, fmt.Sprintf("terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-log-level trace --terragrunt-working-dir %s --terragrunt-out-dir %s", dependencyPath, testPath))
+
 	// run plan with output directory
 	_, _, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt run-all plan --terragrunt-non-interactive --terragrunt-log-level trace --terragrunt-working-dir %s --terragrunt-out-dir %s", testPath, "test"))
 	require.NoError(t, err)
@@ -3892,6 +3899,9 @@ func TestPlanJsonPlanBinaryRunAll(t *testing.T) {
 	helpers.CleanupTerraformFolder(t, tmpEnvPath)
 	testPath := util.JoinPath(tmpEnvPath, testFixtureOutDir)
 
+	dependencyPath := util.JoinPath(tmpEnvPath, testFixtureOutDir, "dependency")
+	helpers.RunTerragrunt(t, fmt.Sprintf("terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-log-level trace --terragrunt-working-dir %s --terragrunt-out-dir %s", dependencyPath, tmpDir))
+
 	// run plan with output directory
 	_, _, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt run-all plan --terragrunt-non-interactive --terragrunt-log-level trace --terragrunt-working-dir %s --terragrunt-json-out-dir %s --terragrunt-out-dir %s", testPath, tmpDir, tmpDir))
 	require.NoError(t, err)
@@ -3927,6 +3937,9 @@ func TestTerragruntRunAllPlanAndShow(t *testing.T) {
 	helpers.CleanupTerraformFolder(t, tmpEnvPath)
 	testPath := util.JoinPath(tmpEnvPath, testFixtureOutDir)
 
+	dependencyPath := util.JoinPath(tmpEnvPath, testFixtureOutDir, "dependency")
+	helpers.RunTerragrunt(t, fmt.Sprintf("terragrunt apply -auto-approve --terragrunt-non-interactive --terragrunt-log-level trace --terragrunt-working-dir %s --terragrunt-out-dir %s", dependencyPath, tmpDir))
+
 	// run plan and apply
 	_, _, err := helpers.RunTerragruntCommandWithOutput(t, fmt.Sprintf("terragrunt run-all plan --terragrunt-non-interactive --terragrunt-log-level trace --terragrunt-working-dir %s --terragrunt-out-dir %s", testPath, tmpDir))
 	require.NoError(t, err)
@@ -4152,3 +4165,24 @@ func TestLogFormatBare(t *testing.T) {
 	assert.Contains(t, stdout, "Initializing the backend...")
 	assert.NotContains(t, stdout, "STDO[0000] Initializing the backend...")
 }
+
+func TestTF110EphemeralVars(t *testing.T) {
+	t.Parallel()
+	if !helpers.IsTerraform110OrHigher() {
+		t.Skip("This test requires Terraform 1.10 or higher")
+
+		return
+	}
+
+	tmpEnvPath := helpers.CopyEnvironment(t, testFixtureEphemeralInputs)
+	helpers.CleanupTerraformFolder(t, tmpEnvPath)
+	testPath := util.JoinPath(tmpEnvPath, testFixtureEphemeralInputs)
+
+	stdout, _, err := helpers.RunTerragruntCommandWithOutput(t, "terragrunt plan --terragrunt-non-interactive --terragrunt-working-dir "+testPath)
+	require.NoError(t, err)
+	assert.Contains(t, stdout, "Plan: 1 to add, 0 to change, 0 to destroy")
+
+	stdout, _, err = helpers.RunTerragruntCommandWithOutput(t, "terragrunt apply --auto-approve --terragrunt-non-interactive --terragrunt-working-dir "+testPath)
+	require.NoError(t, err)
+	assert.Contains(t, stdout, "Apply complete! Resources: 1 added, 0 changed, 0 destroyed")
+}