Всем привет! На связи Дмитрий Силкин, DevOps-инженер компании «Флант». 30 апреля из бета-тестирования вышел OpenTofu 1.7.0. Это Open Source-форк Terraform, который развивается под управлением Linux Foundation. Ранее мы уже писали о причинах разработки OpenTofu, связанных с изменением лицензионной политики HashiCorp в отношении своих продуктов. В этой же статье я сделаю обзор версии 1.7.0: установлю OpenTofu, выполню миграцию инфраструктуры с Terraform, а также рассмотрю ключевые особенности данного релиза.

Установка OpenTofu

Дистрибутив OpenTofu уже доступен на различных платформах. Установим OpenTofu на macOS с помощью пакетного менеджера Homebrew:

brew update
brew install opentofu

Убедимся, что OpenTofu установлен:

❯ tofu -version
OpenTofu v1.7.0
on darwin_arm64

Миграция с Terraform

Теперь выполним миграцию ресурсов Terraform. В качестве примера будем использовать инфраструктуру в Yandex Cloud, которая состоит из отдельного VPC, двух виртуальных машин и DNS-зоны с двумя A-записями:

Конфигурация провайдера следующая:

terraform {
  required_providers {
    yandex = {
      source  = "yandex-cloud/yandex"
    }
  }
  required_version = ">= 0.13"
}

provider "yandex" {
  zone      = "ru-central1-a"
}

Последовательность действий для миграции с разных версий Terraform почти аналогична. У нас инфраструктура развёрнута с помощью Terraform 1.8.3, поэтому нам подойдёт актуальный OpenTofu 1.7.0.

  1. Выполним команды Terraform, чтобы убедиться в соответствии Terraform-state нашей инфраструктуре:

❯ terraform plan
data.yandex_compute_image.vm_2_image: Reading...
data.yandex_compute_image.vm_1_image: Reading...
yandex_vpc_address.vm_1_public_ip: Refreshing state... [id=e9b2ef4mkpojsu6lkba2]
yandex_vpc_network.infra_network: Refreshing state... [id=enpr0clfntcn6k1ljc3l]
yandex_vpc_address.vm_2_public_ip: Refreshing state... [id=e9bvv8ep3ick80l7t4td]
data.yandex_compute_image.vm_1_image: Read complete after 0s [id=fd80bca9kcrb3ubq7eaf]
data.yandex_compute_image.vm_2_image: Read complete after 0s [id=fd80bca9kcrb3ubq7eaf]
yandex_dns_zone.dns_zone: Refreshing state... [id=dnsdv83jlsms5ckpea79]
yandex_vpc_subnet.infra_subnet[2]: Refreshing state... [id=fl84b0cauja9rnf97mt3]
yandex_vpc_subnet.infra_subnet[0]: Refreshing state... [id=e9b9o5fg8f8qfi9rsi5v]
yandex_vpc_subnet.infra_subnet[1]: Refreshing state... [id=e2lfp68t0gj5d3te7fea]
yandex_compute_instance.vm_1: Refreshing state... [id=fhmmv8hlqn52b1jcaj78]
yandex_compute_instance.vm_2: Refreshing state... [id=fhmqqq3jrl1ertu5snq2]
yandex_dns_recordset.vm_1_dns: Refreshing state... [id=dnsdv83jlsms5ckpea79/vm-1.example.com./A]
yandex_dns_recordset.vm_2_dns: Refreshing state... [id=dnsdv83jlsms5ckpea79/vm-2.example.com./A]

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.
  1. Применим конфигурацию terraform apply

  2. Выполним бэкап файла, в котором содержится Terraform-state.

  3. Согласно плану миграции из ссылки выше внесём изменения в код Terraform, если присутствуют следующие конструкции:

    • S3 Backend:

      • OpenTofu не использует параметр skip_s3_checksum, поэтому его нужно убрать. 

      • Если используется опция endpoints → sso или переменная AWS_ENDPOINT_URL, их нужно удалить.

    • Removed Blocks:

      • Removed-блоки в OpenTofu работают иначе, чем в Terraform. Далее мы рассмотрим особенности этой конструкции. Но для выполнения миграции нужно удалить блок lifecycle либо, если используется опция lifecycle → destroy = true, весь removed-блок.

    • Testing Changes:

      • На данный момент поддержка mock providerотсутствует. Поэтому, если в коде присутствуют terraform test и mock provider, нужно переработать тесты так, чтобы данный провайдер не использовался. Аналогично нужно сделать для override_resource, override_data, override_module.

  4. После необходимых изменений кода выполним инициализацию OpenTofu:

