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:
- Part 1: What Is OpenTofu? Infrastructure as Code Explained
- Part 2: Your First OpenTofu Project: init, plan, apply and destroy (you are here)
- Part 3: Providers: Run the Website in Docker
All parts of the OpenTofu Tutorial
Module A: Start Here
- Part 1: What Is OpenTofu? Infrastructure as Code Explained
- Part 2: Your First OpenTofu Project: init, plan, apply and destroy
- Part 3: Providers: Run the Website in Docker
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
- OpenTofu installed. Follow How to Install OpenTofu on macOS; the guides for Windows 11 and Ubuntu are coming next. Running
tofu -versionin a terminal should print a version number. - A terminal: Terminal on macOS and Linux, or PowerShell on Windows.
- A text editor for one small file. Visual Studio Code with the official OpenTofu extension is a good free choice.
- A web browser, to look at the page you build.
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.
resource "local_file" "home_page" {
filename = "index.html"
content = "<h1>Bookshop</h1>"
}
Here’s what each part means:
resourcestarts a block that describes one thing OpenTofu should create."local_file"is the resource type: a file on your computer. It comes from thelocalprovider."home_page"is the name you give it. Together,local_file.home_pageis its address, and OpenTofu uses it in every message about this file.filenameis where the file goes. A plain name puts it in the folder you run OpenTofu from.contentis 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 createlocal_file.home_pagewith 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
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:
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:
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.*.tfstateleaves outterraform.tfstate.*.tfstate.*leaves outterraform.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
| Mistake | What happens | Do this instead |
|---|---|---|
Running tofu plan before tofu init | OpenTofu 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 prompt | OpenTofu prints Apply cancelled. and changes nothing. | Type the whole word yes. |
| Running commands from the wrong folder | OpenTofu stops with Error: No configuration files. | Move into the bookshop folder with cd, then run the command again. |
Editing terraform.tfstate by hand | OpenTofu can lose track of what it created. | Change the .tf file and run tofu apply. |
Troubleshooting
| Problem | Why | Fix |
|---|---|---|
Error: No configuration files | There’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 file | The project hasn’t been initialized yet. | Run tofu init, then run your command again. |
The terminal says tofu isn’t found | OpenTofu 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 browser | Your 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
- The Core OpenTofu Workflow (OpenTofu docs)
- Command: init
- Command: fmt
- Command: validate
- Command: plan
- Command: apply
- Command: destroy
- Command: state list
- State and Sensitive Data in State
- Dependency Lock File
- local_file resource (OpenTofu Registry)
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.
