Repository: gruntwork-io/terragrunt

Issue:
0.77 bootstrap warning

### Discussed in https://github.com/gruntwork-io/terragrunt/discussions/4104

<div type='discussions-op-text'>

<sup>Originally posted by **grimm26** March 31, 2025</sup>
How do I  avoid getting this warning without just changing the log level? I'm not trying to bootstrap any new backend.

`WARN[0001] Bootstrapping backend resources by default is deprecated functionality, and will not be the default behavior in a future version of Terragrunt. Use the explicit `--backend-bootstrap` flag to automatically provision backend resources before they're needed.`</div>

Users shouldn't receive a warning here unless Terragrunt is actively bootstrapping backend resources automatically without usage of `--backend-bootstrap`.

Discussion:
@lev-ok: > WARN[0001] Bootstrapping backend resources by default is deprecated functionality, and will not be the default behavior in a future version of Terragrunt. Use the explicit --backend-bootstrap flag to automatically provision backend resources before they're needed.

This message is only showed if remote state needs to be init or reinit


https://github.com/gruntwork-io/terragrunt/blob/2b5bb160944f0975453c9f33a0234a1aed713618/cli/commands/run/run.go#L746 @lorengordon: I actually like and prefer the prior behavior. Is there a config-level option I can use to continue letting terragrunt setup the backend by default?@lev-ok: @lorengordon, yes, you need to specify the `--backend-bootstrap` flag.@lorengordon: That's a cli option, not a config option. Is there a config option to get the prior behavior without needing to remember to set the cli option every time?@yhakbar: Hey @lorengordon ,

We'll be prioritizing https://github.com/gruntwork-io/terragrunt/issues/3572 soon to make it so that users can set any CLI option as part of a configuration file, which can be scoped at a project level or at a user level.

For now, you can set `TG_BACKEND_BOOTSTRAP=true` in your shell to keep it active.@lorengordon: >For now, you can set TG_BACKEND_BOOTSTRAP=true in your shell to keep it active.

Yeah... My shell, and the CI runner shell, and all the other users' shells... :) 
@kiweezi: > This message is only showed if remote state needs to be init or reinit

I'm still getting this issue on `v0.77.7`, and I can't seem to figure out why. @yhakbar 

As far as I can see #4107 was merged and is in [v0.77.4](https://github.com/gruntwork-io/terragrunt/compare/v0.77.3...v0.77.4).

I've run plans with the following cases, where all still show the warning:
- `terragrunt plan` with `-reconfigure` enabled on `extra_arguments`
- `terragrunt plan` with no `-reconfigure`
- `terragrunt plan --no-auto-init`
- `terragrunt plan --no-auto-init` with all parts of my `terragrunt.hcl` commented out. Except from requiring a var file and Azure remote backend config.

See the output below.

```bash
vscode ➜ /workspaces/iac/core/terraform (main) $ terragrunt version
terragrunt version v0.77.7
vscode ➜ /workspaces/iac/core/terraform (main) $ terragrunt plan --no-auto-init
WARN[0000] NeedsBootstrap for azurerm backend not implemented
WARN[0000] Bootstrapping backend resources by default is deprecated functionality, and will not be the default behavior in a future version of Terragrunt. Use the explicit `--backend-bootstrap` flag to automatically provision backend resources before they're needed.
WARN[0000] Detected that init is needed, but Auto-Init is disabled. Continuing with further actions, but subsequent terraform commands may fail.
INFO[0000] Executing hook: target_stack
[Terragrunt][INFO] Targetting STACK: we001 👈
Acquiring state lock. This may take a few moments...
```

The line below sticks out to me. Is this indicating that AzureRM provider has not implemented a piece of functionality that this change relies upon?
> WARN[0000] NeedsBootstrap for azurerm backend not implemented@lev-ok: @kiweezi, This will be fixed in the next release. Thanks!

Solution patch:
diff --git a/cli/commands/backend/bootstrap/bootstrap.go b/cli/commands/backend/bootstrap/bootstrap.go
index 19831364..3c7bac18 100644
--- a/cli/commands/backend/bootstrap/bootstrap.go
+++ b/cli/commands/backend/bootstrap/bootstrap.go
@@ -20,5 +20,5 @@ func Run(ctx context.Context, opts *options.TerragruntOptions) error {
 		return nil
 	}
 
