What is infrastructure as code?
Simply put, infrastructure as code (IaC) means using declarative configurations represented in source code to manage infrastructure rather than doing each step manually. One benefit of managing infrastructure this way is idempotency. Idempotency in this context means that the user can apply an OpenTofu plan as many times as needed and changes will only occur when the target systems don’t match the described state.
Another benefit to managing infrastructure this way is that it gives us the ability to store our configurations in a version control system such as Git. Version control gives us the ability to audit our source code, roll back changes when needed, and can enhance reproducibility and collaboration.
Installation
We’ll be using OpenTofu to provision our server infrastructure. If you haven’t installed it yet, OpenTofu has packages available for all major Linux distributions, as well as WSL, FreeBSD, and MacOS. Refer to the official OpenTofu documentation for installation instructions before continuing.
A word on OpenTofu providers and hosting
OpenTofu uses something called providers to access APIs for various cloud hosting providers. We’ll be using Hetzner to host our server. A couple of other popular choices are Vultr and DigitalOcean. A searchable list of additional providers is available in the OpenTofu provider registry. OpenTofu uses an API key from your hosting provider to configure your infrastructure. Remember to follow your provider’s instructions for creating one. It’s also a good idea to save the key in a password vault or other protected location.
Defining Your Infrastructure
To define our server we’re going to use three configuration files:
main.tf— the primary configuration file where our infrastructure is definedvariables.tf— holds the input variables we’ll set for our deploymentoutputs.tf— retrieves useful information after provisioning, such as our server’s IP address
OpenTofu comes with a couple of nice built-in tools for formatting its own files. If you suspect a formatting issue you can run tofu fmt and OpenTofu will clean up any formatting issues that it finds.
There is also tofu validate which can be used to check for syntax and logic errors. Most modern text editors also have plugins available to assist with defining your infrastructure. Refer to your text editor’s documentation for details about that.
main.tf
This is our main configuration file for defining our infrastructure. It will tell OpenTofu what to configure and where. Mine looks like:
terraform {
required_providers {
hcloud = {
source = "hetznercloud/hcloud"
version = "~> 1.50"
}
}
}
provider "hcloud" {
token = var.hcloud_token
}
resource "hcloud_server" "main" {
name = "learningunix"
server_type = "cx23"
image = "debian-13"
location = var.location
ssh_keys = [var.ssh_key_name]
}
The terraform block declares the providers your configuration depends on and where to download them from. The required_providers section names each provider — here hcloud is the local name we give the Hetzner provider. source tells OpenTofu where to find it in the provider registry, and version sets a constraint on which versions are acceptable. The ~> operator means “pessimistic constraint” — ~> 1.50 allows any version >= 1.50 and < 2.0, so you get bug fixes automatically but won’t break on a major version change. See the OpenTofu Settings documentation for more details.
The provider block configures the provider itself. For Hetzner, the only required setting is token, which is the API key OpenTofu will use to authenticate with Hetzner’s API. Rather than hardcoding the key here, we reference var.hcloud_token — a variable defined in variables.tf — so the secret stays out of our source code. See the OpenTofu Provider Configuration documentation for more details.
The resource block defines a piece of infrastructure to create. The block type is resource, followed by the resource type (hcloud_server) and a local name (main) used to reference this resource elsewhere in your config. Inside the block, each attribute maps to a setting on the server. See the OpenTofu Resource Blocks documentation for more details.
name— the hostname assigned to the server in Hetznerserver_type— the VM size;cx23is a shared-CPU instance with 2 vCPU and 4 GB RAMimage— the operating system image to installlocation— the Hetzner datacenter regionssh_keys— a list of SSH key names registered in your Hetzner account to install on the server at creation time
variables.tf
variables.tf declares the input variables that main.tf references. Declaring a variable here does not set its value — it defines the variable’s name, an optional description, an optional default, and whether it should be treated as sensitive.
variable "hcloud_token" {
sensitive = true
}
variable "ssh_key_name" {
description = "Name of the SSH key in your Hetzner account"
}
variable "location" {
default = "nbg1"
}
hcloud_token— the Hetzner API key. Markedsensitive = true, which tells OpenTofu to redact its value from plan and apply output.ssh_key_name— the name of the SSH key registered in your Hetzner account that will be installed on the server at creation time.location— the Hetzner datacenter region.nbg1is Nuremberg, Germany. This variable has a default, so it does not need to be set explicitly unless you want a different region.
terraform.tfvars
Actual values are set in a separate file called terraform.tfvars. OpenTofu reads this file automatically when you run tofu plan or tofu apply. A terraform.tfvars file looks like this:
hcloud_token = "your-api-key-here"
ssh_key_name = "your-ssh-key-name"
Keep terraform.tfvars out of version control. It contains potentially sensitive data and should never be committed to a repository.
outputs.tf
outputs.tf defines values that OpenTofu will display after a successful tofu apply. This is useful for retrieving information about the infrastructure that was just created, such as an IP address you’ll need to connect to your server.
output "server_ip" {
value = hcloud_server.main.ipv4_address
}
The output block is named server_ip. The value attribute references the ipv4_address attribute of the hcloud_server resource we defined in main.tf. After applying, OpenTofu will print the server’s public IP address to the terminal.
Applying the Configuration
Before applying the configuration, it’s good practice to format your files. Run tofu fmt, which will clean up any formatting issues and ensure your files conform to HCL style conventions:
tofu fmt
Next, initialize the working directory. This downloads the provider plugins declared in your terraform block and only needs to be run once, or again if you add new providers:
tofu init
Now run tofu validate to check for syntax and logic errors before touching any infrastructure. Note that running tofu validate before tofu init will produce an error — the provider must be downloaded first so OpenTofu has the schema it needs to validate your resource definitions:
tofu validate
Run a plan to preview what OpenTofu intends to do before making any changes:
tofu plan
OpenTofu will display a list of resources it will create, modify, or destroy. Review it carefully — nothing has been changed yet at this point. When you’re satisfied the plan looks correct, apply it:
tofu apply
OpenTofu will display the plan one more time and prompt you to confirm before proceeding. Type yes to continue. When it finishes, the output defined in outputs.tf will be printed to the terminal:
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
server_ip = "1.2.3.4"
You can now use that IP address to connect to your server. Hetzner Debian images use root as the default user:
ssh root@<server_ip>
If your SSH key is not your default key, specify it explicitly with the -i flag:
ssh -i ~/.ssh/your_key root@<server_ip>
Conclusion
At this point we have a running Debian server on Hetzner, provisioned entirely from code. The configuration is reproducible.
If you ever need to rebuild the server, running tofu apply again will produce an identical result.
The sensitive values stay out of your source code, and the infrastructure itself is described in a handful of readable text files.