OpenTofu使ってみた

※この記事は3-shake Advent Calendar 2023の21日目の記事です

# OpenTofuとは？
OpenTofuはTerraformからforkされた、OSSのインフラ構成管理ツールです。
Linux Foundation傘下のプロジェクトとして、MPL2ライセンスで正式リリースに向け開発が進められています。
※OpenTofuが生まれた背景に関してはこの記事では触れません。
※1/10に正式リリースされる予定みたいです。

https://twitter.com/linuxfoundation/status/1737310605818622356?ref_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E1737310605818622356%7Ctwgr%5E030aec39c1140f213f20bf89a7e1ff79af476556%7Ctwcon%5Es1_c10&ref_url=https%3A%2F%2Fgihyo.jp%2Farticle%2F2023%2F12%2Fdaily-linux-231221

公式サイト: https://opentofu.org
リポジトリ: https://github.com/opentofu/opentofu


# 使ってみる
今回は、terraformのチュートリアルにあるDockerでNginxを動かす例にOpenTofuを使用してみます。

https://developer.hashicorp.com/terraform/tutorials/docker-get-started
## インストール(MacOS)

```
$ brew install opentofu

$ tofu -v                                              
OpenTofu v1.6.0-rc1
on darwin_arm64
```
https://opentofu.org/docs/intro/install/
## 1. main.tfの作成
```terraform:main.tf
terraform {
  required_providers {
    docker = {
      source  = "kreuzwerker/docker"
      version = "3.0.2"
    }
  }
}

provider "docker" {}

resource "docker_image" "nginx" {
  name         = "nginx:latest"
  keep_locally = false
}

resource "docker_container" "nginx" {
  image = docker_image.nginx.image_id
  name  = "sample"
  ports {
    internal = 80
    external = 8000
  }
}
```

## 2. `tofu init` 
`terraform init`同様、`.terraform.lock.hcl`と`.terraform`が作成される。
```zsh 
$ tofu init                                                

Initializing the backend...

Initializing provider plugins...
- Finding kreuzwerker/docker versions matching "~> 3.0.1"...
- Installing kreuzwerker/docker v3.0.2...
- Installed kreuzwerker/docker v3.0.2. Signature validation was skipped due to the registry not containing GPG keys for this provider

OpenTofu 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 OpenTofu can guarantee to make the same selections by default when
you run "tofu init" in the future.

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


## 3. 差分確認
terraform同様、`tofu plan`で差分確認ができます。

```zsh
$ tofu plan                                                                       

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

OpenTofu will perform the following actions:

  # docker_container.nginx will be created
  + resource "docker_container" "nginx" {
      + attach                                      = false
      + bridge                                      = (known after apply)
      + command                                     = (known after apply)
      + container_logs                              = (known after apply)
      + container_read_refresh_timeout_milliseconds = 15000
      + entrypoint                                  = (known after apply)
      + env                                         = (known after apply)
      + exit_code                                   = (known after apply)
      + hostname                                    = (known after apply)
      + id                                          = (known after apply)
      + image                                       = (known after apply)
      + init                                        = (known after apply)
      + ipc_mode                                    = (known after apply)
      + log_driver                                  = (known after apply)
      + logs                                        = false
      + must_run                                    = true
      + name                                        = "sample"
      + network_data                                = (known after apply)
      + read_only                                   = false
      + remove_volumes                              = true
      + restart                                     = "no"
      + rm                                          = false
      + runtime                                     = (known after apply)
      + security_opts                               = (known after apply)
      + shm_size                                    = (known after apply)
      + start                                       = true
      + stdin_open                                  = false
      + stop_signal                                 = (known after apply)
      + stop_timeout                                = (known after apply)
      + tty                                         = false
      + wait                                        = false
      + wait_timeout                                = 60

      + ports {
          + external = 8000
          + internal = 80
          + ip       = "0.0.0.0"
          + protocol = "tcp"
        }
    }

  # docker_image.nginx will be created
  + resource "docker_image" "nginx" {
      + id           = (known after apply)
      + image_id     = (known after apply)
      + keep_locally = false
      + name         = "nginx:latest"
      + repo_digest  = (known after apply)
    }

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

