# How to Migrate Infrastructure from Terraform to OpenTofu

> How to migrate existing AWS Terraform-managed infrastructure that uses remote backend storage (e.g. S3) to OpenTofu.

- Source: https://www.itwonderlab.com/terraform-to-opentofu/
- Published: 2023-11-09
- Updated: 2026-10-05
- Author: Javier Ruiz Jiménez (https://www.javierruizjimenez.com/)
- Site: IT Wonder Lab (https://www.itwonderlab.com/)

---

## How to start testing OpenTofu by migrating existing Terraform resources and remote state

This tutorial shows how to migrate an existing AWS Terraform-managed infrastructure that uses remote backend storage (e.g. S3) for the state to OpenTofu.

### Prerequisites

Terraform and OpenTofu ([How to Install OpenTofu](https://www.itwonderlab.com/how-to-install-opentofu/)). An AWS S3 Bucket for state remote backend storage.

> [!NOTE]
> This tutorial was written with OpenTofu 1.6, the first stable release, and Terraform 1.5. The procedure is the same with current versions, but check the [OpenTofu migration guides](https://opentofu.org/docs/intro/migration/) for the version of Terraform that you use: OpenTofu can read the state of the Terraform versions that it is compatible with, and newer Terraform releases may include changes that it does not support.

### The Terraform Plan

Use your existing Terraform infrastructure definition or create a new one to test the migration procedure.

#### Create an example Terraform Infrastructure definition file (optional)

Here we will be creating a VPC and a Subnet on AWS with Terraform and once deployed we will be migrating the Terraform infrastructure definition to OpenTofu.

The example uses an AWS S3 bucket as remote backend storage for the Terraform state.

Update your AWS S3 bucket, key, profile, region, and credential files with your account data.

Resources are created with the tag `Ver = "Terraform"`, it will be changed to “OpenTofu” after migration.

```yaml
# Copyright (C) 2018 - 2023 IT Wonder Lab (https://www.itwonderlab.com)
#
# This software may be modified and distributed under the terms
# of the MIT license.  See the LICENSE file for details.
# -------------------------------- WARNING --------------------------------
# IT Wonder Lab's best practices for infrastructure include modularizing 
# Terraform/OpenTofu configuration. 
# In this example, we define everything in a single file. 
# See other tutorials for best practices at itwonderlab.com
# -------------------------------- WARNING --------------------------------

#Define Terrraform Providers and Backend
terraform {
  required_version = "> 1.5"

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

  # Stores the state as a given key in a given bucket on Amazon S3.
  backend "s3" {
    bucket                  = "ditwonderlab"
    key                     = "ditwl_infradmin/aws_basic_ec2/terraform"
    profile                 = "ditwl_infradmin"
    region                  = "us-east-1"
    shared_credentials_files = ["~/.aws/credentials"] # See change 690. Before shared_credentials_file = "~/.aws/credentials"
  }
}

#-----------------------------------------
# Default provider: AWS
#-----------------------------------------
provider "aws" {
  shared_credentials_files = ["~/.aws/credentials"]
  profile                  = "ditwl_infradmin"
  region                   = "us-east-1" 
  //See BUG https://github.com/hashicorp/terraform-provider-aws/issues/30488
}

# VPC
resource "aws_vpc" "ditlw-vpc" {
  cidr_block = "172.21.0.0/19" #172.21.0.0 - 172.21.31.254
  tags = {
    Name = "ditlw-vpc"
    Ver = "Terraform"
  }
}

# Subnet
resource "aws_subnet" "ditwl-sn-za-pro-pub-00" {
  vpc_id     = aws_vpc.ditlw-vpc.id
  cidr_block = "172.21.0.0/23" #172.21.0.0 - 172.21.1.255
  tags = {
    Name = "ditwl-sn-za-pro-pub-00"
    Ver = "Terraform"
  }
}
```

#### Initialize, Plan, and Apply the Change to the infrastructure using Terraform

Create the demo infrastructure.

#### Init

Initialize the S3 backend and download the Terraform providers:

```shell
$ terraform init

Initializing the backend...

Successfully configured the backend "s3"! 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.24.0...
- Installed hashicorp/aws v5.24.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!
...
```

**Plan & Apply**

Plan and apply the changes to the AWS infrastructure (only apply is shown):

```shell
$ terraform apply

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_subnet.ditwl-sn-za-pro-pub-00 will be created
  + resource "aws_subnet" "ditwl-sn-za-pro-pub-00" {
      + arn                                            = (known after apply)
      + assign_ipv6_address_on_creation                = false
      + availability_zone                              = (known after apply)
      + availability_zone_id                           = (known after apply)
      + cidr_block                                     = "172.21.0.0/23"
      + enable_dns64                                   = false
      + enable_resource_name_dns_a_record_on_launch    = false
      + enable_resource_name_dns_aaaa_record_on_launch = false
      + id                                             = (known after apply)
      + ipv6_cidr_block_association_id                 = (known after apply)
      + ipv6_native                                    = false
      + map_public_ip_on_launch                        = false
      + owner_id                                       = (known after apply)
      + private_dns_hostname_type_on_launch            = (known after apply)
      + tags                                           = {
          + "Name" = "ditwl-sn-za-pro-pub-00"
          + "Ver"  = "Terraform"
        }
      + tags_all                                       = {
          + "Name" = "ditwl-sn-za-pro-pub-00"
          + "Ver"  = "Terraform"
        }
      + vpc_id                                         = (known after apply)
    }

  # aws_vpc.ditlw-vpc will be created
  + resource "aws_vpc" "ditlw-vpc" {
      + arn                                  = (known after apply)
      + cidr_block                           = "172.21.0.0/19"
      + default_network_acl_id               = (known after apply)
      + default_route_table_id               = (known after apply)
      + default_security_group_id            = (known after apply)
      + dhcp_options_id                      = (known after apply)
      + enable_dns_hostnames                 = (known after apply)
      + enable_dns_support                   = true
      + enable_network_address_usage_metrics = (known after apply)
      + id                                   = (known after apply)
      + instance_tenancy                     = "default"
      + ipv6_association_id                  = (known after apply)
      + ipv6_cidr_block                      = (known after apply)
      + ipv6_cidr_block_network_border_group = (known after apply)
      + main_route_table_id                  = (known after apply)
      + owner_id                             = (known after apply)
      + tags                                 = {
          + "Name" = "ditlw-vpc"
          + "Ver"  = "Terraform"
        }
      + tags_all                             = {
          + "Name" = "ditlw-vpc"
          + "Ver"  = "Terraform"
        }
    }

Plan: 2 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

aws_vpc.ditlw-vpc: Creating...
aws_vpc.ditlw-vpc: Creation complete after 5s [id=vpc-0a1bd319d6f8efa64]
aws_subnet.ditwl-sn-za-pro-pub-00: Creating...
aws_subnet.ditwl-sn-za-pro-pub-00: Creation complete after 2s [id=subnet-08fc86ea7f14ba5e4]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
```

**Check that the state has been stored in S3**

![ditwonderlab S3 bucket S3 Global](https://www.itwonderlab.com/media/tutorials/Terraform-OpenTofu-Migration/ditwonderlab-S3-bucket-S3-Global.png)

### Initialize OpenTofu

Initialize OpenTofu to download providers and set the backend.

```shell
$ tofu init

Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 5.0"...
- Reusing previous version of registry.terraform.io/hashicorp/aws from the dependency lock file
- Installing hashicorp/aws v5.24.0...
- Installed hashicorp/aws v5.24.0 (signed, key ID 0DC64ED093B3E9FF)
- Using previously-installed registry.terraform.io/hashicorp/aws v5.24.0

Providers are signed by their developers.
If you'd like to know more about provider signing, you can read about it here: https://opentofu.org/docs/cli/plugins/signing/

OpenTofu has made some changes to the provider dependency selections recorded in the .terraform.lock.hcl file. Review those changes and commit them to your version control system if they represent changes you intended to make.

OpenTofu has been successfully initialized!
...
```

### Make Infrastructure changes with OpenTofu

We can now use OpenTofu to manage the infrastructure.

**Make some changes**

Edit the Terraform definition file to replace tag `Ver = "Terraform"` with `Ver = "OpenTofu"` and apply the change using OpenTofu. You might add or change other infrastructure resources as well (we are adding another subnet `ditwl-sn-za-pro-pri-02`).

```hcl
....
# VPC
resource "aws_vpc" "ditlw-vpc" {
  cidr_block = "172.21.0.0/19" #172.21.0.0 - 172.21.31.254
  tags = {
    Name = "ditlw-vpc"
    Ver = "OpenTofu"
  }
}

# Subnet
resource "aws_subnet" "ditwl-sn-za-pro-pub-00" {
  vpc_id     = aws_vpc.ditlw-vpc.id
  cidr_block = "172.21.0.0/23" #172.21.0.0 - 172.21.1.255
  tags = {
    Name = "ditwl-sn-za-pro-pub-00"
    Ver = "OpenTofu"
  }
}

# Subnet
resource "aws_subnet" "ditwl-sn-za-pro-pri-02" {
  vpc_id     = aws_vpc.ditlw-vpc.id
  cidr_block = "172.21.2.0/23" #172.21.2.0 - 172.21.3.255
  tags = {
    Name = "ditwl-sn-za-pro-pri-02"
    Ver = "OpenTofu"
  }
}
```

**Plan and Apply the changes with OpenTofu**

Apply changes:

```shell
$ tofu apply
aws_vpc.ditlw-vpc: Refreshing state... [id=vpc-0a1bd319d6f8efa64]
aws_subnet.ditwl-sn-za-pro-pub-00: Refreshing state... [id=subnet-08fc86ea7f14ba5e4]

OpenTofu used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create
  ~ update in-place

OpenTofu will perform the following actions:

  # aws_subnet.ditwl-sn-za-pro-pri-02 will be created
  + resource "aws_subnet" "ditwl-sn-za-pro-pri-02" {
      + arn                                            = (known after apply)
      + assign_ipv6_address_on_creation                = false
      + availability_zone                              = (known after apply)
      + availability_zone_id                           = (known after apply)
      + cidr_block                                     = "172.21.2.0/23"
      + enable_dns64                                   = false
      + enable_resource_name_dns_a_record_on_launch    = false
      + enable_resource_name_dns_aaaa_record_on_launch = false
      + id                                             = (known after apply)
      + ipv6_cidr_block_association_id                 = (known after apply)
      + ipv6_native                                    = false
      + map_public_ip_on_launch                        = false
      + owner_id                                       = (known after apply)
      + private_dns_hostname_type_on_launch            = (known after apply)
      + tags                                           = {
          + "Name" = "ditwl-sn-za-pro-pri-02"
          + "Ver"  = "OpenTofu"
        }
      + tags_all                                       = {
          + "Name" = "ditwl-sn-za-pro-pri-02"
          + "Ver"  = "OpenTofu"
        }
      + vpc_id                                         = "vpc-0a1bd319d6f8efa64"
    }

  # aws_subnet.ditwl-sn-za-pro-pub-00 will be updated in-place
  ~ resource "aws_subnet" "ditwl-sn-za-pro-pub-00" {
        id                                             = "subnet-08fc86ea7f14ba5e4"
      ~ tags                                           = {
            "Name" = "ditwl-sn-za-pro-pub-00"
          ~ "Ver"  = "Terraform" -> "OpenTofu"
        }
      ~ tags_all                                       = {
          ~ "Ver"  = "Terraform" -> "OpenTofu"
            # (1 unchanged element hidden)
        }
        # (15 unchanged attributes hidden)
    }

  # aws_vpc.ditlw-vpc will be updated in-place
  ~ resource "aws_vpc" "ditlw-vpc" {
        id                                   = "vpc-0a1bd319d6f8efa64"
      ~ tags                                 = {
            "Name" = "ditlw-vpc"
          ~ "Ver"  = "Terraform" -> "OpenTofu"
        }
      ~ tags_all                             = {
          ~ "Ver"  = "Terraform" -> "OpenTofu"
            # (1 unchanged element hidden)
        }
        # (14 unchanged attributes hidden)
    }

Plan: 1 to add, 2 to change, 0 to destroy.

Do you want to perform these actions?
  OpenTofu will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

aws_vpc.ditlw-vpc: Modifying... [id=vpc-0a1bd319d6f8efa64]
aws_vpc.ditlw-vpc: Modifications complete after 4s [id=vpc-0a1bd319d6f8efa64]
aws_subnet.ditwl-sn-za-pro-pri-02: Creating...
aws_subnet.ditwl-sn-za-pro-pub-00: Modifying... [id=subnet-08fc86ea7f14ba5e4]
aws_subnet.ditwl-sn-za-pro-pub-00: Modifications complete after 2s [id=subnet-08fc86ea7f14ba5e4]
aws_subnet.ditwl-sn-za-pro-pri-02: Creation complete after 2s [id=subnet-053b224aad9c0ee52]

Apply complete! Resources: 1 added, 2 changed, 0 destroyed.
```

The changes on Ver labels and the creation of a new subnet have been done with OpenTofu.

### Migrate back to Terraform (Optional)

If you want to go back to Terraform, just run `terraform init`.

```shell
$ terraform init

Initializing the backend...

Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file
- Reusing previous version of registry.opentofu.org/hashicorp/aws from the dependency lock file
- Using previously-installed hashicorp/aws v5.24.0
- Using previously-installed registry.opentofu.org/hashicorp/aws v5.24.0

Terraform has been successfully initialized!
...
```
