Skip to content

Set Up Terraform Locally

On this page, you will:

  • Install Terraform on your local machine
  • Verify the installation
  • Understand the Terraform workflow
  • Learn basic Terraform commands
  • Create a test configuration to verify everything works

Verify Terraform Installation

You should have already installed Terraform as part of the Local Development Environment setup. Let's verify it's working correctly.

Check Terraform Version

terraform version

Expected output (version number may vary):

Terraform v1.14.3
on darwin_arm64

Terraform Version Management

We installed Terraform using tfenv, which allows you to switch between different Terraform versions easily. This is helpful when working on multiple projects that require different Terraform versions.

To install a different version: tfenv install 1.x.x && tfenv use 1.x.x

If Terraform Is Not Installed

If you skipped the local environment setup or need to install Terraform now, follow these instructions:

macOS (using tfenv - recommended):

brew install tfenv
tfenv install 1.14.3
tfenv use 1.14.3

Other platforms:

Linux (Ubuntu/Debian)
# Install tfenv
git clone https://github.com/tfutils/tfenv.git ~/.tfenv
echo 'export PATH="$HOME/.tfenv/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

# Install Terraform
tfenv install 1.14.3
tfenv use 1.14.3
Windows (Chocolatey)
choco install terraform
Or download the binary from [terraform.io/downloads](https://www.terraform.io/downloads) and add it to your PATH.

Understanding the Terraform Workflow

Before writing any code, understand how Terraform works:

┌─────────────────────────────────────────────────────────┐
│                                                         │
│  1. Write Configuration (.tf files)                     │
│     ↓                                                   │
│  2. terraform init                                      │
│     - Downloads providers                               │
│     - Configures backend (remote state)                 │
│     ↓                                                   │
│  3. terraform plan                                      │
│     - Compares desired state with current state         │
│     - Shows what will change                            │
│     ↓                                                   │
│  4. Review the plan                                     │
│     - Check the changes make sense                      │
│     - Ensure no unexpected deletions                    │
│     ↓                                                   │
│  5. terraform apply                                     │
│     - Executes the plan                                 │
│     - Creates/updates/deletes resources                 │
│     - Updates state file                                │
│     ↓                                                   │
│  6. Verify in provider (AWS Console, GitHub, etc.)      │
│                                                         │
└─────────────────────────────────────────────────────────┘

Key Commands

terraform init Initialises a Terraform working directory. Downloads providers and configures the backend. Run this first in any new Terraform project, and re-run it when you add new providers.

terraform plan Shows what changes Terraform will make without actually making them. Always run this before apply to review changes.

terraform apply Executes the changes shown in the plan. Creates, updates, or deletes resources to match your configuration. You should never run this locally in production - this should always be handled by a service principal.

terraform destroy Removes all resources managed by Terraform. Use with extreme caution in production. You should never run this locally in production - this should always be handled by a service principal.

terraform fmt Formats your Terraform files to standard style. Run before committing code.

terraform validate Checks your configuration for syntax errors. Useful for catching mistakes early.

Create a Test Configuration

Let's create a simple test to verify Terraform works correctly.

Create a Test Directory

Ensure you are in a directory where you store your projects. Run the following:

cd projects/data  # or wherever you store your projects
take terraform-test
code .

Write a Simple Configuration

Create a file called main.tf:

terraform {
  required_version = ">= 1.0"

  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "~> 2.0"
    }
  }
}

resource "local_file" "test" {
  filename = "${path.module}/test-output.txt"
  content  = "Hello from Terraform! Created at ${timestamp()}"
}

output "file_path" {
  value       = local_file.test.filename
  description = "Path to the created file"
}

This configuration: - Specifies Terraform version requirements - Uses the local provider (for creating local files) - Creates a file called test-output.txt - Outputs the file path

Initialise Terraform

terraform init

Expected output:

Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/local versions matching "~> 2.0"...
- Installing hashicorp/local v2.4.0...
- Installed hashicorp/local v2.4.0 (signed by HashiCorp)

Terraform has been successfully initialized!

This downloads the local provider which Terraform needs to create files.

Plan the Changes

terraform plan