────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so OpenTofu can't guarantee to take exactly
these actions if you run "tofu apply" now.
```



## 4. リソース適用
terraform同様、`tofu apply`で適用ができます。
```zsh
$ tofu apply                                                                      
OpenTofu used the selected providers to generate the following execution plan. Resource actions are
indicated with the following symbols:
  + create

OpenTofu will perform the following actions:

  # docker_container.nginx will be created
  + resource "docker_container" "nginx" {
      + attach                                      = false
      + bridge                                      = (known after apply)
      + command                                     = (known after apply)
      + container_logs                              = (known after apply)
      + container_read_refresh_timeout_milliseconds = 15000
      + entrypoint                                  = (known after apply)
      + env                                         = (known after apply)
      + exit_code                                   = (known after apply)
      + hostname                                    = (known after apply)
      + id                                          = (known after apply)
      + image                                       = (known after apply)
      + init                                        = (known after apply)
      + ipc_mode                                    = (known after apply)
      + log_driver                                  = (known after apply)
      + logs                                        = false
      + must_run                                    = true
      + name                                        = "sample"
      + network_data                                = (known after apply)
      + read_only                                   = false
      + remove_volumes                              = true
      + restart                                     = "no"
      + rm                                          = false
      + runtime                                     = (known after apply)
      + security_opts                               = (known after apply)
      + shm_size                                    = (known after apply)
      + start                                       = true
      + stdin_open                                  = false
      + stop_signal                                 = (known after apply)
      + stop_timeout                                = (known after apply)
      + tty                                         = false
      + wait                                        = false
      + wait_timeout                                = 60

      + ports {
          + external = 8000
          + internal = 80
          + ip       = "0.0.0.0"
          + protocol = "tcp"
        }
    }

  # docker_image.nginx will be created
  + resource "docker_image" "nginx" {
      + id           = (known after apply)
      + image_id     = (known after apply)
      + keep_locally = false
      + name         = "nginx:latest"
      + repo_digest  = (known after apply)
    }

Plan: 2 to add, 0 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

docker_image.nginx: Creating...
docker_image.nginx: Still creating... [10s elapsed]
docker_image.nginx: Creation complete after 16s [id=sha256:8aea65d81da202cf886d7766c7f2691bb9e363c6b5d9b1f5d9ddaaa4bc1e90c2nginx:latest]
docker_container.nginx: Creating...
docker_container.nginx: Creation complete after 1s [id=7d8166b585c6307a8c2ad3df5b422720a414bf29d12353348ce8bbd06e85677a]

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

これで、nginxコンテナが作成されたことが確認できました。

![スクリーンショット 2023-12-21 16.34.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988422/356a96a7-a471-531a-222d-d40496a736eb.png)

