Terraform Backend Configurations: Implicit vs Explicit 'local'

Introduction When working with Terraform, backend configuration is a critical aspect that determines how and where your state files are stored. I was under the impression that omitting a backend configuration is equivalent to explicitly configuring a "local" backend. This post explores the subtle but important differences between these approaches, and how these differences impact tools like tf-migrate. The Difference Between No Backend and "local" Backend The confusion for me arose with this statement in docs : Terraform uses a backend called local by default. The local backend type stores state as a local file on disk. Default backend No Backend Specified When you don't specify any backend in your Terraform configuration: terraform { required_providers { aws = { source = "hashicorp/aws" version = "~>5.0" } } } After terraform init : Initializing the backend... Initializing provider plugins... - Finding hashicorp/aws versions matching "~> 5.0"... - Installing hashicorp/aws v5.96.0... - Installed hashicorp/aws v5.96.0 (signed by HashiCorp) Terraform has created a lock file .terraform.lock.hcl to record the provider selections it made above. Include this file in your version control repository so that Terraform can guarantee to make the same selections by default when you run "terraform init" in the future. Terraform has been successfully initialized! >> tree .terraform/ .terraform/ └── providers └── registry.terraform.io └── hashicorp └── aws └── 5.96.0 └── darwin_arm64 ├── LICENSE.txt └── terraform-provider-aws_v5.96.0_x5 7 directories, 2 files Terraform will: Use a "local" backend implicitly Store the state file (terraform.tfstate) in your working directory Use default settings for state locking and workspace management Run a terraform apply As expected you have a terraform.tfstate file. tree -a . . ├── .terraform │   └── providers │   └── registry.terraform.io │   └── hashicorp │   └── aws │   └── 5.96.0 │   └── darwin_arm64 │   ├── LICENSE.txt │   └── terraform-provider-aws_v5.96.0_x5 ├── .terraform.lock.hcl ├── main.tf ├── README.md ├── terraform.tfstate └── tf_migrate.md 8 directories, 7 files What does the state file say ? The state file here shows what you expect with the list of managed resources and their ids. { "version": 4, "terraform_version": "1.11.4", "serial": 1, "lineage": "aa6393bf-3f16-a14b-6d7b-2f120aa28239", "outputs": {}, "resources": [ { "mode": "managed", "type": "aws_s3_bucket", "name": "this", Explicit "local" Backend When you explicitly configure a "local" backend: terraform { backend "local" { path = "./terraform.tfstate" } required_providers { aws = { source = "hashicorp/aws" version = "~>5.0" } } } After terraform init: Initializing the backend... Successfully configured the backend "local"! Terraform will automatically use this backend unless the backend configuration changes. Initializing provider plugins... - Finding hashicorp/aws versions matching "~> 5.0"... - Installing hashicorp/aws v5.96.0... - Installed hashicorp/aws v5.96.0 (signed by HashiCorp) Terraform has created a lock file .terraform.lock.hcl to record the provider selections it made above. Include this file in your version control repository so that Terraform can guarantee to make the same selections by default when you run "terraform init" in the future. Terraform has been successfully initialized! tree .terraform/ .terraform/ ├── providers │   └── registry.terraform.io │   └── hashicorp │   └── aws │   └── 5.96.0 │   └── darwin_arm64 │   ├── LICENSE.txt │   └── terraform-provider-aws_v5.96.0_x5 └── terraform.tfstate Terraform will: Use a "local" backend explicitly Store the state file at the specified path (or in the .terraform directory) Allow customization of backend parameters like path, workspace_dir, etc. Run a terraform apply The directory structure reveal two terraform.tfstate files. tree -a . . ├── .terraform │   ├── providers │   │   └── registry.terraform.io │   │   └── hashicorp │   │   └── aws │   │   └── 5.96.0 │   │   └── darwin_arm64 │   │   ├── LICENSE.txt │   │   └── terraform-provider-aws_v5.96.0_x5 │   └── terraform.tfstate ├── .terraform.lock.hcl ├── main.tf ├── README.md ├── terraform.tfstate └── tf_migrate.md 8 directories, 8 files State file inside the .terraform directory. { "version": 3, "terraform_version": "1.11.4", "backend":

May 1, 2025 - 15:56
 0
Terraform Backend Configurations: Implicit vs Explicit 'local'

Introduction

When working with Terraform, backend configuration is a critical aspect that determines how and where your state files are stored. I was under the impression that omitting a backend configuration is equivalent to explicitly configuring a "local" backend. This post explores the subtle but important differences between these approaches, and how these differences impact tools like tf-migrate.

The Difference Between No Backend and "local" Backend

The confusion for me arose with this statement in docs :

Terraform uses a backend called local by default. 
The local backend type stores state as a local file on disk.

No Backend Specified

When you don't specify any backend in your Terraform configuration:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~>5.0"
    }
  }
}

After terraform init :

Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 5.0"...
- Installing hashicorp/aws v5.96.0...
- Installed hashicorp/aws v5.96.0 (signed by HashiCorp)
Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

>> tree .terraform/
.terraform/
└── providers
    └── registry.terraform.io
        └── hashicorp
            └── aws
                └── 5.96.0
                    └── darwin_arm64
                        ├── LICENSE.txt
                        └── terraform-provider-aws_v5.96.0_x5

7 directories, 2 files

Terraform will:

  • Use a "local" backend implicitly
  • Store the state file (terraform.tfstate) in your working directory
  • Use default settings for state locking and workspace management

Run a terraform apply

As expected you have a terraform.tfstate file.

