How to Migrate Infrastructure from Terraform to OpenTofu

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.

How to migrate Terraform Infrastructure to OpenTofu

  1. Prerequisites

    Install Terraform and OpenTofu

  2. The Terraform Plan

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

  3. Initialize OpenTofu

    Initialize OpenTofu providers and local state.

  4. Make Infrastructure changes with OpenTofu

    Make changes to the infrastructure using OpenTofu.

  5. Migrate back to Terraform (Optional)

    Migrate back from OpenTofu to Terraform to test backward compatibility.

Prerequisites

Terraform and OpenTofu (How to Install OpenTofu). An AWS S3 Bucket for state remote backend storage.

7/Dec/2023: correctly upgraded from OpenTofu v1.6.0-alpha4 to OpenTofu v1.6.0-beta2

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.

# 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:

$ 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):

$ 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

Initialize OpenTofu

Initialize OpenTofu to download providers and set the backend.

$ 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).

....
# 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:

$ 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.

$ 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!
...

Leave a Reply

Your email address will not be published. Required fields are marked *


Related Cloud Tutorials

Securing your Infrastructure: Encrypting Terraform State Files with OpenTofu
Using the Terraform aws_route53_delegation_set, aws_route53_zone, and aws_route53_record resource blocks to configure DNS in AWS.
Using the Terraform aws_db_instance resource block to configure, launch, and secure RDS instances.
How to use the Terraform aws_instance resource block to configure, launch, and secure EC2 instances.
How to configure and use the Terraform aws_ami data source block to find and use AWS AMIs as templates (root volume snapshot with operating system and applications) for EC2 instances.
Javier Ruiz Cloud and SaaS Expert

Javier Ruiz

IT Wonder Lab tutorials are based on the diverse experience of Javier Ruiz, who founded and bootstrapped a SaaS company in the energy sector. His company, later acquired by a NASDAQ traded company, managed over €2 billion per year of electricity for prominent energy producers across Europe and America. Javier has over 25 years of experience in building and managing IT companies, developing cloud infrastructure, leading cross-functional teams, and transitioning his own company from on-premises, consulting, and custom software development to a successful SaaS model that scaled globally.

Are you looking for cloud automation best practices tailored to your company?

linkedin facebook pinterest youtube rss twitter instagram facebook-blank rss-blank linkedin-blank pinterest youtube twitter instagram