10.2 Terraform Core Concepts

This chapter covers the fundamental building blocks of Terraform configurations that you’ll use in every project.

Terraform Core Concepts

1. Providers

Providers are plugins that enable Terraform to interact with cloud platforms, SaaS providers, and other APIs. Each provider adds a set of resource types and data sources.

# Google Cloud Provider (Primary focus)
provider "google" {
  project = "my-gcp-project-id"
  region  = "us-central1"
  zone    = "us-central1-a"
}

# Multiple providers can be used together
provider "aws" {
  region  = "us-west-2"
  profile = "default"
}

# Azure Provider
provider "azurerm" {
  features {}
}

# Provider versioning for stability
terraform {
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "~> 4.0"
    }
  }
}

2. Resources

Resources are the most important element in Terraform. Each resource block describes one or more infrastructure objects, such as virtual networks, compute instances, or DNS records.

# GCP Cloud Storage bucket
resource "google_storage_bucket" "data" {
  name     = "my-company-data-bucket"
  location = "US"

  versioning {
    enabled = true
  }

  encryption {
    default_kms_key_name = google_kms_crypto_key.bucket_key.id
  }

  uniform_bucket_level_access = true

  lifecycle {
    prevent_destroy = true  # Protect from accidental deletion
  }

  labels = {
    environment = "production"
    team        = "data-engineering"
  }
}

# GCP KMS key for bucket encryption
resource "google_kms_crypto_key" "bucket_key" {
  name     = "bucket-encryption-key"
  key_ring = google_kms_key_ring.main.id

  purpose = "ENCRYPT_DECRYPT"

  lifecycle {
    prevent_destroy = true
  }
}

3. Data Sources

Data sources allow Terraform to fetch information from existing infrastructure or external sources.

# Get the latest Ubuntu 22.04 LTS image
data "google_compute_image" "ubuntu" {
  family  = "ubuntu-2204-lts"
  project = "ubuntu-os-cloud"
}

# Get information about available zones
data "google_compute_zones" "available" {
  region = "us-central1"
  status = "UP"
}

# Get current project information
data "google_project" "current" {}

# Use the image in a compute instance
resource "google_compute_instance" "web" {
  name         = "web-server"
  machine_type = "e2-micro"
  zone         = data.google_compute_zones.available.names[0]

  boot_disk {
    initialize_params {
      image = data.google_compute_image.ubuntu.self_link
    }
  }

  metadata = {
    project-id = data.google_project.current.project_id
  }
}

4. Variables

Variables make your Terraform configurations flexible and reusable.

# variables.tf
variable "environment" {
  description = "Environment name"
  type        = string
  default     = "development"
}

variable "instance_count" {
  description = "Number of instances to create"
  type        = number
  default     = 1

  validation {
    condition     = var.instance_count >= 1 && var.instance_count <= 10
    error_message = "Instance count must be between 1 and 10."
  }
}

variable "allowed_cidr_blocks" {
  description = "CIDR blocks allowed to access resources"
  type        = list(string)
  default     = ["0.0.0.0/0"]
}

variable "tags" {
  description = "Tags to apply to resources"
  type        = map(string)
  default     = {
    Project = "MyApp"
    Team    = "DevOps"
  }
}

Variable Types:

# String variable
variable "project_name" {
  type        = string
  description = "Name of the GCP project"
}

# Number variable
variable "disk_size" {
  type        = number
  description = "Boot disk size in GB"
  default     = 20
}

# Boolean variable
variable "enable_https" {
  type        = bool
  description = "Enable HTTPS load balancer"
  default     = true
}

# List variable
variable "availability_zones" {
  type        = list(string)
  description = "List of availability zones"
  default     = ["us-central1-a", "us-central1-b"]
}

# Map variable
variable "instance_types" {
  type = map(string)
  description = "Instance types per environment"
  default = {
    development = "e2-micro"
    staging     = "e2-small"
    production  = "e2-medium"
  }
}

# Object variable
variable "database_config" {
  type = object({
    tier = string
    size = number
    backup_enabled = bool
  })
  description = "Database configuration"
  default = {
    tier           = "db-f1-micro"
    size           = 10
    backup_enabled = true
  }
}

Using Variables:

# In terraform.tfvars file
environment = "production"
instance_count = 3
project_name = "my-web-app"

# In resource configuration
resource "google_compute_instance" "app" {
  count        = var.instance_count
  name         = "${var.project_name}-${var.environment}-${count.index + 1}"
  machine_type = var.instance_types[var.environment]

  labels = {
    environment = var.environment
    project     = var.project_name
  }
}

5. Outputs

Outputs display information about your infrastructure and can be used to pass data to other Terraform configurations.

# outputs.tf
output "instance_public_ip" {
  description = "Public IP address of the web server"
  value       = google_compute_instance.web_server.network_interface[0].access_config[0].nat_ip
}