tree -a .
.
├── .terraform
│   └── providers
│       └── registry.terraform.io
│           └── hashicorp
│               └── aws
│                   └── 5.96.0
│                       └── darwin_arm64
│                           ├── LICENSE.txt
│                           └── terraform-provider-aws_v5.96.0_x5
├── .terraform.lock.hcl
├── main.tf
├── README.md
├── terraform.tfstate
└── tf_migrate.md

8 directories, 7 files

  • What does the state file say ? The state file here shows what you expect with the list of managed resources and their ids.
{
  "version": 4,
  "terraform_version": "1.11.4",
  "serial": 1,
  "lineage": "aa6393bf-3f16-a14b-6d7b-2f120aa28239",
  "outputs": {},
  "resources": [
    {
      "mode": "managed",
      "type": "aws_s3_bucket",
      "name": "this",

Explicit "local" Backend

When you explicitly configure a "local" backend:

terraform {
  backend "local" {
    path = "./terraform.tfstate"
  }

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~>5.0"
    }
  }
}

After terraform init:

Initializing the backend...

Successfully configured the backend "local"! Terraform will automatically
use this backend unless the backend configuration changes.
Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 5.0"...
- Installing hashicorp/aws v5.96.0...
- Installed hashicorp/aws v5.96.0 (signed by HashiCorp)
Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

tree .terraform/
.terraform/
├── providers
│   └── registry.terraform.io
│       └── hashicorp
│           └── aws
│               └── 5.96.0
│                   └── darwin_arm64
│                       ├── LICENSE.txt
│                       └── terraform-provider-aws_v5.96.0_x5
└── terraform.tfstate

Terraform will:

  • Use a "local" backend explicitly
  • Store the state file at the specified path (or in the .terraform directory)
  • Allow customization of backend parameters like path, workspace_dir, etc.

Run a terraform apply

The directory structure reveal two terraform.tfstate files.


tree -a .
.
├── .terraform
│   ├── providers
│   │   └── registry.terraform.io
│   │       └── hashicorp
│   │           └── aws
│   │               └── 5.96.0
│   │                   └── darwin_arm64
│   │                       ├── LICENSE.txt
│   │                       └── terraform-provider-aws_v5.96.0_x5
│   └── terraform.tfstate
├── .terraform.lock.hcl
├── main.tf
├── README.md
├── terraform.tfstate
└── tf_migrate.md

8 directories, 8 files

  • State file inside the .terraform directory.

{
  "version": 3,
  "terraform_version": "1.11.4",
  "backend": {
    "type": "local",
    "config": {
      "path": "./terraform.tfstate",
      "workspace_dir": null
    },
    "hash": 2676510787
  }
}

  • State file at root of the directory where terraform apply is run.

{
  "version": 4,
  "terraform_version": "1.11.4",
  "serial": 1,
  "lineage": "805a28e9-d69a-3fef-84ca-f96d1cb21b11",
  "outputs": {},
  "resources": [
    {
      "mode": "managed",
      "type": "aws_s3_bucket",
      "name": "this",

Why This Matters for tfmigrate

The tfmigrate utility is designed to help manage Terraform state migrations from different backends to HCP Terraform or Terraform Enterprise. This tool expects a properly configured backend to function correctly.

The tfmigrate Challenge

When using tfmigrate with a configuration that doesn't explicitly specify a backend, tfmigrate may not correctly identify the backend type leading to errors during the migration process.

No backend specified


tf-migrate prepare
✓ Current working directory: ####/tfmigrate
The current directory is not a git repository, all git operations will be skipped.

Run 'git init' to initialize a Git repository.

✓ Environment readiness checks completed
✓ Found 3 HCP Terraform organizations
┌────────────────┐
│ Available Orgs │
├────────────────┤
│ test-org       │
│ ne-devops      │
│ manu-org       │
└────────────────┘
Enter the name of the HCP Terraform organization to migrate to:   manu-org
✓ You have selected organization  manu-org for migration
✓ Found 1 directories with Terraform files
┌────────────────────────────┐
│ Terraform File Directories │
├────────────────────────────┤
│ tfmigrate                  │
└────────────────────────────┘
The following 1 directories were skipped:

  •  .


as they either have no backend, no supported backend to migrate or were excluded based on user-specified skip-dir arguments.

All 1 directories either have errors or do not have a supported backend to migrate. No migration config will be generated

backend local specified

For tfmigrate to work properly, you should have an explicit backend configuration:

terraform {
  backend "local" {
    path = "terraform.tfstate"
  }
}

This allows tfmigrate to:

  • Correctly identify the source backend type
  • Generate appropriate migration configurations
tf-migrate prepare
✓ Current working directory: ####/tfmigrate
The current directory is not a git repository, all git operations will be skipped.

Run 'git init' to initialize a Git repository.

✓ Environment readiness checks completed
✓ Found 3 HCP Terraform organizations
┌────────────────┐
│ Available Orgs │
├────────────────┤
│ abp-test-org   │
│ ne-devops      │
│ manu-org       │
└────────────────┘
Enter the name of the HCP Terraform organization to migrate to:  manu-org
✓ You have selected organization manu-org for migration
✓ Found 1 directories with Terraform files
┌────────────────────────────┐
│ Terraform File Directories │
├────────────────────────────┤
│ tfmigrate                  │
└────────────────────────────┘
✓ Migration config generation completed

Conclusion

While it might seem redundant to explicitly configure a "local" backend when it's the default, doing so provides clarity, consistency, and compatibility with tools like tfmigrate. There is definitely a counter argument that you might as well modify the configuration to use TFE or HCP TF backend without needing to use the tf-migrate utility in case of local state.