❯ tofu init

Initializing the backend...

Initializing provider plugins...
- Finding latest version of yandex-cloud/yandex...
- Installing yandex-cloud/yandex v0.118.0...
- Installed yandex-cloud/yandex v0.118.0. Signature validation was skipped due to the registry not containing GPG keys for this provider

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!

You may now begin working with OpenTofu. Try running "tofu plan" to see
any changes that are required for your infrastructure. All OpenTofu commands
should now work.

If you ever set or change modules or backend configuration for OpenTofu,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
  1. Выполним планирование ресурсов:

❯ tofu plan
yandex_vpc_network.infra_network: Refreshing state... [id=enpr0clfntcn6k1ljc3l]
yandex_vpc_address.vm_2_public_ip: Refreshing state... [id=e9bvv8ep3ick80l7t4td]
yandex_vpc_address.vm_1_public_ip: Refreshing state... [id=e9b2ef4mkpojsu6lkba2]
data.yandex_compute_image.vm_2_image: Reading...
data.yandex_compute_image.vm_1_image: Reading...
data.yandex_compute_image.vm_2_image: Read complete after 1s [id=fd80bca9kcrb3ubq7eaf]
data.yandex_compute_image.vm_1_image: Read complete after 1s [id=fd80bca9kcrb3ubq7eaf]
yandex_dns_zone.dns_zone: Refreshing state... [id=dnsdv83jlsms5ckpea79]
yandex_vpc_subnet.infra_subnet[2]: Refreshing state... [id=fl84b0cauja9rnf97mt3]
yandex_vpc_subnet.infra_subnet[1]: Refreshing state... [id=e2lfp68t0gj5d3te7fea]
yandex_vpc_subnet.infra_subnet[0]: Refreshing state... [id=e9b9o5fg8f8qfi9rsi5v]
yandex_compute_instance.vm_1: Refreshing state... [id=fhmmv8hlqn52b1jcaj78]
yandex_compute_instance.vm_2: Refreshing state... [id=fhmqqq3jrl1ertu5snq2]
yandex_dns_recordset.vm_1_dns: Refreshing state... [id=dnsdv83jlsms5ckpea79/vm-1.example.com./A]
yandex_dns_recordset.vm_2_dns: Refreshing state... [id=dnsdv83jlsms5ckpea79/vm-2.example.com./A]

No changes. Your infrastructure matches the configuration.

OpenTofu has compared your real infrastructure against your configuration and found no differences, so no changes are needed.
  1. OpenTofu говорит, что нет никаких изменений. Применим конфигурацию:

❯ tofu apply
data.yandex_compute_image.vm_1_image: Reading...
yandex_vpc_address.vm_2_public_ip: Refreshing state... [id=e9bvv8ep3ick80l7t4td]
data.yandex_compute_image.vm_2_image: Reading...
yandex_vpc_network.infra_network: Refreshing state... [id=enpr0clfntcn6k1ljc3l]
yandex_vpc_address.vm_1_public_ip: Refreshing state... [id=e9b2ef4mkpojsu6lkba2]
data.yandex_compute_image.vm_2_image: Read complete after 1s [id=fd80bca9kcrb3ubq7eaf]
data.yandex_compute_image.vm_1_image: Read complete after 1s [id=fd80bca9kcrb3ubq7eaf]
yandex_dns_zone.dns_zone: Refreshing state... [id=dnsdv83jlsms5ckpea79]
yandex_vpc_subnet.infra_subnet[0]: Refreshing state... [id=e9b9o5fg8f8qfi9rsi5v]
yandex_vpc_subnet.infra_subnet[2]: Refreshing state... [id=fl84b0cauja9rnf97mt3]
yandex_vpc_subnet.infra_subnet[1]: Refreshing state... [id=e2lfp68t0gj5d3te7fea]
yandex_compute_instance.vm_2: Refreshing state... [id=fhmqqq3jrl1ertu5snq2]
yandex_compute_instance.vm_1: Refreshing state... [id=fhmmv8hlqn52b1jcaj78]
yandex_dns_recordset.vm_2_dns: Refreshing state... [id=dnsdv83jlsms5ckpea79/vm-2.example.com./A]
yandex_dns_recordset.vm_1_dns: Refreshing state... [id=dnsdv83jlsms5ckpea79/vm-1.example.com./A]

