# Terraform Basics

## 1. The Building Blocks

Terraform (and its open-source fork **OpenTofu**, CLI `tofu`, compatible syntax) reads `*.tf` files in a directory — a **root module** — written in **HCL**.

```hcl
terraform {
  required_version = ">= 1.6"
  required_providers {
    aws = { source = "hashicorp/aws", version = "~> 5.0" }   # pin providers
  }
}

provider "aws" {
  region = "eu-west-1"
  default_tags { tags = { project = "petpal", managed_by = "terraform" } }
}

resource "aws_s3_bucket" "uploads" {          # resource "<TYPE>" "<LOCAL NAME>"
  bucket = "petpal-prod-uploads"
}

resource "aws_s3_bucket_public_access_block" "uploads" {
  bucket                  = aws_s3_bucket.uploads.id   # reference → implicit dependency
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

data "aws_caller_identity" "current" {}        # data source: READ existing info

output "bucket_arn" {
  value = aws_s3_bucket.uploads.arn
}
```

| Block | Purpose |
|---|---|
| `terraform` | Terraform version, required providers, backend |
| `provider` | configures a plugin that talks to an API (AWS, GitHub, Cloudflare…) |
| `resource` | something Terraform **creates and manages** |
| `data` | something Terraform **reads** but doesn't manage (an AMI ID, an existing VPC, the account ID) |
| `variable` / `output` / `locals` | inputs, outputs, computed values (`04-variables_outputs_and_locals/`) |
| `module` | reuse a group of resources (`05-modules/`) |

Addresses: `aws_s3_bucket.uploads` (resource), `data.aws_caller_identity.current`, `module.vpc.aws_subnet.private[0]`.

## 2. The Workflow

```bash
terraform init        # download providers/modules, configure the backend (.terraform/, .terraform.lock.hcl)
terraform fmt         # canonical formatting
terraform validate    # syntax + internal consistency (no API calls)
terraform plan -out=tfplan   # compute the diff against real infrastructure
terraform apply tfplan       # execute exactly that plan
terraform destroy     # remove everything this configuration manages
```

Plan symbols: `+` create · `~` update in place · `-/+` **replace** (destroy then create) · `+/-` create-before-destroy · `-` destroy · `<=` read data source. **Always read the plan** — a `-/+` on a database or a `-` on a DNS zone is how outages happen.

Commit `.terraform.lock.hcl` (pins exact provider versions and checksums); never commit `.terraform/` or state files.

## 3. Expressions You'll Use Daily

```hcl
resource "aws_instance" "web" {
  count         = var.web_count                         # N copies: aws_instance.web[0..N-1]
  ami           = data.aws_ami.al2023.id
  instance_type = var.env == "prod" ? "m7g.large" : "t4g.small"
  subnet_id     = element(var.private_subnet_ids, count.index)
  tags          = merge(local.common_tags, { Name = "web-${count.index}" })
}

resource "aws_iam_user" "devs" {
  for_each = toset(["ana", "bo", "cy"])                  # keyed copies: aws_iam_user.devs["ana"]
  name     = each.key
}

locals {
  subnet_cidrs = [for i in range(3) : cidrsubnet(var.vpc_cidr, 8, i)]   # for expression + built-in function
}
```

- Prefer **`for_each` over `count`** for collections: removing an item from the middle of a `count` list shifts indexes and causes unrelated resources to be replaced; `for_each` keys are stable.
- `dynamic` blocks generate repeated nested blocks (e.g. several `ingress` rules).
- Functions: `cidrsubnet`, `merge`, `lookup`, `coalesce`, `jsonencode`, `templatefile`, `try`, `toset`…

## 4. Dependencies and Lifecycle

- **Implicit dependencies** come from references (`aws_s3_bucket.uploads.id`). Use `depends_on` only for hidden dependencies (e.g. an IAM policy must exist before a Lambda can use it, without a direct reference).
- `lifecycle` meta-argument:

```hcl
resource "aws_db_instance" "main" {
  # ...
  lifecycle {
    prevent_destroy       = true            # plan fails if anything would destroy it
    create_before_destroy = false
    ignore_changes        = [password]      # managed elsewhere (rotation)
  }
}
```

## 5. Bringing Existing Resources Under Management

```hcl
import {
  to = aws_s3_bucket.legacy
  id = "petpal-legacy-bucket"
}
```

`terraform plan -generate-config-out=generated.tf` writes starter HCL for imported resources; refine it until the plan shows **no changes**. Use `moved {}` blocks to rename/refactor addresses without destroying resources, and `removed {}` (or `terraform state rm`) to stop managing something without deleting it.

## Under the Hood: Providers Are Separate Programs

Terraform core knows nothing about AWS. Each **provider** is a separate executable (downloaded by `init` into `.terraform/providers/`) that Terraform launches as a child process and talks to over **gRPC**. Core parses HCL, builds the dependency graph, and for each resource asks the provider to `PlanResourceChange` (given prior state and config, what will change? which attributes force replacement?) and later `ApplyResourceChange`, which makes the real API calls (e.g. `s3:CreateBucket`) and returns the new attribute values. Values not known until apply (an ARN, a generated ID) appear in the plan as `(known after apply)`. The provider's **schema** — every resource type, attribute, and which changes require replacement — is what powers `validate`, plan diffs and editor auto-completion. That separation is why one tool can manage AWS, GitHub teams and Datadog monitors in a single plan.

![Terraform core parses HCL into a dependency graph, refreshes state, asks provider plugins over gRPC to plan each resource change, shows the plan, and applies it in dependency order while updating state](diagrams/01-core-and-providers.svg)

## Common Pitfalls

1. **Unpinned provider versions** — a new major version changes behaviour under you.
2. **`count` for maps of things** — index shifts cause replacements; use `for_each`.
3. **Not reading `-/+` lines** in the plan.
4. **Committing `.terraform/` or `terraform.tfstate`**.
5. **Overusing `depends_on`** — slows plans and hides real dependencies.
6. **Renaming resources without `moved` blocks** — Terraform destroys and recreates them.

## Quick Reference

```
init → fmt → validate → plan -out → apply plan · destroy
resource (manage) · data (read) · provider (API plugin) · output
for_each > count · lifecycle { prevent_destroy, ignore_changes, create_before_destroy }
import {} + -generate-config-out · moved {} · removed {}
commit .terraform.lock.hcl; never state or .terraform/
```
