How to Install Terraform on Ubuntu 24.04 — Infrastructure as Code
If you still create VPS instances, DNS records, and Kubernetes clusters by clicking buttons in a dashboard, you are leaving speed, reproducibility, and sanity on the table. Terraform is the de facto standard for Infrastructure as Code (IaC) — a declarative language that lets you describe your entire stack in text files, version it in Git, review it in pull requests, and roll it out (or roll it back) with a single command.
In this guide you will install Terraform on Ubuntu 24.04 LTS three different ways, understand the 2023 licensing drama that led to the OpenTofu fork, and write your first real main.tf file that provisions actual cloud resources. By the end you will have a working IaC workflow, know how to manage remote state safely, and understand the commands that matter in production.
What Is Terraform?
Terraform is an open-source (well, formerly open-source — more on that in a moment) IaC tool created by HashiCorp in 2014. Instead of writing shell scripts or clicking through cloud consoles, you declare the desired state of your infrastructure in HashiCorp Configuration Language (HCL):
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
tags = {
Name = "production-web"
}
}Terraform compares that desired state against reality (tracked in a state file), calculates the diff, and applies the changes. It works with over 3,000 providers — AWS, Azure, GCP, Contabo, Cloudflare, Kubernetes, GitHub, Datadog, Stripe, and almost every API you can imagine.
Why teams adopt Terraform:
- Reproducibility — the same config spins up identical dev, staging, and prod environments.
- Auditability — every change lives in Git with an author, timestamp, and review trail.
- Disaster recovery — lose a region?
terraform applyrebuilds it in minutes. - Multi-cloud — one language for AWS, GCP, Azure, and everything else.
The BSL License Controversy and the OpenTofu Fork
In August 2023, HashiCorp dropped a bomb on the IaC community: Terraform — along with Vault, Consul, and Nomad — was relicensed from the permissive Mozilla Public License 2.0 (MPL) to the Business Source License (BSL) 1.1. In plain English, the BSL prohibits anyone from offering a competing commercial product built on top of Terraform. It is not an open-source license by OSI standards.
The community revolted. Within weeks, a coalition including Gruntwork, Spacelift, Harness, and Env0 announced OpenTF — a hard fork of the last MPL-licensed Terraform commit. The project was quickly adopted by the Linux Foundation, renamed OpenTofu, and cut its first GA release (1.6.0) in January 2024.
OpenTofu is a drop-in replacement for Terraform. It reads the same HCL, uses the same provider protocol, and supports the same state format. Binaries are named tofu instead of terraform, but most teams alias them.
In April 2025, OpenTofu 1.9 shipped features Terraform still lacks — notably native OCI registry support for modules and providers, early variable evaluation, and provider-defined functions across all blocks.
Which should you pick? If you are starting fresh, OpenTofu is the safer long-term bet: truly open source, Linux Foundation governance, no vendor lock-in. If you rely on HCP Terraform (formerly Terraform Cloud) or specific enterprise integrations, stick with HashiCorp. We cover installing both below.
Prerequisites
- Ubuntu 24.04 LTS (also works on 22.04)
sudoaccess- At least 512 MB of RAM and 1 GB of free disk
- A target cloud account (AWS, GCP, Contabo, Cloudflare — anything with an API)
Method 1: Install Terraform via HashiCorp's Official APT Repository
This is the official HashiCorp-recommended path and the one you want for long-term, patched, apt-managed installs.
Step 1 — Install dependencies:
sudo apt update
sudo apt install -y gnupg software-properties-common curlStep 2 — Add HashiCorp's GPG key:
wget -O- https://apt.releases.hashicorp.com/gpg | \
sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpgStep 3 — Verify the key fingerprint (should be 798A EC65 4E5C 1542 8C8E 42EE AA16 FCBC A621 E701):
gpg --no-default-keyring \
--keyring /usr/share/keyrings/hashicorp-archive-keyring.gpg \
--fingerprintStep 4 — Add the repository:
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.listStep 5 — Install Terraform:
sudo apt update
sudo apt install -y terraformStep 6 — Verify:
terraform versionTerraform v1.9.x
on linux_amd64
Enable tab-completion while you are here:
terraform -install-autocompleteMethod 2: Install OpenTofu (The Linux Foundation Fork)
OpenTofu ships its own apt repo and a convenient installer script.
Option A — One-line installer:
curl --proto '=https' --tlsv1.2 -fsSL \
https://get.opentofu.org/install-opentofu.sh | sh -s -- --install-method debOption B — Manual apt repo:
# Add the GPG keys curl -fsSL https://get.opentofu.org/opentofu.gpg | \ sudo tee /etc/apt/keyrings/opentofu.gpg >/dev/null curl -fsSL https://packages.opentofu.org/opentofu/tofu/gpgkey | \ sudo gpg --no-tty --batch --dearmor -o /etc/apt/keyrings/opentofu-repo.gpg >/dev/nullAdd the repo
echo "deb [signed-by=/etc/apt/keyrings/opentofu.gpg,/etc/apt/keyrings/opentofu-repo.gpg] \ https://packages.opentofu.org/opentofu/tofu/any/ any main" | \ sudo tee /etc/apt/sources.list.d/opentofu.list >/dev/null
sudo apt update sudo apt install -y tofu
Verify:
tofu version
OpenTofu v1.9.x
From here on, anywhere you see terraform <cmd> you can substitute tofu <cmd> — the UX is intentionally identical.
Method 3: Install tfenv for Multiple Versions
If you manage several projects pinned to different Terraform versions — which is common in agencies and large orgs — use tfenv, a version manager modelled after pyenv and rbenv.
git clone https://github.com/tfutils/tfenv.git ~/.tfenv
echo 'export PATH="$HOME/.tfenv/bin:$PATH"' >> ~/.bashrc
source ~/.bashrcList available versions
tfenv list-remote | head -20Install a specific version
tfenv install 1.9.8
tfenv install 1.5.7 # last MPL-licensed releaseSwitch globally
tfenv use 1.9.8tfenv respects a .terraform-version file in any project directory:
cd ~/projects/legacy-infra
echo "1.5.7" > .terraform-version
terraform version # automatically uses 1.5.7For OpenTofu, the equivalent tool is tofuenv (git clone https://github.com/tofuutils/tofuenv.git ~/.tofuenv).
Your First Terraform File
Let's provision a real resource — a Cloudflare DNS record, because it is free, fast, and failure-safe.
mkdir -p ~/terraform-demo && cd ~/terraform-demoCreate main.tf:
terraform { required_version = ">= 1.5.0"required_providers { cloudflare = { source = "cloudflare/cloudflare" version = "~> 4.0" } } }
provider "cloudflare" { api_token = var.cloudflare_api_token }
variable "cloudflare_api_token" { description = "Cloudflare API token with DNS:Edit scope" type = string sensitive = true }
variable "zone_id" { description = "Cloudflare zone ID" type = string }
resource "cloudflare_record" "demo" { zone_id = var.zone_id name = "iac-demo" value = "192.0.2.1" type = "A" ttl = 300 proxied = false }
output "fqdn" { description = "Fully qualified domain name of the created record" value = cloudflare_record.demo.hostname }
Create terraform.tfvars (never commit this file):
cloudflare_api_token = "your-scoped-token-here"
zone_id = "your-zone-id"Add .gitignore:
*.tfvars
*.tfstate
*.tfstate.backup
.terraform/
.terraform.lock.hcl.bak
crash.logThe init / plan / apply / destroy Workflow
Four commands carry 95% of your daily work.
1. terraform init — downloads providers, initialises the backend, and builds .terraform/:
terraform init2. terraform plan — shows exactly what Terraform will change, without changing anything:
terraform plan -out=tfplanRead this output carefully. Lines starting with + mean create, - means destroy, ~ means update-in-place, and -/+ means destroy-then-recreate (dangerous for stateful resources).
3. terraform apply — executes the saved plan:
terraform apply tfplanWithout a saved plan, apply asks for interactive confirmation.
4. terraform destroy — tears down everything Terraform manages in that workspace:
terraform destroyUse this in ephemeral environments (preview branches, CI test sandboxes). Never run it against production without triple-checking -target flags and backups.
Popular Providers Worth Knowing
- hashicorp/aws — AWS (EC2, S3, Lambda, IAM, RDS, EKS)
- hashicorp/google — GCP
- hashicorp/azurerm — Azure
- cloudflare/cloudflare — DNS, WAF, Workers, R2
- hashicorp/kubernetes — any Kubernetes cluster
- hashicorp/helm — Helm chart installs
- digitalocean/digitalocean — Droplets, Spaces, Kubernetes
- hetznercloud/hcloud — Hetzner Cloud
- ovh/ovh — OVHcloud
- contabo/contabo — community Contabo provider
- integrations/github — repos, branch protection, Actions secrets
- vercel/vercel — Vercel projects and deployments
- datadog/datadog — monitors, dashboards, SLOs
- stripe/stripe — products, prices, webhooks (yes, really)
State Management: Local vs Remote
The state file (terraform.tfstate) is Terraform's memory. It maps your HCL resources to real-world IDs, stores attribute values, and tracks dependencies. Lose it and you lose the ability to safely manage your infrastructure.
Local State (Default)
Fine for solo learning. Never for team or production use — concurrent runs corrupt state, and the file often contains secrets in plain text.
Remote State with S3 Backend
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/network/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks"
}
}S3 stores state; DynamoDB provides state locking so two engineers cannot apply at once. Enable bucket versioning and server-side encryption.
Remote State with GCS Backend
terraform {
backend "gcs" {
bucket = "my-tf-state"
prefix = "prod/network"
}
}GCS includes native object-level locking — no separate lock table needed.
Other Backends
- HCP Terraform / Terraform Cloud — free tier, built-in runs and state
- Azure Blob Storage — native locking
- Consul, etcd — for the HashiCorp faithful
- Terraform HTTP backend — Spacelift, Scalr, Env0 all implement this
terraform init -migrate-stateVariables and Outputs
Variables make configs reusable. Three input styles:
variable "instance_count" { description = "Number of app instances" type = number default = 3 validation { condition = var.instance_count >= 1 && var.instance_count <= 10 error_message = "instance_count must be between 1 and 10." } }variable "tags" { type = map(string) default = { Environment = "prod" } }
variable "allowed_cidrs" { type = list(string) default = ["10.0.0.0/8"] }
Values flow in from (highest precedence first): -var CLI flag, -var-file, terraform.tfvars, .auto.tfvars, environment (TF_VAR_), then default.
Outputs expose values to humans and to other modules:
output "db_endpoint" { description = "RDS endpoint for the application" value = aws_db_instance.main.endpoint sensitive = false }
output "db_password" { value = aws_db_instance.main.password sensitive = true # redacted in CLI output }
Read outputs with terraform output or terraform output -json.
Modules
A module is a directory containing .tf files. Modules are the unit of reuse.
module "vpc" { source = "terraform-aws-modules/vpc/aws" version = "5.8.1"name = "prod-vpc" cidr = "10.0.0.0/16"
azs = ["us-east-1a", "us-east-1b", "us-east-1c"] private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] public_subnets = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]
enable_nat_gateway = true single_nat_gateway = true
tags = { Environment = "production" Terraform = "true" } }
Always pin the version — modules are mutable; pinning protects you from upstream breaks. Popular community modules: terraform-aws-modules/eks, terraform-aws-modules/rds, terraform-google-modules/network, cloudposse/label.
Workspaces
Workspaces give you multiple state files from one config — handy for dev, staging, prod variants:
terraform workspace new dev
terraform workspace new prod
terraform workspace select prod
terraform workspace listReference the current workspace in HCL:
resource "aws_instance" "app" {
instance_type = terraform.workspace == "prod" ? "t3.large" : "t3.micro"
tags = {
Env = terraform.workspace
}
}Caveat: workspaces share the same backend config and provider credentials. For stronger isolation between production and everything else, use separate root modules in different directories with different backends, not workspaces.
terraform fmt and validate
Two commands that belong in every commit:
terraform fmt -recursive # canonical formatting
terraform validate # syntax and schema checkfmt is opinionated and non-negotiable in well-run teams. validate catches typos, missing required args, and type mismatches without calling any cloud APIs.
Pre-commit Hooks
Stop bad HCL from ever reaching main. Install pre-commit:
pip install pre-commitCreate .pre-commit-config.yaml:
repos:
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.89.1
hooks:
- id: terraform_fmt
- id: terraform_validate
- id: terraform_tflint
- id: terraform_trivy # security scan
- id: terraform_docs # auto-generated READMEThen:
pre-commit install
pre-commit run --all-filesCI/CD Integration
GitHub Actions example:
name: Terraformon: pull_request: paths: ["terraform/**"] push: branches: [main]
jobs: terraform: runs-on: ubuntu-24.04 defaults: run: working-directory: ./terraform steps: - uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3 with: terraform_version: 1.9.8
- name: Terraform fmt run: terraform fmt -check -recursive
- name: Terraform init run: terraform init env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Terraform validate run: terraform validate
- name: Terraform plan if: github.event_name == 'pull_request' run: terraform plan -no-color
- name: Terraform apply if: github.ref == 'refs/heads/main' run: terraform apply -auto-approve
For OpenTofu, swap hashicorp/setup-terraform@v3 for opentofu/setup-opentofu@v1.
Drift Detection
"Drift" is when reality diverges from what Terraform believes — usually because someone clicked in a console. Detect it nightly:
terraform plan -detailed-exitcodeexit 0 = no changes
exit 1 = error
exit 2 = drift detected
Wire that into a cron or a scheduled CI job and alert on exit 2. Tools like driftctl, Spacelift, and env0 offer richer drift UIs and continuous monitoring.
Destroy Command Safety
terraform destroy is the single most dangerous command in your toolbox. Guardrails:
resource "aws_db_instance" "main" {
# ...
lifecycle {
prevent_destroy = true
}
}-target to remove one resource rather than everything.cp terraform.tfstate terraform.tfstate.$(date +%s).bak.Best Practices
- Separate state files per environment and per domain.
prod/network,prod/app,staging/network. Blast radius stays small. - Never commit
.tfvarsthat contain secrets. Use environment vars, SOPS, Vault, or cloud secret managers. - Pin everything — Terraform version, provider versions, module versions.
terraform.lock.hcllocks providers; commit it. - Use remote state with locking from day one. Even solo projects benefit.
- Enable detailed logging when stuck:
TF_LOG=DEBUG terraform apply 2> debug.log. - Run
terraform fmtandvalidateon every commit via pre-commit. - Tag every resource with Environment, Owner, CostCentre, and ManagedBy=terraform.
- Use data sources to read, not to write — look up existing VPCs, AMIs, secrets.
- Small, focused modules beat megamodules. Aim for modules that do one thing well.
- Write module READMEs with
terraform-docs— auto-generated, always current. - Review plans, not applies. Plan output in PR comments is your friend.
Troubleshooting
"Error acquiring the state lock" — a previous run crashed. Inspect with terraform force-unlock <LOCK_ID>. Only do this after confirming no other run is active.
"Provider produced inconsistent result after apply" — provider bug. Upgrade the provider: terraform init -upgrade.
"Failed to install provider" — network, proxy, or registry mirror. Check ~/.terraformrc for mirror config.
State file corrupted — restore from S3 versioning (always enable it) or from the automatic .backup sibling file.
"Resource already exists" — something was created out-of-band. Import it:
terraform import aws_s3_bucket.existing my-bucket-nameSlow plans on large state — split the state. Refactor monoliths into root modules by domain.
Provider auth failures — export credentials explicitly:
export AWS_PROFILE=prod
export TF_LOG=DEBUG
terraform planFAQ
Terraform vs OpenTofu — is OpenTofu really a drop-in replacement?
For 99% of configs, yes. OpenTofu 1.6 started from the last MPL Terraform commit and has stayed compatible. Diverging features (OCI registries, early variable evaluation) are additive — existing configs still work. Do a plan diff before switching production.
Terraform vs Pulumi — which should I pick? Terraform uses HCL, a purpose-built declarative language. Pulumi uses real programming languages (TypeScript, Python, Go, C#). HCL is simpler and easier to review; Pulumi is more flexible for complex logic. The IaC ecosystem (modules, providers, tooling) is an order of magnitude larger for Terraform/OpenTofu.
Is HCL hard to learn? No. You can be productive in a day. The difficulty lies in cloud-provider semantics, not HCL itself.
What about CloudFormation, Bicep, or AWS CDK? Great if you are single-cloud and want native integration. Terraform wins the moment you touch more than one provider — which most teams eventually do.
Is OpenTofu production-ready? Yes. Version 1.6 GA shipped in January 2024; the Linux Foundation backs it; companies including Spacelift, Gruntwork, and Harness run it in production. Many large orgs have migrated entirely.
Can I run both Terraform and OpenTofu on the same state? Technically yes — they read the same format. In practice, pick one per project to avoid version-skew headaches.
How do I handle secrets?
Never in .tfvars committed to git. Options: environment variables (TF_VAR_db_password), SOPS + age, HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager, or your CI/CD's encrypted secrets. Mark output and variable blocks sensitive = true.
Do I need Terraform Cloud / HCP Terraform? No. S3 + DynamoDB (or GCS) provides the same core value — remote state and locking — for pennies. HCP Terraform adds policy-as-code, run management, and a UI that many teams find worth the cost.
How do I upgrade Terraform versions?
Bump required_version, test in staging, run terraform init -upgrade, run plan, review. Never skip major versions without reading the upgrade guide.
Next Steps
You have Terraform or OpenTofu installed, you understand the core workflow, and you know how to manage state safely. From here:
fmt, validate, and plan on every PR, and apply on merge to main.Infrastructure as Code is not just a tool change — it is a cultural one. Commits become deployments, reviews become change control, and main becomes the source of truth for your entire production stack. Start small, pin everything, automate the boring parts, and let the declarative model do the heavy lifting.