No changes. Your infrastructure matches the configuration.

OpenTofu has compared your real infrastructure against your configuration and found no differences, so no changes are needed.

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

Outputs:

vm_1_address = "<ip 1>"
vm_1_name = "vm-1"
vm_2_address = "<ip 2>"
vm_2_name = "vm-2"
  1. Миграция выполнена. Теперь на данной инфраструктуре можно использовать OpenTofu. Посмотрим, в чём разница между бэкапом Terraform-state и текущим:

❯ diff state tofu_state
3,4c3,4
<   "terraform_version": "1.8.3",
<   "serial": 4,
---
>   "terraform_version": "1.7.0",
>   "serial": 5,
29c29
<       "provider": "provider[\"registry.terraform.io/yandex-cloud/yandex\"]",
---
>       "provider": "provider[\"registry.opentofu.org/yandex-cloud/yandex\"]",
59c59
<       "provider": "provider[\"registry.terraform.io/yandex-cloud/yandex\"]",
---
>       "provider": "provider[\"registry.opentofu.org/yandex-cloud/yandex\"]",
89c89
<       "provider": "provider[\"registry.terraform.io/yandex-cloud/yandex\"]",
---
>       "provider": "provider[\"registry.opentofu.org/yandex-cloud/yandex\"]",
205c205
<       "provider": "provider[\"registry.terraform.io/yandex-cloud/yandex\"]",
---
>       "provider": "provider[\"registry.opentofu.org/yandex-cloud/yandex\"]",
321c321
<       "provider": "provider[\"registry.terraform.io/yandex-cloud/yandex\"]",
---
>       "provider": "provider[\"registry.opentofu.org/yandex-cloud/yandex\"]",
350c350
<       "provider": "provider[\"registry.terraform.io/yandex-cloud/yandex\"]",
---
>       "provider": "provider[\"registry.opentofu.org/yandex-cloud/yandex\"]",
379c379
<       "provider": "provider[\"registry.terraform.io/yandex-cloud/yandex\"]",
---
>       "provider": "provider[\"registry.opentofu.org/yandex-cloud/yandex\"]",
410c410
<       "provider": "provider[\"registry.terraform.io/yandex-cloud/yandex\"]",
---
>       "provider": "provider[\"registry.opentofu.org/yandex-cloud/yandex\"]",
444c444
<       "provider": "provider[\"registry.terraform.io/yandex-cloud/yandex\"]",
---
>       "provider": "provider[\"registry.opentofu.org/yandex-cloud/yandex\"]",
478c478
<       "provider": "provider[\"registry.terraform.io/yandex-cloud/yandex\"]",
---
>       "provider": "provider[\"registry.opentofu.org/yandex-cloud/yandex\"]",
506c506
<       "provider": "provider[\"registry.terraform.io/yandex-cloud/yandex\"]",
---
>       "provider": "provider[\"registry.opentofu.org/yandex-cloud/yandex\"]",

Разница получилась в terraform_version и адресах registry для провайдеров, так как OpenTofu использует собственный registry.

Ключевые особенности

Рассмотрим возможности, которые появились в OpenTofu 1.7.0.

Шифрование Terraform-state 

Шифрование выполняется либо с помощью passphrase, либо с помощью облачных хранилищ: AWS KMS, GCP KMS, OpenBao и так далее. Чтобы зашифровать существующий Terraform-state, добавим следующую конфигурацию с использованием passphrase:

terraform {
  required_providers {
    yandex = {
      source  = "yandex-cloud/yandex"
    }
  }
  required_version = ">= 0.13"
  encryption {
    method "unencrypted" "migrate" {}

    key_provider "pbkdf2" "mykey" {
      passphrase = "passphrasepassphrase123!"
    }

    method "aes_gcm" "new_method" {
      keys = key_provider.pbkdf2.mykey
    }

    state {
      method = method.aes_gcm.new_method

      fallback {
        method = method.unencrypted.migrate
      }
    }
  }
}
provider "yandex" {
  zone      = "ru-central1-a"
}

После её применения получили зашифрованный Terraform-state:

Чтобы расшифровать файл, внесём изменения в конфигурацию, указав в качестве fallback метод шифрования, а в state пропишем unencrypted-метод:

