Install and Use Terraform on Linux
Install Terraform on Debian, Fedora, and Arch Linux, configure AWS, Hetzner, and DigitalOcean providers, and manage infrastructure with plan, apply, and remote state backends.
Before you start
- ▸A user account with sudo privileges
- ▸API credentials or access keys for your chosen cloud provider
- ▸curl and gnupg installed (Debian/Ubuntu only)
- ▸Basic familiarity with the Linux command line
Terraform is HashiCorp's infrastructure-as-code tool that lets you provision, change, and version infrastructure across dozens of cloud providers using declarative configuration files. This guide covers installing Terraform on the major Linux families, writing a real provider configuration, understanding the plan/apply workflow, and setting up remote state so your team shares a single source of truth.
Install Terraform
HashiCorp maintains official repositories for Debian/Ubuntu and Fedora/RHEL. Arch users get Terraform from the community repo. Always prefer the official package over a manually downloaded binary so you get automatic updates.
Debian / Ubuntu
sudo apt update && sudo apt install -y gnupg software-properties-common curl
curl -fsSL https://apt.releases.hashicorp.com/gpg | \
sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
https://apt.releases.hashicorp.com $(lsb_release -cs) main" | \
sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install -y terraform
Fedora / RHEL / Rocky Linux
sudo dnf install -y dnf-plugins-core
sudo dnf config-manager --add-repo \
https://rpm.releases.hashicorp.com/fedora/hashicorp.repo
sudo dnf install -y terraform
On RHEL 9 / Rocky 9 replace fedora in the repo URL with RHEL/9.
Arch Linux
sudo pacman -S terraform
Verify the installation
terraform version
You should see something like Terraform v1.8.x on linux_amd64. If the command is not found, ensure /usr/bin or /usr/local/bin is in your PATH.
Project Structure and Initialization
Terraform reads all *.tf files in the working directory. A minimal project needs at least a provider block and one resource. Create a fresh directory for each infrastructure project.
mkdir ~/tf-demo && cd ~/tf-demo
Create main.tf with a provider declaration. Examples for three popular providers follow; pick one and delete the others.
AWS provider
cat > main.tf <<'EOF'
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "eu-west-1"
}
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0" # Amazon Linux 2023, eu-west-1
instance_type = "t3.micro"
tags = {
Name = "tf-demo-web"
}
}
EOF
Set AWS credentials via environment variables rather than hard-coding them:
export AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
export AWS_DEFAULT_REGION="eu-west-1"
Hetzner Cloud provider
cat > main.tf <<'EOF'
terraform {
required_providers {
hcloud = {
source = "hetznercloud/hcloud"
version = "~> 1.47"
}
}
}
provider "hcloud" {
token = var.hcloud_token
}
variable "hcloud_token" {
sensitive = true
}
resource "hcloud_server" "web" {
name = "tf-demo-web"
image = "ubuntu-24.04"
server_type = "cx22"
location = "nbg1"
}
EOF
export TF_VAR_hcloud_token="your-hetzner-api-token"
DigitalOcean provider
cat > main.tf <<'EOF'
terraform {
required_providers {
digitalocean = {
source = "digitalocean/digitalocean"
version = "~> 2.0"
}
}
}
provider "digitalocean" {
token = var.do_token
}
variable "do_token" {
sensitive = true
}
resource "digitalocean_droplet" "web" {
name = "tf-demo-web"
region = "fra1"
size = "s-1vcpu-1gb"
image = "ubuntu-24-04-x64"
}
EOF
export TF_VAR_do_token="your-digitalocean-personal-access-token"
Initialize the working directory
terraform init downloads the declared providers into .terraform/ and creates a lock file.
terraform init
You will see Terraform downloading the provider plugin and printing Terraform has been successfully initialized!. The .terraform.lock.hcl file should be committed to version control; the .terraform/ directory should not.
Plan and Apply
The plan/apply cycle is the core Terraform workflow. Never run apply without reviewing a plan first in a production environment.
terraform plan
plan compares your configuration against the current state and shows exactly what will be created, changed, or destroyed — without touching anything real.
terraform plan
Save the plan to a file to guarantee that exactly this plan is applied and nothing else:
terraform plan -out=tfplan
terraform apply
terraform apply tfplan
When you pass a saved plan file, Terraform skips the confirmation prompt and applies exactly what was planned. Without a plan file, it runs a fresh plan and asks you to type yes.
Inspect and destroy
terraform show
terraform output # prints declared output values
terraform destroy # tears down everything in the state file
destroy always prompts for confirmation. Use -auto-approve only in CI pipelines where you have already validated the plan.
State Backends
By default, Terraform writes state to terraform.tfstate in the local working directory. This works fine for solo work but breaks collaboration: two engineers running apply simultaneously can corrupt state. A remote backend stores state centrally and provides locking.
S3 backend (AWS)
Create the bucket and a DynamoDB table for locking before adding the backend block — Terraform cannot create its own state storage.
aws s3api create-bucket --bucket my-tf-state-bucket \
--region eu-west-1 \
--create-bucket-configuration LocationConstraint=eu-west-1
aws s3api put-bucket-versioning --bucket my-tf-state-bucket \
--versioning-configuration Status=Enabled
aws dynamodb create-table \
--table-name tf-state-lock \
--attribute-definitions AttributeName=LockID,AttributeType=S \
--key-schema AttributeName=LockID,KeyType=HASH \
--billing-mode PAY_PER_REQUEST
Add the backend configuration to your main.tf (or a dedicated backend.tf):
cat > backend.tf <<'EOF'
terraform {
backend "s3" {
bucket = "my-tf-state-bucket"
key = "demo/terraform.tfstate"
region = "eu-west-1"
dynamodb_table = "tf-state-lock"
encrypt = true
}
}
EOF
terraform init # re-run to migrate local state to S3
HTTP backend (self-hosted / Hetzner / DigitalOcean)
If you are not on AWS, GitLab provides a built-in Terraform HTTP state backend at no extra cost. Configure it under Settings → Terraform states in your GitLab project and use the provided snippet, or use any S3-compatible object storage (Hetzner Object Storage, DigitalOcean Spaces) with the s3 backend by setting endpoint, skip_credentials_validation, and skip_metadata_api_check.
cat > backend.tf <<'EOF'
terraform {
backend "s3" {
endpoint = "https://nbg1.your-objectstorage.com"
bucket = "my-tf-state"
key = "demo/terraform.tfstate"
region = "main"
skip_credentials_validation = true
skip_metadata_api_check = true
skip_region_validation = true
force_path_style = true
encrypt = true
}
}
EOF
Verification
After applying, confirm Terraform recorded the resource in state:
terraform state list
Output will be similar to aws_instance.web or hcloud_server.web. For a remote backend, also verify the state file exists in the bucket:
aws s3 ls s3://my-tf-state-bucket/demo/
Troubleshooting
- Provider authentication errors: Double-check environment variables are exported in the same shell session. Use
printenv | grep AWSorprintenv | grep TF_VARto confirm. - State lock not released: If a previous run crashed, run
terraform force-unlock <LOCK_ID>. The lock ID appears in the error message. Only do this when you are certain no other process is running. - Version conflicts: If a colleague has a different provider version, run
terraform init -upgradeto update the lock file, then commit it. - Plan shows unexpected destruction: Usually caused by a changed required field (e.g.,
regionorimage) that forces resource replacement. Read the plan output carefully; look for lines marked-/+(destroy then create). - terraform: command not found after install: Run
hash -r(bash) orrehash(zsh) to refresh the shell's command cache.
Frequently asked questions
- Should I commit the terraform.tfstate file to Git?
- No. Local state files often contain sensitive data such as passwords and private IPs in plain text. Use a remote backend and add terraform.tfstate and terraform.tfstate.backup to .gitignore.
- What is the difference between terraform plan and terraform apply?
- plan is a dry run that shows what Terraform would do without making any changes. apply executes those changes against the real infrastructure. Always review a plan before applying in production.
- Can I use Terraform with multiple providers in one project?
- Yes. You can declare multiple provider blocks, including two instances of the same provider with different aliases (e.g., two AWS regions). Each resource then references the correct provider via the provider meta-argument.
- How do I import existing infrastructure into Terraform state?
- Use terraform import <resource_type>.<name> <provider_id> to bring an existing resource under Terraform management. You still need to write the matching resource block in your configuration manually.
- Is Terraform 1.x free to use?
- The open-source Terraform CLI is free under the Business Source License (BSL 1.1) as of version 1.6. For purely open-source workflows, OpenTofu is a CNCF-backed fork under the MPL 2.0 license and is a drop-in replacement.
Related guides
Configure Prometheus Alertmanager
Configure Prometheus Alertmanager with routing trees, receivers, inhibition rules, grouping, Go templates, and PagerDuty/Slack on-call integrations.
Build an Intranet Server on Linux
Set up a complete small-office intranet on one Linux box: Nginx web server, dnsmasq local DNS, Samba file sharing, and a Wiki.js team wiki.
Build an nftables Firewall Script
Build a complete nftables firewall from scratch: tables, chains, sets, default-deny input policy, service allowlisting, and persistent systemd configuration.
Caddy as a Reverse Proxy
Set up Caddy as a reverse proxy with automatic HTTPS, load balancing, WebSocket passthrough, reusable snippets, and header control — no certbot required.