What is Terraform? #

Modern infrastructure is too complex to manage manually. Every time you create a server, set up a network, or configure a database through a cloud provider’s UI, you leave behind a process that’s undocumented, hard to repeat, and prone to errors. Terraform exists to solve this problem — by defining your entire infrastructure as code that can be read, verified, and executed consistently.

This article explains what Terraform is, how it works declaratively, the basic building blocks of its configuration, and why it has become the tool of choice for managing cloud infrastructure across the industry. By the end of this article, you’ll understand the core concepts that form the foundation of the entire Terraform learning series.

Defining Terraform #

Terraform is an open-source tool created by HashiCorp that lets you define cloud infrastructure as declarative code. This code is called a Terraform configuration, written in HCL (HashiCorp Configuration Language), and can be run to create, modify, or destroy infrastructure automatically.

This concept is known as Infrastructure as Code (IaC) — treating infrastructure like software code: it can be version controlled, reviewed, and tested. But Terraform isn’t just a “script to spin up servers”. The fundamental difference lies in its declarative approach, which we’ll cover in the next section.

# The simplest Terraform configuration example
# Creates an S3 bucket in AWS

resource "aws_s3_bucket" "contoh" {
  bucket = "nama-bucket-saya"
}

With the code above, Terraform knows you want an S3 bucket to exist. Terraform handles the rest — calling the AWS API, waiting for the bucket to finish being created, and recording the result in a state file. You don’t need to know which API endpoint to call, how to handle retries, or what the required parameters are.


The Problems Terraform Solves #

Before tools like Terraform existed, infrastructure teams relied on manual methods full of problems. Clicking around in the cloud provider console leaves no documentation. Bash scripts written in a hurry aren’t idempotent and are hard to maintain. Configuration in the dev environment can end up wildly different from production without anyone noticing until an incident happens.

THE PROBLEMS WITH MANUAL INFRASTRUCTURE:
  ✗ Clicking around the cloud provider console — undocumented
  ✗ Ad-hoc bash scripts — not idempotent, hard to maintain
  ✗ Different configuration in every environment (dev ≠ staging ≠ prod)
  ✗ No audit trail — who created what, and when?
  ✗ Slow disaster recovery — you have to rebuild everything manually

WITH TERRAFORM:
  ✓ Infrastructure defined in .tf files — committable to Git
  ✓ Every change can be reviewed before it's applied
  ✓ Dev, staging, and prod environments can be identical
  ✓ Rebuilding infrastructure from scratch takes a single command
  ✓ Change history is stored in version control

Consider this scenario: your team runs 50 servers in production. One day, the region hosting those servers goes down. Without Terraform, you’d have to recreate all 50 servers manually — remembering which instance type was used, what the security groups looked like, the right subnets, and hundreds of other settings. With Terraform, you just run terraform apply in a new region and the same infrastructure is rebuilt in minutes.


Declarative vs Imperative #

Terraform takes a declarative approach — you define what you want, not how to achieve it. This is the most important distinction to understand from the start.

An imperative approach (like bash scripts or general-purpose programming languages) requires you to write steps in sequence: create the VPC first, then the subnet, then the security group, then the instance. The order has to be right, and if an error happens midway, you have to handle the rollback yourself.

The declarative approach flips this model around. You only state the desired end state, and Terraform figures out what steps are needed to reach it.

// ANTI-PATTERN: imperative approach  you have to know the order and the how
// export VPC_ID=$(aws ec2 create-vpc --cidr-block 10.0.0.0/16 --query 'Vpc.VpcId' --output text)
// aws ec2 create-subnet --vpc-id $VPC_ID --cidr-block 10.0.1.0/24
// aws ec2 run-instances --image-id ami-xxx --subnet-id $SUBNET_ID ...

// CORRECT: declarative approach  just define what you want
resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
}

resource "aws_subnet" "app" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.1.0/24"
}

Notice that in the declarative code above, you never write create-vpc or create-subnet. You only state that “a VPC with CIDR 10.0.0.0/16 must exist” and “a subnet with CIDR 10.0.1.0/24 must exist inside that VPC”. Terraform figures out on its own that the VPC has to be created first because the subnet depends on it.


How Terraform Works #

Terraform works in three main steps that form its core cycle. Understanding this cycle is essential because every Terraform operation — whether creating new resources, modifying existing ones, or removing unneeded ones — always goes through the same flow.