terraform {
  required_providers {
    yandex = {
      source  = "yandex-cloud/yandex"
    }
  }
  required_version = ">= 0.13"
  encryption {
    method "unencrypted" "migrate" {}

    key_provider "pbkdf2" "mykey" {
      passphrase = "passphrasepassphrase123!"
    }

    method "aes_gcm" "new_method" {
      keys = key_provider.pbkdf2.mykey
    }

    state {
      method = method.unencrypted.migrate

      fallback {
        method = method.aes_gcm.new_method
      }

      enforced = false      
    }
  }
}
provider "yandex" {
  zone      = "ru-central1-a"
}

После применения конфигурации файл Terraform-state вновь стал храниться в открытом виде.

Поддержка динамических provider-defined-функций

Кроме добавления поддержки динамических provider-defined-функций, разработчики OpentTofu создали собственные провайдеры Lua и Go. Благодаря им прямо в конфигурации Terraform можно писать кастомные функции на этих языках программирования. 

Рассмотрим пример сортировки IPv4/IPv6- и MAC-адресов с помощью Go в конфигурации Terraform:

  1. Добавим провайдера Go в required_providers:

required_providers {
    yandex = {
      source  = "yandex-cloud/yandex"
    }
    go = {
      source = "registry.opentofu.org/opentofu/go"
      version = "0.0.3"
    }
  }
  1. Положим рядом с конфигурациями файл network.go, в котором опишем функции сортировки:

package lib

import (
	"bytes"
	"net"
	"sort"
)

func Sort_IP(ips []string) []string {
	parsed := make([]net.IP, len(ips))

	for i, ip := range ips {
		parsed[i] = net.ParseIP(ip)
	}

	sort.Slice(parsed, func(i, j int) bool {
		return bytes.Compare(parsed[i], parsed[j]) < 0
	})

	result := make([]string, len(ips))

	for i, ip := range parsed {
		result[i] = ip.String()
	}

	return result
}

func Sort_Mac(macs []string) ([]string, error) {
	parsed := make([]net.HardwareAddr, len(macs))

	var err error

	for i, mac := range macs {
		parsed[i], err = net.ParseMAC(mac)
		if err != nil {
			return nil, err
		}
	}

	sort.Slice(parsed, func(i, j int) bool {
		return bytes.Compare(parsed[i], parsed[j]) < 0
	})

	result := make([]string, len(macs))

	for i, mac := range parsed {
		result[i] = mac.String()
	}

	return result, nil
}
  1. Опишем применение provider-defined-функции в network.tf:

provider "go" {
    alias = "net"
    go = file("./network.go")
}

locals {
    target_ips = [
    "192.168.1.5",
    "69.52.220.44",
    "10.152.16.23",
    "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
    "192.168.3.10",
    "192.168.1.4",
    "192.168.1.41",
    ]
    target_macs = [
    "00:00:5e:00:53:01",
    "02-00-5e-10-00-00-00-01",
    "0000.0000.fe80.0000.0000.0000.0200.5e10.0000.0001",
    ]

    sorted_ips = provider::go::net::sort_ip(local.target_ips)
    sorted_macs = provider::go::net::sort_mac(local.target_macs)
}

resource "local_file" "ip_configs" {
    filename = "./ip_addresses.txt"
    content = join("\n", local.sorted_ips)
}


resource "local_file" "mac_configs" {
    filename = "./mac_addresses.txt"
    content = join("\n", local.sorted_macs)
}
  1. Выполним tofu plan:

В результате мы смогли вызвать функции, написанные на Go, прямо в конфигурации Terraform. Более подробно авторы рассказывают о provider-defined-функциях в видео «Native Lua (+more) in OpenTofu 1.7.0».

Поддержка removed-блоков 

Позволяет удалять ресурсы из state-файла и конфигурации, при этом сохраняя их в развёрнутой инфраструктуре. Для примера удалим из конфигурации ресурс yandex_compute_instance.vm_2 и добавим блок removed:

removed {
  from = yandex_compute_instance.vm_2
}

При планировании OpenTofu пометит ресурс к удалению из Terraform-state, не удаляя его из инфраструктуры:

Примечание

Как раз из-за кода этой функциональности HashiCorp обвинила разработчиков OpenTofu в плагиате.

Loopable import blocks 