output "network_id" {
  description = "ID of the VPC network"
  value       = google_compute_network.main.id
}

output "database_connection_name" {
  description = "Cloud SQL instance connection name"
  value       = google_sql_database_instance.main.connection_name
  sensitive   = true  # Mark sensitive data
}

output "load_balancer_ip" {
  description = "External IP of the load balancer"
  value       = google_compute_global_address.lb_ip.address
}

Output Usage:

# View all outputs
terraform output

# Get specific output value
terraform output instance_public_ip

# Get output in JSON format
terraform output -json

# Use in scripts
IP=$(terraform output -raw instance_public_ip)
curl http://$IP

6. Local Values

Locals assign a name to an expression, making it easier to reuse values throughout your configuration.

locals {
  common_tags = {
    Environment = var.environment
    ManagedBy   = "Terraform"
    Project     = "WebApp"
  }

  name_prefix = "${var.environment}-webapp"

  instance_type = var.environment == "production" ? "e2-medium" : "e2-micro"

  # Complex expressions
  availability_zones = data.google_compute_zones.available.names

  # Conditional logic
  backup_retention = var.environment == "production" ? 30 : 7
}

resource "google_compute_instance" "app" {
  name         = "${local.name_prefix}-server"
  machine_type = local.instance_type
  zone         = local.availability_zones[0]

  boot_disk {
    initialize_params {
      image = data.google_compute_image.ubuntu.self_link
    }
  }

  network_interface {
    network = "default"
    access_config {}
  }

  labels = local.common_tags

  metadata = {
    Name = "${local.name_prefix}-server"
  }
}

Advanced Concepts

1. Resource Dependencies

Terraform automatically handles dependencies through resource references:

# Implicit dependency - network referenced in instance
resource "google_compute_network" "main" {
  name = "main-network"
}

resource "google_compute_instance" "app" {
  name = "app-server"

  network_interface {
    network = google_compute_network.main.id  # Creates dependency
  }
}

# Explicit dependency when reference isn't enough
resource "google_compute_instance" "app" {
  depends_on = [
    google_compute_network.main,
    google_compute_firewall.allow_ssh
  ]
}

2. Count and For Each

Create multiple similar resources:

# Using count
resource "google_compute_instance" "web" {
  count = 3

  name = "web-server-${count.index + 1}"
  # ... other configuration
}

# Using for_each with list
variable "instance_names" {
  default = ["web", "api", "database"]
}

resource "google_compute_instance" "servers" {
  for_each = toset(var.instance_names)

  name = "${each.value}-server"
  # ... other configuration
}

# Using for_each with map
variable "environments" {
  default = {
    dev  = "e2-micro"
    prod = "e2-medium"
  }
}

resource "google_compute_instance" "env_servers" {
  for_each = var.environments

  name         = "${each.key}-server"
  machine_type = each.value
  # ... other configuration
}

3. Lifecycle Management

Control resource behavior:

resource "google_storage_bucket" "important_data" {
  name = "critical-application-data"

  lifecycle {
    # Prevent accidental deletion
    prevent_destroy = true

    # Create new resource before destroying old one
    create_before_destroy = true

    # Ignore changes to specific attributes
    ignore_changes = [
      labels,
      versioning
    ]
  }
}

4. Conditional Resources

Create resources based on conditions:

# Create load balancer only in production
resource "google_compute_global_forwarding_rule" "lb" {
  count = var.environment == "production" ? 1 : 0

  name       = "production-lb"
  target     = google_compute_target_http_proxy.lb[0].id
  port_range = "80"
}

# Using for_each with conditional
resource "google_compute_firewall" "conditional" {
  for_each = var.enable_monitoring ? toset(["http", "https"]) : toset([])

  name = "allow-${each.value}"
  # ... configuration
}

File Organization

Best Practices for Terraform File Structure:

terraform-project/
├── main.tf              # Primary resource definitions
├── variables.tf         # Input variable declarations
├── outputs.tf           # Output value declarations
├── terraform.tfvars     # Variable value assignments
├── versions.tf          # Provider version constraints
├── locals.tf            # Local value definitions
└── modules/
    ├── networking/
       ├── main.tf
       ├── variables.tf
       └── outputs.tf
    └── compute/
        ├── main.tf
        ├── variables.tf
        └── outputs.tf

Example File Contents:

versions.tf:

terraform {
  required_version = ">= 1.0"

  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "~> 4.0"
    }
  }
}

main.tf:

provider "google" {
  project = var.project_id
  region  = var.region
}

# Resource definitions...

variables.tf:

variable "project_id" {
  description = "GCP project ID"
  type        = string
}

variable "region" {
  description = "GCP region"
  type        = string
  default     = "us-central1"
}

terraform.tfvars:

project_id = "my-gcp-project"
region     = "europe-west1"

Note

Never commit terraform.tfvars files containing sensitive information to version control. Use .tfvars.example files as templates instead.