flowchart TD
    A["📝 You write configuration (.tf)"] --> B["terraform init"]
    B --> C["terraform plan"]
    C --> D{"Approve\nchanges?"}
    D -- Yes --> E["terraform apply"]
    D -- No --> A
    E --> F["State saved"]
    F --> A

    style A fill:#e8f5e9,stroke:#2e7d32
    style C fill:#e3f2fd,stroke:#1565c0
    style E fill:#fff3e0,stroke:#e65100
    style F fill:#f3e5f5,stroke:#6a1b9a

Step 1: Write — Write the Configuration #

You write .tf files defining the resources you want. This can be one file or hundreds — Terraform reads them all from a single working directory.

resource "aws_instance" "web" {
  ami           = "ami-0abcdef1234567890"
  instance_type = "t3.micro"

  tags = {
    Name = "web-server"
  }
}

Step 2: Plan — See the Change Plan #

The terraform plan command compares the configuration you wrote against the infrastructure state recorded in the state file. The result is a change plan showing exactly what will be added, modified, or removed.

Terraform will perform the following actions:

  # aws_instance.web will be created
  + resource "aws_instance" "web" {
      + ami           = "ami-0abcdef1234567890"
      + instance_type = "t3.micro"
      + tags          = {
          + "Name" = "web-server"
        }
    }

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

This output is one of Terraform’s most powerful features — you can see exactly what’s about to happen before any change is actually made. No surprises, no “oops, we just deleted the production database”.

Step 3: Apply — Execute the Changes #

Once you’ve reviewed the plan and you’re confident everything is correct, terraform apply executes the planned changes. Terraform calls the appropriate provider APIs, creates the requested resources, and stores the results in the state file.

This flow is consistent across all providers and all scales — whether you manage one resource or a thousand. It doesn’t matter if the target is AWS, GCP, Azure, or GitHub — the cycle is always the same: write, plan, apply.


HCL — Terraform’s Configuration Language #

Terraform configurations are written in HCL (HashiCorp Configuration Language). HCL is designed to be easy for humans to read while remaining easy for machines to process. Unlike YAML or JSON, which are generic, HCL is purpose-built for infrastructure configuration with expressive, safe syntax.

Every block in HCL has a clear purpose. There are three basic blocks you’ll encounter in almost every Terraform configuration:

# 1. terraform block — global configuration and provider versions
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

# 2. provider block — connection to external services
provider "aws" {
  region = "ap-southeast-1"
}

# 3. resource block — the infrastructure you want to manage
resource "aws_instance" "web_server" {
  ami           = "ami-0abcdef1234567890"
  instance_type = "t3.micro"

  tags = {
    Name        = "web-server"
    Environment = "production"
  }
}

The terraform block holds configuration about Terraform itself — which providers are used, the minimum required version, and the backend for storing state. The provider block configures the connection to an external service — region, credentials, and provider-specific options. The resource block defines the actual infrastructure you want to create and manage.

HCL also supports single-line comments with # or //, and multi-line comments with /* ... */. For Terraform configuration, the common convention is to use #.


State — Terraform’s Memory #

After a successful terraform apply, Terraform stores the state of your infrastructure in a file called terraform.tfstate. This file maps between the configuration in your code and the actual resources in the cloud provider.

State is how Terraform knows what needs to change when you modify your configuration. Without state, Terraform would have no way to know which resources already exist, what parameters were used when they were created, or their resource IDs in the cloud provider.

{
  "resources": [
    {
      "type": "aws_instance",
      "name": "web_server",
      "attributes": {
        "id": "i-0abc123def456789",
        "ami": "ami-0abcdef1234567890",
        "instance_type": "t3.micro",
        "public_ip": "54.239.28.85"
      }
    }
  ]
}

The example above shows what’s in a state file, simplified. When you change instance_type from t3.micro to t3.medium, Terraform reads the state, sees that instance i-0abc123def456789 already exists with type t3.micro, and concludes that the required change is a modify — not a delete and create from scratch.

The concept of state is explored in more depth in the article State in the Concept section.


Multi-Provider — One Tool for Every Cloud #

One of Terraform’s advantages over similar tools is its ability to work with hundreds of different providers from a single configuration. Providers aren’t just cloud providers — anything with an API can have a Terraform provider.

# Terraform can manage resources from various providers
# in the same workspace

# Resource on AWS
resource "aws_s3_bucket" "storage" {
  bucket = "app-storage-prod"
}

# DNS on Cloudflare — referencing the IP of the AWS instance
resource "cloudflare_record" "dns" {
  zone_id = var.cloudflare_zone_id
  name    = "app"
  value   = aws_instance.web.public_ip
  type    = "A"
}