Функция позволяет использовать конструкцию for_each для импорта ресурсов в Terraform. В качестве примера разработчики предлагают следующий код:

variable "server_ids" {
  type = list(string)
}

resource "random_id" "test_id" {
  byte_length = 8
  count = 2
}

import {
  to = random_id.test_id[tonumber(each.key)]
  id = each.value
  for_each = {
    for idx, item in var.server_ids: idx => item
  }
}

output "id" {
  value = random_id.test_id.*.b64_url
}

В результате получится случайно сгенерированная последовательность ID:

Outputs:

id = [
  "5bKi8T9ii1s",
  "ztf4QbiyiRo",
]

Также стоит отметить, что при использовании конструкции for_each невозможно выполнить генерацию конфигураций с помощью флага -generate-config-out:

❯ tofu plan -generate-config-out=gen.tf
var.vm_ids
  Enter a value: ["fhm4itmi6agp8qmru1t9", "fhmehq41fusfbaet5e1p"]

╷
│ Error: Configuration generation for count and for_each resources not supported

Выводы

По словам разработчиков, OpenTofu обеспечивает полную совместимость с Terraform. Однако поддержка будущих версий Terraform будет зависеть от сообщества. Кроме того, OpenTofu обладает дополнительной функциональностью, которая отсутствует в Terraform. Фичи в проект добавляются согласно дорожной карте.

Тем временем некоторые крупные компании уже начали мигрировать свою инфраструктуру и использовать OpenTofu. Так, Oracle мигрировала свой EBS Cloud Manager с Terraform на OpenTofu. GitOps-инструмент FluxCD также полностью совместим с OpenTofu. А в Grafana Labs предлагают использовать в качестве datasource OpenTofu, а не Terraform.

Стоит также добавить ложку дёгтя: кроме обвинений от HashiCorp в плагиате кода, OpenTofu, по мнению части сообщества, развивается медленнее, чем Terraform. Например, разработчик Cristian Măgherușan-Stanciu проанализировал активность в репозиториях OpenTofu и Terraform и пришел к выводу, что за полтора года бо́льшую часть коммитов сделали лишь шесть разработчиков и при этом их наибольшая активность пришлась на момент начала анонса OpenTofu.

Несмотря на это, OpenTofu на данный момент выглядит как перспективная Open Source-альтернатива Terraform. Поэтому мы во «Фланте» тоже планируем использовать его в своих проектах в будущем.

P. S.

Читайте также в наш блоге:

Комментарии (6)


  1. selivanov_pavel
    13.06.2024 06:52

    Где у него документация к модулям?

    https://github.com/opentofu/registry/tree/main/providers это просто куча кода.

    Где аналог доков, потипу https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws/latest ?


    1. TimurTukaev
      13.06.2024 06:52
      +1

      Москва не сразу строилась. Команда OpenTofu уже сделала немало всего. Со временем и документация подтянется.


    1. ZillahGiovanni
      13.06.2024 06:52
      +6

      https://library.tf/providers
      гуглится кстати...


      1. selivanov_pavel
        13.06.2024 06:52
        +1

        Спасибо. Теперь можно и подумать над тем, чтобы это пробовать. Софт без документации - как чемодан без ручки

        На https://opentofu.org/ и https://github.com/opentofu/registry никаких ссылок на этот проект нету. Достаточно неочевидно, что надо искать в гугле сторонний проект.


  1. ky0
    13.06.2024 06:52

    Абстрактное "изменение лицензии" - так себе причина для миграции. Вот бы вы ещё рассказали, каковы риски использования Терраформа, если вы не предоставляете коммерческих сервисов на его основе... (ответ: никаких)


    1. TimurTukaev
      13.06.2024 06:52
      +11

      Компания уже один раз изменила лицензию — что мешает сменить ее снова? Теперь уже на полностью проприетарную и платную. Вот и первый риск. Второй риск проще и понятнее — за OpenTofu теперь стоит Linux Foundation, соответственно, проект 100% будет развиваться и постепенно может стать стандартом индустрии (в том числе и за счет компаний, которые предоставляют на его основе коммерческие сервисы). То есть потенциально Terraform может со временем превратиться в нишевое решение и перестать быть рыночным стандартом. Насколько это риски лично для вас — другой вопрос. Для кого-то это весомая причина. Ну и как минимум интересно было посмотреть, чем эти проекты отличаются сейчас и насколько легко можно мигрировать с одного на другой.