-	return remoteState.Init(ctx, opts)
+	return remoteState.Bootstrap(ctx, opts)
 }
diff --git a/cli/commands/run/run.go b/cli/commands/run/run.go
index 6c82f93f..84ff5f7b 100644
--- a/cli/commands/run/run.go
+++ b/cli/commands/run/run.go
@@ -276,7 +276,7 @@ func GenerateConfig(opts *options.TerragruntOptions, cfg *config.TerragruntConfi
 	}
 
 	if cfg.RemoteState != nil && cfg.RemoteState.Generate != nil {
-		if err := cfg.RemoteState.GenerateTerraformCode(opts); err != nil {
+		if err := cfg.RemoteState.GenerateOpenTofuCode(opts); err != nil {
 			return err
 		}
 	} else if cfg.RemoteState != nil {
@@ -520,7 +520,7 @@ func prepareInitCommand(ctx context.Context, terragruntOptions *options.Terragru
 		}
 
 		if remoteStateNeedsInit {
-			if err := terragruntConfig.RemoteState.Init(ctx, terragruntOptions); err != nil {
+			if err := terragruntConfig.RemoteState.Bootstrap(ctx, terragruntOptions); err != nil {
 				return err
 			}
 		}
@@ -743,7 +743,7 @@ func remoteStateNeedsInit(ctx context.Context, remoteState *remotestate.RemoteSt
 		return false, nil
 	}
 
-	if ok, err := remoteState.NeedsInit(ctx, opts); err != nil || !ok {
+	if ok, err := remoteState.NeedsBootstrap(ctx, opts); err != nil || !ok {
 		return false, err
 	}
 
diff --git a/codegen/generate.go b/codegen/generate.go
index df9d7322..f3d896c5 100644
--- a/codegen/generate.go
+++ b/codegen/generate.go
@@ -96,7 +96,7 @@ type GenerateConfig struct {
 // - if ExistsSkip, do nothing and return
 // - if ExistsOverwrite, overwrite the existing file
 func WriteToFile(terragruntOptions *options.TerragruntOptions, basePath string, config GenerateConfig) error {
-	// Figure out thee target path to generate the code in. If relative, merge with basePath.
+	// Figure out the target path to generate the code in. If relative, merge with basePath.
 	var targetPath string
 	if filepath.IsAbs(config.Path) {
 		targetPath = config.Path
diff --git a/config/config.go b/config/config.go
index 2d26b00d..7e5053da 100644
--- a/config/config.go
+++ b/config/config.go
@@ -51,6 +51,8 @@ const (
 
 	iamRoleCacheName = "iamRoleCache"
 
+	logMsgSeparator = "\n"
+
 	DefaultEngineType                   = "rpc"
 	MetadataTerraform                   = "terraform"
 	MetadataTerraformBinary             = "terraform_binary"
@@ -95,7 +97,11 @@ var (
 	}
 
 	DefaultParserOptions = func(opts *options.TerragruntOptions) []hclparse.Option {
-		writer := writer.New(writer.WithLogger(opts.Logger), writer.WithDefaultLevel(log.ErrorLevel))
+		writer := writer.New(
+			writer.WithLogger(opts.Logger),
+			writer.WithDefaultLevel(log.ErrorLevel),
+			writer.WithMsgSeparator(logMsgSeparator),
+		)
 
 		return []hclparse.Option{
 			hclparse.WithDiagnosticsWriter(writer, opts.Logger.Formatter().DisabledColors()),
diff --git a/config/dependency.go b/config/dependency.go
index 66b177c7..d2f479c2 100644
--- a/config/dependency.go
+++ b/config/dependency.go
@@ -949,7 +949,7 @@ func getTerragruntOutputJSONFromRemoteState(
 		}
 	}
 
-	if err := remoteState.GenerateTerraformCode(targetTGOptions); err != nil {
+	if err := remoteState.GenerateOpenTofuCode(targetTGOptions); err != nil {
 		return nil, err
 	}
 
diff --git a/config/stack.go b/config/stack.go
index 0db867b6..e3a6f246 100644
--- a/config/stack.go
+++ b/config/stack.go
@@ -26,13 +26,13 @@ import (
 )
 
 const (
-	stackDir           = ".terragrunt-stack"
-	valuesFile         = "terragrunt.values.hcl"
-	manifestName       = ".terragrunt-stack-manifest"
-	defaultStackFile   = "terragrunt.stack.hcl"
-	unitDirPerm        = 0755
-	valueFilePerm      = 0644
-	generationMaxDepth = 100
+	stackDir          = ".terragrunt-stack"
+	valuesFile        = "terragrunt.values.hcl"
+	manifestName      = ".terragrunt-stack-manifest"
+	defaultStackFile  = "terragrunt.stack.hcl"
+	unitDirPerm       = 0755
+	valueFilePerm     = 0644
+	generationMaxPath = 1024
 )
 
 // StackConfigFile represents the structure of terragrunt.stack.hcl stack file.
@@ -687,10 +687,8 @@ func listStackFiles(opts *options.TerragruntOptions, dir string) ([]string, erro
 			return nil
 		}
 
-		relPath, _ := filepath.Rel(dir, path)
-		depth := len(strings.Split(relPath, string(os.PathSeparator)))
-		if depth > generationMaxDepth {
-			return errors.Errorf("Cycle detected: max depth of %d exceeded at %s", generationMaxDepth, path)
+		if len(path) >= generationMaxPath {
+			return errors.Errorf("Cycle detected: maximum path length (%d) exceeded at %s", generationMaxPath, path)
 		}
 
 		if strings.HasSuffix(path, defaultStackFile) {



Test patch:
diff --git a/internal/remotestate/backend/backend.go b/internal/remotestate/backend/backend.go
index 26bceaad..ca5f02e9 100644
--- a/internal/remotestate/backend/backend.go
+++ b/internal/remotestate/backend/backend.go
@@ -24,11 +24,11 @@ type Backend interface {
 	// Names returns the backend name.
 	Name() string
 
-	// NeedsInit returns true if remote state needs to be initialized.
-	NeedsInit(ctx context.Context, config Config, existingConfig Config, opts *options.TerragruntOptions) (bool, error)
+	// NeedsBootstrap returns true if remote state needs to be bootstrapped.
+	NeedsBootstrap(ctx context.Context, config Config, opts *options.TerragruntOptions) (bool, error)
 
-	// Init initializes the remote state.
-	Init(ctx context.Context, config Config, opts *options.TerragruntOptions) error
+	// Bootstrap bootstraps the remote state.
+	Bootstrap(ctx context.Context, config Config, opts *options.TerragruntOptions) error
 
 	// Delete deletes the remote state.
 	Delete(ctx context.Context, config Config, opts *options.TerragruntOptions) error
diff --git a/internal/remotestate/backend/common.go b/internal/remotestate/backend/common.go
index 9fe05e16..ff96ef9f 100644
--- a/internal/remotestate/backend/common.go
+++ b/internal/remotestate/backend/common.go
@@ -29,23 +29,23 @@ func (backend *CommonBackend) Name() string {
 	return backend.name
 }
 
-// NeedsInit implements `backends.NeedsInit` interface.
-func (backend *CommonBackend) NeedsInit(ctx context.Context, config Config, existingConfig Config, opts *options.TerragruntOptions) (bool, error) {
-	needsInits := !config.IsEqual(existingConfig, backend.Name(), opts.Logger)
+// NeedsBootstrap implements `backends.NeedsBootstrap` interface.
+func (backend *CommonBackend) NeedsBootstrap(ctx context.Context, config Config, opts *options.TerragruntOptions) (bool, error) {
+	opts.Logger.Warnf("NeedsBootstrap for %s backend not implemented", backend.Name())
 
-	return needsInits, nil
+	return true, nil
 }
 
-// Init implements `backends.Init` interface.
-func (backend *CommonBackend) Init(ctx context.Context, config Config, opts *options.TerragruntOptions) error {
-	opts.Logger.Warnf("Initialization of remote state for %s backend not implemented", backend.Name())
+// Bootstrap implements `backends.Bootstrap` interface.
+func (backend *CommonBackend) Bootstrap(ctx context.Context, config Config, opts *options.TerragruntOptions) error {
+	opts.Logger.Warnf("Bootstrap for %s backend not implemented", backend.Name())
 
 	return nil
 }
 
 // Delete implements `backends.Delete` interface.
 func (backend *CommonBackend) Delete(ctx context.Context, config Config, opts *options.TerragruntOptions) error {
-	opts.Logger.Warnf("Deleting remote state for %s backend not implemented", backend.Name())
+	opts.Logger.Warnf("Delete for %s backend not implemented", backend.Name())
 
 	return nil
 }
diff --git a/internal/remotestate/backend/gcs/backend.go b/internal/remotestate/backend/gcs/backend.go
index 0ff58358..56a102a4 100644
--- a/internal/remotestate/backend/gcs/backend.go
+++ b/internal/remotestate/backend/gcs/backend.go
@@ -25,23 +25,19 @@ func NewBackend() *Backend {
 	}
 }
 
-// NeedsInit returns true if the GCS bucket specified in the given config does not exist or if the bucket
+// NeedsBootstrap returns true if the GCS bucket specified in the given config does not exist or if the bucket
 // exists but versioning is not enabled.
 //
 // Returns true if:
 //
 // 1. Any of the existing backend settings are different than the current config
 // 2. The configured GCS bucket does not exist
-func (backend *Backend) NeedsInit(ctx context.Context, backendConfig backend.Config, backendExistingConfig backend.Config, opts *options.TerragruntOptions) (bool, error) {
+func (backend *Backend) NeedsBootstrap(ctx context.Context, backendConfig backend.Config, opts *options.TerragruntOptions) (bool, error) {
 	extGCSCfg, err := Config(backendConfig).ExtendedGCSConfig()
 	if err != nil {
 		return false, err
 	}
 
-	if Config(backendConfig).IsEqual(Config(backendExistingConfig), opts.Logger) {
-		return true, nil
-	}
-
 	var (
 		gcsCfg     = &extGCSCfg.RemoteStateConfigGCS
 		bucketName = gcsCfg.Bucket
@@ -65,9 +61,9 @@ func (backend *Backend) NeedsInit(ctx context.Context, backendConfig backend.Con
 	return false, nil
 }
 
-// Init the remote state GCS bucket specified in the given config. This function will validate the config
+// Bootstrap the remote state GCS bucket specified in the given config. This function will validate the config
 // parameters, create the GCS bucket if it doesn't already exist, and check that versioning is enabled.
-func (backend *Backend) Init(ctx context.Context, backendConfig backend.Config, opts *options.TerragruntOptions) error {
+func (backend *Backend) Bootstrap(ctx context.Context, backendConfig backend.Config, opts *options.TerragruntOptions) error {
 	extGCSCfg, err := Config(backendConfig).ExtendedGCSConfig()
 	if err != nil {
 		return err
diff --git a/internal/remotestate/backend/s3/backend.go b/internal/remotestate/backend/s3/backend.go
index 5c12a301..2c1146bd 100644
--- a/internal/remotestate/backend/s3/backend.go
+++ b/internal/remotestate/backend/s3/backend.go
@@ -26,19 +26,15 @@ func NewBackend() *Backend {
 	}
 }
 
-// NeedsInit returns true if the remote state S3 bucket specified in the given config needs to be initialized.
+// NeedsBootstrap returns true if the remote state S3 bucket specified in the given config needs to be bootstrapped.
 //
 // Returns true if:
 //
 // 1. Any of the existing backend settings are different than the current config
 // 2. The configured S3 bucket or DynamoDB table does not exist
-func (backend *Backend) NeedsInit(ctx context.Context, backendConfig backend.Config, backendExistingConfig backend.Config, opts *options.TerragruntOptions) (bool, error) {
+func (backend *Backend) NeedsBootstrap(ctx context.Context, backendConfig backend.Config, opts *options.TerragruntOptions) (bool, error) {
 	cfg := Config(backendConfig).Normalize(opts.Logger)
 
-	if !cfg.IsEqual(Config(backendExistingConfig), opts.Logger) {
-		return true, nil
-	}
-
 	extS3Cfg, err := cfg.ExtendedS3Config(opts.Logger)
 	if err != nil {
 		return false, err
@@ -67,9 +63,9 @@ func (backend *Backend) NeedsInit(ctx context.Context, backendConfig backend.Con
 	return false, nil
 }
 
-// Init the remote state S3 bucket specified in the given config. This function will validate the config
+// Bootstrap the remote state S3 bucket specified in the given config. This function will validate the config
 // parameters, create the S3 bucket if it doesn't already exist, and check that versioning is enabled.
-func (backend *Backend) Init(ctx context.Context, backendConfig backend.Config, opts *options.TerragruntOptions) error {
+func (backend *Backend) Bootstrap(ctx context.Context, backendConfig backend.Config, opts *options.TerragruntOptions) error {
 	extS3Cfg, err := Config(backendConfig).ExtendedS3Config(opts.Logger)
 	if err != nil {
 		return err
diff --git a/internal/remotestate/backend/s3/config.go b/internal/remotestate/backend/s3/config.go
index b2966c5b..fa26196b 100644
--- a/internal/remotestate/backend/s3/config.go
+++ b/internal/remotestate/backend/s3/config.go
@@ -3,11 +3,9 @@ package s3
 import (
 	"reflect"
 	"slices"
-	"strconv"
 
 	"github.com/gruntwork-io/terragrunt/internal/errors"
 	"github.com/gruntwork-io/terragrunt/internal/hclhelper"
-	"github.com/gruntwork-io/terragrunt/internal/remotestate/backend"
 	"github.com/gruntwork-io/terragrunt/pkg/log"
 	"github.com/mitchellh/mapstructure"
 
@@ -99,37 +97,6 @@ func (cfg Config) Normalize(logger log.Logger) Config {
 
 	return normalized
 }
-func (cfg Config) IsEqual(targetCfg Config, logger log.Logger) bool {
-	// Terraform's `backend` configuration uses a boolean for the `encrypt` parameter. However, perhaps for backwards compatibility reasons,
-	// Terraform stores that parameter as a string in the `terraform.tfstate` file. Therefore, we have to convert it accordingly, or `DeepEqual`
-	// will fail.
-	if util.KindOf(targetCfg[configEncryptKey]) == reflect.String && util.KindOf(cfg[configEncryptKey]) == reflect.Bool {
-		// If encrypt in remoteState is a bool and a string in existingBackend, DeepEqual will consider the maps to be different.
-		// So we convert the value from string to bool to make them equivalent.
-		if value, err := strconv.ParseBool(targetCfg[configEncryptKey].(string)); err == nil {
-			targetCfg[configEncryptKey] = value
-		} else {
-			logger.Warnf("Remote state configuration encrypt contains invalid value %v, should be boolean.", targetCfg["encrypt"])
-		}
-	}
-
-	// If other keys in config are bools, DeepEqual also will consider the maps to be different.
-	for key, value := range targetCfg {
-		if util.KindOf(targetCfg[key]) == reflect.String && util.KindOf(cfg[key]) == reflect.Bool {
-			if convertedValue, err := strconv.ParseBool(value.(string)); err == nil {
-				targetCfg[key] = convertedValue
-			}
-		}
-	}
-
-	// We now construct a version of the config that matches what we expect in the backend by stripping out terragrunt
-	// related configs.
-	newConfig := backend.Config{}
-
-	maps.Copy(newConfig, cfg.FilterOutTerragruntKeys())
-
-	return newConfig.IsEqual(backend.Config(targetCfg), BackendName, logger)
-}
 
 // ParseExtendedS3Config parses the given map into an extended S3 config.
 func (cfg Config) ParseExtendedS3Config() (*ExtendedRemoteStateConfigS3, error) {
diff --git a/internal/remotestate/backend/s3/config_test.go b/internal/remotestate/backend/s3/config_test.go
deleted file mode 100644
index 709f4bd1..00000000
--- a/internal/remotestate/backend/s3/config_test.go
+++ /dev/null
@@ -1,118 +0,0 @@
-package s3_test
-
-import (
-	"testing"
-
-	"github.com/gruntwork-io/terragrunt/internal/remotestate/backend/s3"
-	"github.com/gruntwork-io/terragrunt/pkg/log"
-	"github.com/stretchr/testify/assert"
-)
-
-func TestConfig_IsEqual(t *testing.T) {
-	t.Parallel()
-
-	logger := log.New()
-
-	testCases := []struct { //nolint: govet
-		name          string
-		cfg           s3.Config
-		comparableCfg s3.Config
-		shouldBeEqual bool
-	}{
-		{
-			"equal-both-empty",
-			s3.Config{},
-			s3.Config{},
-			true,
-		},
-		{
-			"equal-empty-and-nil",
-			s3.Config{},
-			nil,
-			true,
-		},
-		{
-			"equal-one-key",
-			s3.Config{"foo": "bar"},
-			s3.Config{"foo": "bar"},
-			true,
-		},
-		{
-			"equal-multiple-keys",
-			s3.Config{"foo": "bar", "baz": []string{"a", "b", "c"}, "blah": 123, "bool": true},
-			s3.Config{"foo": "bar", "baz": []string{"a", "b", "c"}, "blah": 123, "bool": true},
-			true,
-		},
-		{
-			"equal-encrypt-bool-handling",
-			s3.Config{"encrypt": true},
-			s3.Config{"encrypt": "true"},
-			true,
-		},
-		{
-			"equal-general-bool-handling",
-			s3.Config{"something": true, "encrypt": true},
-			s3.Config{"something": "true", "encrypt": "true"},
-			true,
-		},
-		{
-			"equal-ignore-s3-tags",
-			s3.Config{"foo": "bar", "s3_bucket_tags": []map[string]string{{"foo": "bar"}}},
-			s3.Config{"foo": "bar"},
-			true,
-		},
-		{
-			"equal-ignore-dynamodb-tags",
-			s3.Config{"dynamodb_table_tags": []map[string]string{{"foo": "bar"}}},
-			s3.Config{},
-			true,
-		},
-		{
-			"equal-ignore-accesslogging-options",
-			s3.Config{
-				"accesslogging_bucket_tags":                         []map[string]string{{"foo": "bar"}},
-				"accesslogging_target_object_partition_date_source": "EventTime",
-				"accesslogging_target_prefix":                       "test/",
-				"skip_accesslogging_bucket_acl":                     false,
-				"skip_accesslogging_bucket_enforced_tls":            false,
-				"skip_accesslogging_bucket_public_access_blocking":  false,
-				"skip_accesslogging_bucket_ssencryption":            false,
-			},
-			s3.Config{},
-			true,
-		},
-		{
-			"unequal-values",
-			s3.Config{"foo": "bar"},
-			s3.Config{"foo": "different"},
-			false,
-		},
-		{
-			"unequal-non-empty-config-nil",
-			s3.Config{"foo": "bar"},
-			nil,
-			false,
-		},
-		{
-			"unequal-general-bool-handling",
-			s3.Config{"something": true},
-			s3.Config{"something": "false"},
-			false,
-		},
-		{
-			"equal-null-ignored",
-			s3.Config{"something": "foo"},
-			s3.Config{"something": "foo", "ignored-because-null": nil},
-			true,
-		},
-	}
-
-	for _, tc := range testCases {
-		t.Run(tc.name, func(t *testing.T) {
-			t.Parallel()
-
-			actual := tc.cfg.IsEqual(tc.comparableCfg, logger)
-			assert.Equal(t, tc.shouldBeEqual, actual)
-		})
-	}
-}
diff --git a/internal/remotestate/config.go b/internal/remotestate/config.go
index 2a2d6b06..69f0ca23 100644
--- a/internal/remotestate/config.go
+++ b/internal/remotestate/config.go
@@ -8,7 +8,6 @@ import (
 	"github.com/gruntwork-io/terragrunt/internal/errors"
 	"github.com/gruntwork-io/terragrunt/internal/remotestate/backend"
 	"github.com/gruntwork-io/terragrunt/options"
-	"github.com/gruntwork-io/terragrunt/pkg/log"
 	"github.com/zclconf/go-cty/cty"
 )
 
@@ -55,26 +54,8 @@ func (cfg *Config) Validate() error {
 	return nil
 }
 
-func (cfg *Config) NeedsInit(tfState *TerraformState, logger log.Logger) bool {
-	if tfState == nil || tfState.Backend == nil {
-		return true
-	}
-
-	if len(tfState.Backend.Config) == 0 && len(cfg.BackendConfig) != 0 {
-		return true
-	}
-
-	if tfState.Backend.Type != cfg.BackendName {
-		logger.Debugf("Backend type has changed from %s to %s", cfg.BackendName, tfState.Backend.Type)
-
-		return true
-	}
-
-	return false
-}
-
-// GenerateTerraformCode generates the terraform code for configuring remote state backend.
-func (cfg *Config) GenerateTerraformCode(opts *options.TerragruntOptions, backendConfig map[string]any) error {
+// GenerateOpenTofuCode generates the OpenTofu/Terraform code for configuring remote state backend.
+func (cfg *Config) GenerateOpenTofuCode(opts *options.TerragruntOptions, backendConfig map[string]any) error {
 	if cfg.Generate == nil {
 		return errors.New(ErrGenerateCalledWithNoGenerateAttr)
 	}
diff --git a/internal/remotestate/remote_state.go b/internal/remotestate/remote_state.go
index 40ab89ba..6e0890e4 100644
--- a/internal/remotestate/remote_state.go
+++ b/internal/remotestate/remote_state.go
@@ -55,23 +55,23 @@ func (remote *RemoteState) DeleteBucket(ctx context.Context, opts *options.Terra
 	return remote.backend.DeleteBucket(ctx, remote.BackendConfig, opts)
 }
 
-// Init performs any actions necessary to initialize the remote state before it's used for storage. For example, if you're
+// Bootstrap performs any actions necessary to bootstrap remote state before it's used for storage. For example, if you're
 // using S3 or GCS for remote state storage, this may create the bucket if it doesn't exist already.
-func (remote *RemoteState) Init(ctx context.Context, opts *options.TerragruntOptions) error {
-	opts.Logger.Debugf("Initializing remote state for the %s backend", remote.BackendName)
+func (remote *RemoteState) Bootstrap(ctx context.Context, opts *options.TerragruntOptions) error {
+	opts.Logger.Debugf("Bootstrapping remote state for the %s backend", remote.BackendName)
 
-	return remote.backend.Init(ctx, remote.BackendConfig, opts)
+	return remote.backend.Bootstrap(ctx, remote.BackendConfig, opts)
 }
 
-// NeedsInit returns true if remote state needs to be configured. This will be the case when:
+// NeedsBootstrap returns true if remote state needs to be configured. This will be the case when:
 //
 // 1. Remote state auto-initialization has been disabled.
 // 2. Remote state has not already been configured.
 // 3. Remote state has been configured, but with a different configuration.
-// 4. The remote state initializer for this backend type, if there is one, says initialization is necessary.
-func (remote *RemoteState) NeedsInit(ctx context.Context, opts *options.TerragruntOptions) (bool, error) {
+// 4. The remote state bootstrapper for this backend type, if there is one, says bootstrap is necessary.
+func (remote *RemoteState) NeedsBootstrap(ctx context.Context, opts *options.TerragruntOptions) (bool, error) {
 	if opts.DisableBucketUpdate {
-		opts.Logger.Debug("Skipping remote state initialization")
+		opts.Logger.Debug("Skipping remote state bootstrap")
 		return false, nil
 	}
 
@@ -79,21 +79,10 @@ func (remote *RemoteState) NeedsInit(ctx context.Context, opts *options.Terragru
 		return false, nil
 	}
 
-	tfState, err := ParseTerraformStateFileFromLocation(remote.BackendName, remote.BackendConfig, opts.WorkingDir, opts.DataDir())
-	if err != nil {
-		return false, err
-	}
-
-	if remote.Config.NeedsInit(tfState, opts.Logger) {
-		return true, nil
-	}
-
-	if !tfState.IsRemote() {
-		return false, nil
-	}
+	// The specific backend type will check if bootstrap is necessary.
+	opts.Logger.Debugf("Checking if remote state bootstrap is necessary for the %s backend", remote.BackendName)
 
-	// Remote state initializer says initialization is necessary.
-	return remote.backend.NeedsInit(ctx, remote.BackendConfig, tfState.Backend.Config, opts)
+	return remote.backend.NeedsBootstrap(ctx, remote.BackendConfig, opts)
 }
 
 // GetTFInitArgs converts the RemoteState config into the format used by the `tofu init` command.
@@ -119,9 +108,9 @@ func (remote *RemoteState) GetTFInitArgs() []string {
 	return backendConfigArgs
 }
 
-// GenerateTerraformCode generates the terraform code for configuring remote state backend.
-func (remote *RemoteState) GenerateTerraformCode(opts *options.TerragruntOptions) error {
+// GenerateOpenTofuCode generates the OpenTofu/Terraform code for configuring remote state backend.
+func (remote *RemoteState) GenerateOpenTofuCode(opts *options.TerragruntOptions) error {
 	backendConfig := remote.backend.GetTFInitArgs(remote.BackendConfig)
 
-	return remote.Config.GenerateTerraformCode(opts, backendConfig)
+	return remote.Config.GenerateOpenTofuCode(opts, backendConfig)
 }
diff --git a/internal/remotestate/terraform_state_file.go b/internal/remotestate/terraform_state_file.go
index 0df28f08..39a15a66 100644
--- a/internal/remotestate/terraform_state_file.go
+++ b/internal/remotestate/terraform_state_file.go
@@ -44,7 +44,7 @@ type TerraformStateModule struct {
 
 // IsRemote returns true if this Terraform state is configured for remote state storage.
 func (state *TerraformState) IsRemote() bool {
-	return state.Backend != nil && state.Backend.Type != "local"
+	return state != nil && state.Backend != nil && state.Backend.Type != "local"
 }
 
 // ParseTerraformStateFileFromLocation parses the Terraform .tfstate file. If a local backend is used then search
diff --git a/test/integration_aws_test.go b/test/integration_aws_test.go
index f552c853..5af87f1f 100644
--- a/test/integration_aws_test.go
+++ b/test/integration_aws_test.go
@@ -124,6 +124,49 @@ func TestAwsBootstrapBackend(t *testing.T) {
 	}
 }
 
+func TestAwsBootstrapBackendLegacyBehavior(t *testing.T) {
+	t.Parallel()
+
+	helpers.CleanupTerraformFolder(t, testFixtureBootstrapS3Backend)
+	tmpEnvPath := helpers.CopyEnvironment(t, testFixtureBootstrapS3Backend)
+	rootPath := util.JoinPath(tmpEnvPath, testFixtureBootstrapS3Backend)
+
+	testID := strings.ToLower(helpers.UniqueID())
+
+	s3BucketName := "terragrunt-test-bucket-" + testID
+	dynamoDBName := "terragrunt-test-dynamodb-" + testID
+
+	defer func() {
+		deleteS3Bucket(t, s3BucketName, helpers.TerraformRemoteStateS3Region)
+		cleanupTableForTest(t, dynamoDBName, helpers.TerraformRemoteStateS3Region)
+	}()
+
+	commonConfigPath := util.JoinPath(rootPath, "common.hcl")
+	helpers.CopyTerragruntConfigAndFillPlaceholders(t, commonConfigPath, commonConfigPath, s3BucketName, dynamoDBName, helpers.TerraformRemoteStateS3Region)
+
+	_, stderr, err := helpers.RunTerragruntCommandWithOutput(t, "terragrunt run --all --non-interactive --log-level debug --experiment cli-redesign --working-dir "+rootPath+" apply")
+	require.NoError(t, err)
+
+	validateS3BucketExistsAndIsTagged(t, helpers.TerraformRemoteStateS3Region, s3BucketName, nil)
+	validateDynamoDBTableExistsAndIsTagged(t, helpers.TerraformRemoteStateS3Region, dynamoDBName, nil)
+
+	assert.Contains(t, stderr, "Use the explicit `--backend-bootstrap` flag to automatically provision backend resources before they're needed.")
+
+	t.Logf("Validating that the warning is not shown if the backend is not being bootstrapped automatically.")
+
+	// Remove the `.terragrunt-cache` directory in each of `unit1` and `unit2`
+	// to simulate that the backend was already bootstrapped.
+	helpers.RemoveFolder(t, util.JoinPath(rootPath, "unit1", ".terragrunt-cache"))
+	helpers.RemoveFolder(t, util.JoinPath(rootPath, "unit2", ".terragrunt-cache"))
+
+	// Users should only see the warning if the backend is actually being
+	// bootstrapped automatically.
+	_, stderr, err = helpers.RunTerragruntCommandWithOutput(t, "terragrunt run --all --non-interactive --log-level debug --experiment cli-redesign --working-dir "+rootPath+" apply")
+	require.NoError(t, err)
+
+	assert.NotContains(t, stderr, "Use the explicit `--backend-bootstrap` flag to automatically provision backend resources before they're needed.")
+}
+
 func TestAwsBootstrapBackendWithoutVersioning(t *testing.T) {
 	t.Parallel()
 
diff --git a/test/integration_sops_test.go b/test/integration_sops_test.go
index f60a6c86..2a57fc23 100644
--- a/test/integration_sops_test.go
+++ b/test/integration_sops_test.go
@@ -115,6 +115,6 @@ func TestSopsDecryptOnMissing(t *testing.T) {
 	errorOut = strings.ReplaceAll(errorOut, "\n", " ")
 
 	assert.Contains(t, errorOut, "Encountered error while evaluating locals in file ./terragrunt.hcl")
-	assert.Contains(t, errorOut, "./missing.yaml: no such file")
+	assert.Regexp(t, `\./missing\.yaml:.+no such file`, errorOut)
 
 }