# Repository on GitHub
resource "github_repository" "app" {
  name        = "my-app"
  description = "Repository aplikasi utama"
  visibility  = "private"
}

Notice how cloudflare_record.dns references aws_instance.web.public_ip directly. Terraform understands cross-provider dependencies and creates resources in the right order — the AWS instance first, then the DNS record referencing its IP. This is the power of declaration: you simply say “this DNS points to that server’s IP”, and Terraform determines the execution order.

Providers are available for AWS, GCP, Azure, Kubernetes, Cloudflare, GitHub, Datadog, PagerDuty, and hundreds of other services. You can find the full list in the Terraform Registry.


Terraform vs Other Tools #

Terraform isn’t the only IaC tool on the market. There are several alternatives, each with a different approach. Understanding where Terraform sits among them helps you know when it’s the right choice.

flowchart LR
    subgraph IaC["Infrastructure as Code"]
        direction TB
        D["Declarative"]
        I["Imperative"]
    end

    D --> T["Terraform"]
    D --> P["Pulumi"]
    I --> A["Ansible"]
    I --> CF["CloudFormation"]

    T -- "Multi-cloud, HCL" --> R1["Best for: multi-cloud, infrastructure"]
    P -- "Multi-cloud, general languages" --> R2["Best for: developers who want a programming language"]
    A -- "Agentless, YAML" --> R3["Best for: provisioning + config management"]
    CF -- "AWS only, JSON/YAML" --> R4["Best for: AWS-only, tight integration"]

    style T fill:#e8f5e9,stroke:#2e7d32
    style D fill:#e3f2fd,stroke:#1565c0
    style I fill:#fff3e0,stroke:#e65100
AspectTerraformCloudFormationAnsiblePulumi
ApproachDeclarativeDeclarativeImperativeDeclarative/Imperative
Multi-cloud✅ Hundreds of providers❌ AWS only✅ But not its specialty✅ Multi-cloud
LanguageHCLJSON/YAMLYAMLGo, Python, TS, C#
State management✅ Built-in✅ Managed by AWS❌ None✅ Built-in
Drift detectionterraform plandrift detection❌ Manual check needed
Learning curveModerateLow (if AWS only)LowModerate-High
CommunityVery largeLarge (AWS ecosystem)Very largeGrowing

Terraform excels in multi-cloud and multi-provider scenarios. If your infrastructure only lives on AWS and never will leave, CloudFormation could be a simpler alternative. If you need provisioning as well as server configuration (installing packages, editing config files), Ansible is a better fit. For a full explanation of the alternatives and when to use each, see the article Alternatives.


When Terraform Is Used #

Terraform isn’t a universal solution — there are situations where it fits perfectly, and situations where a better option exists. Understanding the context helps you avoid over-engineering or picking the wrong tool.

A PERFECT FIT:
  ✓ Multi-cloud infrastructure (AWS + GCP + Azure at once)
  ✓ Infrastructure that needs to be replicated (dev = staging = prod)
  ✓ Teams that need to review infrastructure changes (GitOps)
  ✓ Disaster recovery — rebuild infrastructure from scratch quickly
  ✓ Compliance — audit trail for every infrastructure change

CONSIDER AN ALTERNATIVE:
  ✗ Configuration inside servers (install nginx, edit /etc/hosts) → use Ansible/Chef
  ✗ AWS-only and never multi-cloud → CloudFormation can be simpler
  ✗ Very simple infrastructure (1-2 resources) → the console might be faster
  ✗ Pure serverless apps → SAM or SST might fit better

For a deeper look at when you should and shouldn’t use Terraform, read the article When to Use Terraform.


Summary #

  • Terraform is an IaC tool — infrastructure is defined as HCL code, not clicked together manually in a console.
  • It works declaratively — you define what you want, and Terraform decides how to achieve it. This differs from the imperative approach, where you write steps in sequence.
  • The working cycle is write → plan → apply — every change can be reviewed and approved before it’s applied, avoiding surprises in production.
  • State is Terraform’s memory — the state file records the mapping between your code configuration and the actual cloud resources, enabling accurate incremental changes.
  • Multi-provider — one tool to manage AWS, GCP, Azure, Kubernetes, and hundreds of other services simultaneously, with cross-provider dependencies handled automatically.
  • Suitable for every scale — from a single simple server to complex multi-region infrastructure, with the same working cycle throughout.

← Previous: Introduction   Next: Manual Infrastructure →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact