$linuxjunkies
>

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.

IntermediateUbuntuDebianFedoraArch10 min readUpdated June 7, 2026

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 AWS or printenv | grep TF_VAR to 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 -upgrade to update the lock file, then commit it.
  • Plan shows unexpected destruction: Usually caused by a changed required field (e.g., region or image) 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) or rehash (zsh) to refresh the shell's command cache.
tested on:Ubuntu 24.04Debian 12Fedora 40Arch rolling

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