OpenTofu Tutorial — Part 2: Your First OpenTofu Project: init, plan, apply and destroy

Time to build your first OpenTofu project. You’ll write one small block of code, and OpenTofu will create a real web page from it: the home page of the Bookshop website you’ll grow through this series.

By the end of this part, you’ll have used the four commands every OpenTofu project uses, init, plan, apply and destroy, to create a web page, change it and remove it again.

This is Part 2 of the OpenTofu Tutorial. The parts in Module A, Start Here:

All parts of the OpenTofu Tutorial

Module A: Start Here

Module B: Write Configuration

  • Part 4: Variables, tfvars and Outputs
  • Part 5: Locals, Expressions and tofu console
  • Part 6: Functions
  • Part 7: Data Sources
  • Part 8: count, for_each and enabled
  • Part 9: Dependencies, Networks, Volumes, Dynamic Blocks and the Graph
  • Part 10: Lifecycle Rules
  • Part 11: Validation, Conditions and Check Blocks

Module C: Add a Database

  • Part 12: PostgreSQL in Docker, Managed with OpenTofu

Module D: Modules

  • Part 13: Your First Module
  • Part 14: Module Sources and Versions
  • Part 15: OCI Registries for Modules and Providers

Module E: State

  • Part 16: Inside OpenTofu State
  • Part 17: Refactor Safely: moved, removed, state mv and rm
  • Part 18: Import Existing Infrastructure
  • Part 19: Remote State in PostgreSQL with Locking
  • Part 20: S3 Backend with Native Locking
  • Part 21: Encrypt State and Plan Files
  • Part 22: Workspaces and Sharing Outputs

Module F: Secrets

  • Part 23: Ephemeral Values and Write-Only Attributes
  • Part 24: Secrets with OpenBao

Module G: Operations

  • Part 25: Plan and Apply Options
  • Part 26: CLI Configuration and Provider Installation
  • Part 27: Logs and Tracing
  • Part 28: Provisioners and terraform_data

Module H: Test and Team

  • Part 29: Test Your Configuration with tofu test
  • Part 30: Mock Providers and Overrides
  • Part 31: Multiple Provider Instances
  • Part 32: Manage GitHub with OpenTofu
  • Part 33: GitHub Actions CI/CD

Module I: Kubernetes

  • Part 34: Kubernetes for Free with kind
  • Part 35: Helm Charts and Kubernetes Secrets

Module J: Azure for Free

  • Part 36: Deploy to Azure for Free: Resource Group and App Service F1
  • Part 37: Azure Static Web Apps Free and Clean Up

Module K: Finish

  • Part 38: Migrate from Terraform to OpenTofu
  • Part 39: Final Project, Cheat Sheet and What to Learn Next

Cost: free. Everything in this part runs on your own computer, and you don’t need an account.

Every command and output on this page is real, run on OpenTofu 1.13 on macOS. The commands are the same in Windows PowerShell and on Linux.

Prerequisites

Step 1: Create a Folder for Your First OpenTofu Project

Every OpenTofu project lives in its own folder. Open a terminal and run these four commands. They create a tofu-lab folder for this series, a bookshop folder inside it, and move you into it. They work the same in Terminal and PowerShell.

mkdir tofu-lab
cd tofu-lab
mkdir bookshop
cd bookshop

Run every command in this part from the bookshop folder.

Step 2: Write Your First Configuration

In your editor, create a file named main.tf in the bookshop folder, paste this block into it and save it. OpenTofu reads every file that ends in .tf in the folder.

A code window titled main.tf with one OpenTofu resource block of type local_file named home_page, which sets filename to index.html and content to an h1 heading that says Bookshop.
Your first configuration: one resource block in main.tf.
resource "local_file" "home_page" {
  filename = "index.html"
  content  = "<h1>Bookshop</h1>"
}

Here’s what each part means:

  • resource starts a block that describes one thing OpenTofu should create.
  • "local_file" is the resource type: a file on your computer. It comes from the local provider.
  • "home_page" is the name you give it. Together, local_file.home_page is its address, and OpenTofu uses it in every message about this file.
  • filename is where the file goes. A plain name puts it in the folder you run OpenTofu from.
  • content is what goes inside the file: one HTML heading.

Step 3: Initialize the Project with tofu init

tofu init prepares the folder. It reads your configuration, sees that it needs the local provider, and downloads it:

tofu init
Initializing the backend...

Initializing provider plugins...
- Finding latest version of hashicorp/local...
- Installing hashicorp/local v2.9.1...
- Installed hashicorp/local v2.9.1 (signed, key ID 0C0AF313E5FD9F80)

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

Two new things are now in the folder. The .terraform folder holds the provider OpenTofu downloaded, and .terraform.lock.hcl records its exact version, so the project gets the same one next time. Run tofu init once in every new project, and again whenever you add a provider. It’s always safe to run it again.

Step 4: Check the File with tofu fmt and tofu validate

Two quick checks catch mistakes before OpenTofu changes anything. tofu fmt tidies the layout of your .tf files, such as lining up the = signs:

tofu fmt

It prints the name of each file it changes. Here it printed nothing, because the block was already tidy.

tofu validate checks that the configuration makes sense, for example that every setting name exists:

tofu validate
Success! The configuration is valid.

Step 5: Preview the Change with tofu plan

tofu plan shows what OpenTofu would do, without doing it:

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:

  # local_file.home_page will be created
  + resource "local_file" "home_page" {
      + content              = "<h1>Bookshop</h1>"
      + content_base64sha256 = (known after apply)
      + content_base64sha512 = (known after apply)
      + content_md5          = (known after apply)
      + content_sha1         = (known after apply)
      + content_sha256       = (known after apply)
      + content_sha512       = (known after apply)
      + directory_permission = "0777"
      + file_permission      = "0777"
      + filename             = "index.html"
      + id                   = (known after apply)
    }

Plan: 1 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.

Read it from the top:

  • The + symbol means create. OpenTofu will create local_file.home_page with your content.
  • Values marked (known after apply), such as the file’s checksums, are worked out once the file exists. The two "0777" permissions are the provider’s defaults; your computer’s own settings still apply on top of them.
  • The summary line, Plan: 1 to add, 0 to change, 0 to destroy., is the line to check every time.
  • The note at the end is about saving a plan to a file. You can ignore it for now.

Step 6: Create the Page with tofu apply

tofu apply shows the same plan again and asks before it changes anything. When it asks, type yes and press Return (Enter on Windows and Linux):

tofu apply
A Terminal window at the end of tofu apply: the plan summary says 1 to add, the prompt Enter a value is answered with yes, local_file.home_page is created, and the last line says Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
tofu apply asks for yes, then creates the page.
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:

  # local_file.home_page will be created
  + resource "local_file" "home_page" {
      + content              = "<h1>Bookshop</h1>"
      + content_base64sha256 = (known after apply)
      + content_base64sha512 = (known after apply)
      + content_md5          = (known after apply)
      + content_sha1         = (known after apply)
      + content_sha256       = (known after apply)
      + content_sha512       = (known after apply)
      + directory_permission = "0777"
      + file_permission      = "0777"
      + filename             = "index.html"
      + id                   = (known after apply)
    }

Plan: 1 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

local_file.home_page: Creating...
local_file.home_page: Creation complete after 0s [id=c0b605bc039c5f97ef612cd50f8e285306aea74f]

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

Apply complete! means the file now exists. Open the bookshop folder in Finder (macOS), File Explorer (Windows) or your Linux file manager, and double-click index.html. It opens in your web browser:

A web browser showing the index.html page that OpenTofu created, with one large heading that says Bookshop.
The page OpenTofu created, opened in a browser.

OpenTofu built that page from the four lines you wrote.

Step 7: See What OpenTofu Remembers

After the apply, a new file appeared in the folder: terraform.tfstate. This is OpenTofu’s state, its record of everything it created, so next time it can compare what exists with what your configuration asks for. To list what it holds:

tofu state list
local_file.home_page

Two rules from day one. Never edit terraform.tfstate by hand: OpenTofu’s documentation discourages it, and tofu commands change it safely. And never share it, because state can hold sensitive data such as passwords. Step 9 keeps it out of Git.

Step 8: Change the Page

With infrastructure as code, you change things by changing the file. In main.tf, change the content line to this and save the file:

  content  = "<h1>Welcome to the Bookshop</h1>"

Run tofu apply again and type yes:

tofu apply
local_file.home_page: Refreshing state... [id=c0b605bc039c5f97ef612cd50f8e285306aea74f]

OpenTofu used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement

OpenTofu will perform the following actions:

  # local_file.home_page must be replaced
-/+ resource "local_file" "home_page" {
      ~ content              = "<h1>Bookshop</h1>" -> "<h1>Welcome to the Bookshop</h1>" # forces replacement
      ~ content_base64sha256 = "+5sTgVHJocmrr02gbEGM4VmZc6Dc2n0A0CHFa1Kr2js=" -> (known after apply)
      ~ content_base64sha512 = "geSAoyg3EZpsjVqNqLLJOy38aTEed/kD6zxHGHWSx/sbGZgWxo2NPAuI+sdB3FGlL2UQHlLzH5cZ99r3gqaZQA==" -> (known after apply)
      ~ content_md5          = "d9c651a380dd05647dd51dd41330682d" -> (known after apply)
      ~ content_sha1         = "c0b605bc039c5f97ef612cd50f8e285306aea74f" -> (known after apply)
      ~ content_sha256       = "fb9b138151c9a1c9abaf4da06c418ce1599973a0dcda7d00d021c56b52abda3b" -> (known after apply)
      ~ content_sha512       = "81e480a32837119a6c8d5a8da8b2c93b2dfc69311e77f903eb3c47187592c7fb1b199816c68d8d3c0b88fac741dc51a52f65101e52f31f9719f7daf782a69940" -> (known after apply)
      ~ id                   = "c0b605bc039c5f97ef612cd50f8e285306aea74f" -> (known after apply)
        # (3 unchanged attributes hidden)
    }

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

local_file.home_page: Destroying... [id=c0b605bc039c5f97ef612cd50f8e285306aea74f]
local_file.home_page: Destruction complete after 0s
local_file.home_page: Creating...
local_file.home_page: Creation complete after 0s [id=3964b5d99b456322e0ab0fe000a3113843d01874]

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

This time the plan shows -/+, which means destroy and then create replacement. The # forces replacement note shows why: for a local_file, new content means a new file. The ~ symbol marks each value that changes. Refresh the page in your browser:

The same browser page after the change, now with the heading Welcome to the Bookshop.
After the change, the page shows the new heading.

Step 9: Keep State Out of Git

Later in this series you’ll keep projects in Git. Some files must never go into it: the .terraform folder, because tofu init can always download it again, and the state files, because they can hold sensitive data. In your editor, create a file named .gitignore in the bookshop folder with these three lines:

.terraform/
*.tfstate
*.tfstate.*
  • .terraform/ leaves out the downloaded providers.
  • *.tfstate leaves out terraform.tfstate.
  • *.tfstate.* leaves out terraform.tfstate.backup, the copy of the previous state that OpenTofu keeps.

Keep .terraform.lock.hcl in Git. The tofu init output asked for that, so everyone who uses the project gets the same provider version.

Clean Up

When you’re done, remove everything this project created. tofu destroy shows what it will remove and asks first; type yes:

tofu destroy
local_file.home_page: Refreshing state... [id=3964b5d99b456322e0ab0fe000a3113843d01874]

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:

  # local_file.home_page will be destroyed
  - resource "local_file" "home_page" {
      - content              = "<h1>Welcome to the Bookshop</h1>" -> null
      - content_base64sha256 = "CU/1o1AFagdnJfGiIuwV9lXSL3hXN3JluMO+cWzmsxk=" -> null
      - content_base64sha512 = "bv2MB991RPHo2VZ+W328mj3tAJvywHbpXcdeUfheLZw+D3X2drxWTxGoxtJC/H9pknChV2SU7TTHL7wX7vN1iw==" -> null
      - content_md5          = "70e9b16b69a3e01a3a6f5c79b4ff0e82" -> null
      - content_sha1         = "3964b5d99b456322e0ab0fe000a3113843d01874" -> null
      - content_sha256       = "094ff5a350056a076725f1a222ec15f655d22f7857377265b8c3be716ce6b319" -> null
      - content_sha512       = "6efd8c07df7544f1e8d9567e5b7dbc9a3ded009bf2c076e95dc75e51f85e2d9c3e0f75f676bc564f11a8c6d242fc7f699270a1576494ed34c72fbc17eef3758b" -> null
      - directory_permission = "0777" -> null
      - file_permission      = "0777" -> null
      - filename             = "index.html" -> null
      - id                   = "3964b5d99b456322e0ab0fe000a3113843d01874" -> null
    }

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

local_file.home_page: Destroying... [id=3964b5d99b456322e0ab0fe000a3113843d01874]
local_file.home_page: Destruction complete after 0s

Destroy complete! Resources: 1 destroyed.

The - symbol means destroy. Destroy complete! means index.html is gone. Your main.tf stays, so tofu apply can build the page again at any time. Part 3 starts from this folder.

Common Mistakes

MistakeWhat happensDo this instead
Running tofu plan before tofu initOpenTofu stops with Error: Inconsistent dependency lock file and tells you to run tofu init.Run tofu init first in every new project.
Typing y at the promptOpenTofu prints Apply cancelled. and changes nothing.Type the whole word yes.
Running commands from the wrong folderOpenTofu stops with Error: No configuration files.Move into the bookshop folder with cd, then run the command again.
Editing terraform.tfstate by handOpenTofu can lose track of what it created.Change the .tf file and run tofu apply.

Troubleshooting

ProblemWhyFix
Error: No configuration filesThere’s no file ending in .tf in the folder you’re in, or the file was saved with another ending, such as main.tf.txt.Check that you’re in the bookshop folder and that the file is named exactly main.tf.
Error: Inconsistent dependency lock fileThe project hasn’t been initialized yet.Run tofu init, then run your command again.
The terminal says tofu isn’t foundOpenTofu isn’t installed, or the terminal can’t find it.Install it with the guide for your computer, then open a new terminal window.
index.html opens in your editor instead of the browserYour computer opens .html files with the editor.Drag index.html onto an open browser window.

FAQ

Do I have to run tofu plan before tofu apply?

No. tofu apply shows the same plan and asks before it changes anything. tofu plan is useful when you only want to look.

What is the .terraform folder?

It’s where tofu init keeps the providers it downloads. tofu init can always download them again, so the folder never goes into Git.

Why are the files called terraform.tfstate if this is OpenTofu?

OpenTofu started as a fork of Terraform and kept its file names, which makes it easier to move existing projects across. terraform.tfstate is OpenTofu’s default name for the state file.

Where does OpenTofu put index.html?

In the folder you run tofu from, because filename is a plain name. A path such as site/index.html would put it in a site folder inside the project.

Does this cost anything?

No. OpenTofu and the local provider are free, and everything stays on your computer.

Conclusion

You’ve built your first OpenTofu project. You wrote a resource block, ran tofu init to download its provider, checked the file with tofu fmt and tofu validate, previewed the change with tofu plan, built the page with tofu apply, changed it by editing the file, and removed it with tofu destroy. Every OpenTofu project works this way, from one file to hundreds of cloud resources.

← Previous: Part 1: What Is OpenTofu? Infrastructure as Code Explained

Coming next: Part 3: Providers: Run the Website in Docker.

Additional Resources

OpenTofu® is a registered trademark of LF Projects, LLC. Terraform and other names mentioned are trademarks of their respective owners. JD Bots isn’t affiliated with or endorsed by the OpenTofu project or HashiCorp.

Leave a Reply

Up ↑