## 5. リソース削除
これもterraform同様、`tofu destroy`で可能です。
```zsh
$ tofu destroy                                                                    
docker_image.nginx: Refreshing state... [id=sha256:8aea65d81da202cf886d7766c7f2691bb9e363c6b5d9b1f5d9ddaaa4bc1e90c2nginx:latest]
docker_container.nginx: Refreshing state... [id=7d8166b585c6307a8c2ad3df5b422720a414bf29d12353348ce8bbd06e85677a]

OpenTofu used the selected providers to generate the following execution plan. Resource actions are
indicated with the following symbols:
  - destroy

OpenTofu will perform the following actions:

  # docker_container.nginx will be destroyed
  - resource "docker_container" "nginx" {
      - attach                                      = false -> null
      - command                                     = [
          - "nginx",
          - "-g",
          - "daemon off;",
        ] -> null
      - container_read_refresh_timeout_milliseconds = 15000 -> null
      - cpu_shares                                  = 0 -> null
      - dns                                         = [] -> null
      - dns_opts                                    = [] -> null
      - dns_search                                  = [] -> null
      - entrypoint                                  = [
          - "/docker-entrypoint.sh",
        ] -> null
      - env                                         = [] -> null
      - group_add                                   = [] -> null
      - hostname                                    = "" -> null
      - id                                          = "7d8166b585c6307a8c2ad3df5b422720a414bf29d12353348ce8bbd06e85677a" -> null
      - image                                       = "sha256:8aea65d81da202cf886d7766c7f2691bb9e363c6b5d9b1f5d9ddaaa4bc1e90c2" -> null
      - init                                        = false -> null
      - ipc_mode                                    = "private" -> null
      - log_driver                                  = "json-file" -> null
      - log_opts                                    = {} -> null
      - logs                                        = false -> null
      - max_retry_count                             = 0 -> null
      - memory                                      = 0 -> null
      - memory_swap                                 = 0 -> null
      - must_run                                    = true -> null
      - name                                        = "sample" -> null
      - network_data                                = [
          - {
              - gateway                   = "172.17.0.1"
              - global_ipv6_address       = ""
              - global_ipv6_prefix_length = 0
              - ip_address                = "172.17.0.2"
              - ip_prefix_length          = 16
              - ipv6_gateway              = ""
              - mac_address               = ""
              - network_name              = "bridge"
            },
        ] -> null
      - network_mode                                = "default" -> null
      - privileged                                  = false -> null
      - publish_all_ports                           = false -> null
      - read_only                                   = false -> null
      - remove_volumes                              = true -> null
      - restart                                     = "no" -> null
      - rm                                          = false -> null
      - runtime                                     = "runc" -> null
      - security_opts                               = [] -> null
      - shm_size                                    = 64 -> null
      - start                                       = true -> null
      - stdin_open                                  = false -> null
      - stop_signal                                 = "SIGQUIT" -> null
      - stop_timeout                                = 0 -> null
      - storage_opts                                = {} -> null
      - sysctls                                     = {} -> null
      - tmpfs                                       = {} -> null
      - tty                                         = false -> null
      - wait                                        = false -> null
      - wait_timeout                                = 60 -> null

      - ports {
          - external = 8000 -> null
          - internal = 80 -> null
          - ip       = "0.0.0.0" -> null
          - protocol = "tcp" -> null
        }
    }

  # docker_image.nginx will be destroyed
  - resource "docker_image" "nginx" {
      - id           = "sha256:8aea65d81da202cf886d7766c7f2691bb9e363c6b5d9b1f5d9ddaaa4bc1e90c2nginx:latest" -> null
      - image_id     = "sha256:8aea65d81da202cf886d7766c7f2691bb9e363c6b5d9b1f5d9ddaaa4bc1e90c2" -> null
      - keep_locally = false -> null
      - name         = "nginx:latest" -> null
      - repo_digest  = "nginx@sha256:bd30b8d47b230de52431cc71c5cce149b8d5d4c87c204902acf2504435d4b4c9" -> null
    }

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

Do you really want to destroy all resources?
  OpenTofu will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: yes

docker_container.nginx: Destroying... [id=7d8166b585c6307a8c2ad3df5b422720a414bf29d12353348ce8bbd06e85677a]
docker_container.nginx: Destruction complete after 1s
docker_image.nginx: Destroying... [id=sha256:8aea65d81da202cf886d7766c7f2691bb9e363c6b5d9b1f5d9ddaaa4bc1e90c2nginx:latest]
docker_image.nginx: Destruction complete after 0s

Destroy complete! Resources: 2 destroyed.
```

# Terraformからの移行
OpenTofuでは既存のTerraformのtfファイルやtfstateがそのまま使えます。
手順としては、stateのバックアップを取りつつ、OpenTofuでのinitをし、差分が出ないか確認するだけのようです。
今のところ、移行は簡単ですが、開発が進むにつれ、新機能の追加、Terraform ProviderやRegistryとの互換性が維持できなくなる可能性は十分ありそうです。

https://opentofu.org/docs/intro/migration/




# おわりに
TerraformのforkであるOpenTofuについて紹介しました。
Terraformとの使用感の差はなく、移行も簡単にできそうです。
今後のIaCの選択肢の一つにOpenTofuが入ってくるのかどうなのか、注目したいと思います。