Expected output:

Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # local_file.test will be created
  + resource "local_file" "test" {
      + content              = "Hello from Terraform! Created at 2026-01-17T22:00:00Z"
      + 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             = "/Users/yourname/terraform-test/test-output.txt"
      + id                   = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + file_path = "/Users/yourname/terraform-test/test-output.txt"

Notice: - + means "create" - Some values show (known after apply) - these are computed after the resource is created - The plan shows 1 resource to add - Outputs show what will be displayed after apply

Apply the Changes

terraform apply

Terraform shows the plan again and asks for confirmation:

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value:

Type yes and press Enter.

Expected output:

local_file.test: Creating...
local_file.test: Creation complete after 0s [id=abc123...]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Outputs:

file_path = "projects/data/terraform-test/test-output.txt"

Verify the Result

Check the file was created:

cat test-output.txt

Expected output:

Hello from Terraform! Created at 2026-01-17T22:00:00Z

Check the State File

Terraform created a state file tracking what it created:

ls -la

You'll see:

-rw-r--r--  1 yourname  staff   123 Jan 17 22:00 main.tf
-rw-r--r--  1 yourname  staff   456 Jan 17 22:00 terraform.tfstate
-rw-r--r--  1 yourname  staff    89 Jan 17 22:00 test-output.txt

State File Contains Sensitive Data

The terraform.tfstate file contains the complete state of your infrastructure, including sensitive data like passwords and API keys. Never commit this file to Git. For real projects, you'll use remote state (which we set up in the previous page).

Modify the Configuration

Let's make a change. Edit main.tf and change the content:

terraform {
  required_version = ">= 1.0"

  required_providers {
    local = {
      source  = "hashicorp/local"
      version = "~> 2.0"
    }
  }
}

resource "local_file" "test" {
  filename = "${path.module}/test-output.txt"
  content  = "Hello from Terraform! Updated at ${timestamp()}"
}

output "file_path" {
  value       = local_file.test.filename
  description = "Path to the created file"
}

Run plan to see what will change:

terraform plan

Expected output:

Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # local_file.test will be updated in-place
  ~ resource "local_file" "test" {
      ~ content              = "Hello from Terraform! Created at ..." -> "Hello from Terraform! Updated at ..."
        id                   = "abc123..."
        # (8 unchanged attributes hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.

Notice: - ~ means "update in-place" - Terraform shows what's changing (content) and what's staying the same - Plan shows 1 to change

Apply the change:

terraform apply -auto-approve

Auto-Approve Flag

The -auto-approve flag skips the confirmation prompt. Use it when you're confident in the changes, but be careful - it's safer to review the plan first.

Clean Up

Remove the test resources:

terraform destroy

Terraform shows what it will delete:

Terraform used the selected providers to generate the following execution plan.
Resource actions are indicated with the following symbols:
  - destroy

Terraform will perform the following actions:

  # local_file.test will be destroyed
  - resource "local_file" "test" {
      - content              = "Hello from Terraform! Updated at ..." -> null
      - filename             = "/Users/yourname/terraform-test/test-output.txt" -> null
      - id                   = "abc123..." -> null
      # (8 unchanged attributes hidden)
    }

Plan: 0 to add, 0 to change, 1 to destroy.

Do you really want to destroy all resources?
  Terraform will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value:

Type yes to confirm.

The file and state are removed:

ls -la

Only main.tf remains (and .terraform/ directory with provider plugins).

Terraform Configuration Language Basics

Now that you've seen Terraform in action, let's understand the syntax.

Blocks

Terraform uses blocks to define configuration:

block_type "label" "name" {
  argument = "value"

  nested_block {
    nested_argument = "value"
  }
}

Common block types: - terraform - Terraform settings - provider - Provider configuration - resource - Infrastructure resources - data - Data sources (read existing resources) - variable - Input variables - output - Output values - locals - Local values (computed within config) - module - Reusable modules

Resources

Resources are the most important block type:

resource "resource_type" "local_name" {
  argument1 = "value1"
  argument2 = "value2"
}
  • resource_type: Provider-specific type (e.g., aws_s3_bucket, github_repository)
  • local_name: Name you use to reference this resource in your config
  • arguments: Resource-specific settings

Variables

Variables make your configuration reusable:

variable "environment" {
  description = "Environment name (dev, staging, prod)"
  type        = string
  default     = "dev"
}

resource "aws_s3_bucket" "data" {
  bucket = "my-data-${var.environment}"
}

Outputs

Outputs display values after apply:

output "bucket_name" {
  value       = aws_s3_bucket.data.bucket
  description = "Name of the S3 bucket"
}

Expressions and Functions

Terraform supports expressions and built-in functions:

# String interpolation
name = "bucket-${var.environment}"

# Functions
timestamp = timestamp()
upper_env = upper(var.environment)

# Conditionals
count = var.create_bucket ? 1 : 0

# Lists and maps
tags = {
  Environment = var.environment
  ManagedBy   = "terraform"
}

Best Practices for Local Development

Directory Structure

Organise your Terraform code logically:

terraform-project/
├── main.tf           # Main resources
├── variables.tf      # Variable definitions
├── outputs.tf        # Output definitions
├── providers.tf      # Provider configurations
├── backend.tf        # Backend configuration (remote state)
├── terraform.tfvars  # Variable values (don't commit if contains secrets)
└── .gitignore        # Ignore state files and secrets

.gitignore for Terraform

Always use a .gitignore to prevent committing sensitive files:

# Local .terraform directories
**/.terraform/*

# .tfstate files
*.tfstate
*.tfstate.*

# Crash log files
crash.log
crash.*.log

# Exclude all .tfvars files which may contain sensitive data
*.tfvars
*.tfvars.json

# Ignore override files
override.tf
override.tf.json
*_override.tf
*_override.tf.json

# Ignore CLI configuration files
.terraformrc
terraform.rc

Format Your Code

You can format your code manually:

terraform fmt -recursive

However, we'll set up pre-commit hooks to do this automatically when you commit. This ensures consistent style across your team without having to remember to run the command.

Validate Before Committing

Check for errors:

terraform validate

This catches syntax errors and invalid references.

Use Pre-commit Hooks

You should have already installed pre-commit as part of the Local Development Environment setup. Pre-commit automatically runs checks before pushing code to the remote repository.

We'll set up pre-commit hooks for Terraform in the next page when we create the repository structure. These hooks will automatically run on push:

  • Format Terraform files with terraform fmt
  • Validate Terraform syntax
  • Generate documentation with terraform-docs
  • Check for security issues
  • Ensure consistent code style

Pre-commit on Push vs Commit

We configure pre-commit to run on push rather than on every commit. This gives you flexibility to make incremental commits during development without waiting for all checks to pass. When you're ready to push your work, pre-commit ensures everything meets quality standards before it reaches the remote repository.

Common Terraform Patterns

Managing Multiple Environments

Use workspaces or separate directories:

Option 1: Workspaces

terraform workspace new dev
terraform workspace new staging
terraform workspace new prod

terraform workspace select dev
terraform apply

Option 2: Separate Directories (Recommended)

terraform/
├── dev/
│   ├── main.tf
│   └── terraform.tfvars
├── staging/
│   ├── main.tf
│   └── terraform.tfvars
└── prod/
    ├── main.tf
    └── terraform.tfvars

We'll use separate directories as it's clearer and safer.

Using Modules

Modules allow code reuse:

module "s3_bucket" {
  source = "./modules/s3-bucket"

  bucket_name = "my-data-bucket"
  environment = "prod"
}

Remote State for Teams

For team projects, always use remote state:

terraform {
  backend "s3" {
    bucket         = "terraform-state-123456789012"
    key            = "project/terraform.tfstate"
    region         = "eu-west-2"
    dynamodb_table = "terraform-state-lock"
    encrypt        = true
  }
}

We have already built the resources we need for this, but we'll configure it for your production repo in the next page.

Troubleshooting

Error: Command not found

If terraform isn't found, ensure it's in your PATH:

which terraform

For Homebrew on macOS, it should be at /opt/homebrew/bin/terraform.

Error: Lock timeout

If someone else is running Terraform, you might see:

Error: Error acquiring the state lock

Wait for their run to complete, or if it's stuck:

terraform force-unlock <lock-id>

Force Unlock Carefully

Only force unlock if you're certain no one else is running Terraform. Otherwise, you risk corrupting the state.

Error: Provider version conflict

If you see version conflicts, update your required_providers block to use compatible versions:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"  # Accept 5.x versions
    }
  }
}

What's Next

You now have Terraform installed and understand the basic workflow:

  • Terraform installed and verified
  • Understand the workflow (init → plan → apply)
  • Created a test configuration
  • Know basic Terraform syntax
  • Understand best practices

Next, you'll create the actual Terraform repository structure and configure it to use the remote state you set up in S3.

Continue to Create the Terraform Repository