본문으로 건너뛰기 [2026] Terraform complete guide — Infrastructure as Code, multi-cloud, state & security

[2026] Terraform complete guide — Infrastructure as Code, multi-cloud, state & security

[2026] Terraform complete guide — Infrastructure as Code, multi-cloud, state & security

이 글의 핵심

Terraform is a declarative Infrastructure as Code (IaC) tool: you describe the desired state in HCL and providers create, update, and destroy cloud resources consistently. This post covers core concepts, HCL and modules, AWS/Azure/GCP providers, state and remote backends, workspaces and environment separation, security practices, and a practical configuration example.

At a glance

Terraform is an open-source Infrastructure as Code (IaC) tool from HashiCorp. Infrastructure built by clicking in a console is hard to reproduce and weak on audit trails. Terraform lets you capture the desired end state in code, compute the diff against the current state (State), and apply changes safely.

This article covers:

  • Core concepts for Terraform (providers, resources, dependencies, plan/apply, state)
  • HCL (HashiCorp Configuration Language) syntax and module design
  • Notes for AWS, Microsoft Azure, and Google Cloud providers
  • State management, remote backends, locking, and collaboration
  • Workspaces and strategies for splitting environments (dev/stage/prod)
  • Security best practices (secrets, least privilege, supply chain)
  • Hands-on example: skeleton for VPC, subnets, compute, and load balancer level

Assumptions: You have the Terraform CLI installed and a target cloud account with billing and permissions. Examples are for learning—always validate cost, quotas, and organizational security policy before applying in real environments.


1. Terraform and Infrastructure as Code

1.1 Declarative model and idempotency

Terraform is declarative. You describe what infrastructure should exist, not the order of script steps. Re-applying the same configuration aims for idempotency: if resources already match the target, there is no change or only the necessary adjustments.

In practice this helps with:

  • Reproducibility: Code in Git lets you recreate the same environment.
  • Reviewability: Infrastructure changes can go through pull requests like application code.
  • Pre-validation: terraform plan shows the blast radius before you apply.

1.2 Core building blocks

ConceptDescription
ProviderPlugin that talks to cloud APIs (AWS, Azure, GCP, etc.)
ResourceInfrastructure object to create and manage (e.g. VPC, VM, bucket)
Data sourceRead-only lookup of existing resources without creating them
StateSnapshot of resources and attributes Terraform is tracking
ModuleReusable bundle of .tf files (inputs and outputs as the interface)

Terraform analyzes references between resources to build a dependency graph and runs independent creates in parallel. Use depends_on when you need an explicit ordering edge.

1.3 Basic workflow

  1. Author: Define resource blocks (and more) in .tf files.
  2. Initialize: Run terraform init to download providers and backend modules.
  3. Plan: Run terraform plan to see what will change.
  4. Apply: Run terraform apply to reconcile real infrastructure.
  5. Destroy (optional): In lab or validation environments, terraform destroy tears resources down.

In production, teams often store plan output as an artifact, require approval, then apply—integrated with CI/CD pipelines.


2. HCL syntax and modules

2.1 terraform block and required_providers

Pinning versions lets the whole team rely on the same provider behavior. Constraints like ~> follow Semantic Versioning conventions.

terraform {
  required_version = ">= 1.5.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

This sets a minimum Terraform core version and allows AWS provider 5.x. If your org standardizes versions, enforce required_version in CI as well.

2.2 Variables, outputs, and locals

Use input variables (variable) for per-environment differences, outputs (output) to expose values to other stacks or module consumers, and locals (locals) to deduplicate expressions within a directory.

variable "environment" {
  type        = string
  description = "Deployment environment name"
}

locals {
  name_prefix = "app-${var.environment}"
}

output "name_prefix" {
  value       = local.name_prefix
  description = "Resource name prefix"
}

Habitually setting description and type helps module users avoid invalid values.

2.3 Module design

A module is a directory of Terraform configuration. The root module calls child directories with module blocks.

  • Reuse: Share network patterns as modules instead of copying files across projects.
  • Interface: Keep boundaries clear with variables.tf and outputs.tf.
  • Composition: Combine small modules (VPC, subnets, security groups) into larger stacks.

When sourcing modules from Git URLs or the Terraform Registry, pin tagged versions to fix the supply chain.

module "network" {
  source = "./modules/network"

  cidr_block = "10.0.0.0/16"
  az_count   = 3
}

Inside modules, take environment-specific strings as inputs and avoid hard-coded account IDs or regions for better portability.


3. AWS, Azure, and GCP providers

Each cloud differs in resource naming, ID rules, tagging, and IAM models. Terraform abstracts APIs, but you still need to read each provider’s documentation.

3.1 AWS (hashicorp/aws)

One of the most widely used providers. Understand region and the credential chain (environment variables, shared credentials file, IAM roles, etc.).

provider "aws" {
  region = "ap-northeast-2"
}

resource "aws_s3_bucket" "logs" {
  bucket = "example-logs-unique-suffix"
}

Practical tip: S3 bucket names must be globally unique. Production configs often add public access block, encryption, and lifecycle policies. Sensitive logs may need extra governance.

3.2 Azure (hashicorp/azurerm)

Subscription, resource group, and tenant matter. Use a service principal or managed identity for non-interactive auth from CI.

provider "azurerm" {
  features {}
}

resource "azurerm_resource_group" "main" {
  name     = "rg-example"
  location = "Korea Central"
}

Azure resources have different naming rules and limits; validating inputs in modules (e.g. string length) reduces failed deploys.

3.3 Google Cloud (hashicorp/google)

Project ID, region/zone, and enabling service APIs often come first.

provider "google" {
  project = var.gcp_project_id
  region  = "asia-northeast3"
}

resource "google_storage_bucket" "assets" {
  name          = "${var.gcp_project_id}-assets-unique"
  location      = "ASIA-NORTHEAST3"
  force_destroy = false
}

On GCP, split IAM to least privilege and check alignment with VPC Service Controls, organization policies, and other guardrails.

3.4 Operating multi-cloud

Even with the same HCL “syntax,” resource semantics differ per cloud. Align internal vocabulary (“what counts as a VPC”) and document networking, identity, and billing per provider.


4. State management and backends

4.1 Why state exists

Declarative code alone does not tell Terraform every real cloud ID. State maps Terraform resources to remote IDs and caches some attributes. Missing or stale state makes plans inaccurate and raises the risk of duplicate creates or wrong deletes.

4.2 Remote backends and locking

A local terraform.tfstate file is fine for solo learning; teams usually move to a remote backend.

Backend exampleLockingNotes
AWS S3 + DynamoDBDynamoDB table for locksVery common pattern
Azure StorageBlob lockingSee azurerm backend docs
GCSLocking provided by GCSDesign IAM alongside
Terraform Cloud / HCP TerraformSaaS locks and RBACStrong for team policy

Example remote backend (S3, conceptual):

terraform {
  backend "s3" {
    bucket         = "my-org-terraform-state"
    key            = "prod/network/terraform.tfstate"
    region         = "ap-northeast-2"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

Use a separate key per stack to reduce state collisions. Encryption, versioning, and access logging are close to mandatory for operations.

4.3 Splitting state and terraform import

One giant state increases plan time and change blast radius. Split state by team or domain, and connect stacks loosely—e.g. only shared services (DNS, hub VPC)—via terraform_remote_state. Resources created in the console can be brought into state with terraform import, but you still need to align code with reality.


5. Workspaces and environment separation

5.1 Workspaces

Workspaces are a built-in way to keep multiple states for the same configuration. Create with terraform workspace new staging, and reference terraform.workspace for naming.

Pros: quick environment splits when config stays simple. Cons: mistakes splitting backend keys, or switching workspace on the same branch and applying to production by mistake.

5.2 Directories, branches, and pipelines

Many teams split root modules like env/dev and env/prod, with different backends, different variable files (terraform.tfvars), and different approval gates. Restricting production applies to merges into main is safer.

5.3 Variable files and secrets

Do not put passwords or API keys in .tf files. Combine environment variables, CI secrets, cloud secret managers (AWS Secrets Manager, Azure Key Vault, GCP Secret Manager), and external data sources. Design sensitive resources so secrets do not linger in state unnecessarily.


6. Security best practices

6.1 Least privilege and separation of duties

Grant the CI service account only the minimum IAM needed for the stack. Letting developers apply to production with personal credentials hurts auditability.

6.2 Secrets and state

  • State files may hold sensitive data—use encryption at rest, access control, and audit logs.
  • Do not commit secrets in .tfvars. Use .gitignore and pre-commit hooks to block accidents.

6.3 Supply chain and module sources

  • Prefer the official registry and verified modules; for Git sources, pin commit hashes or tags.
  • Constrain provider versions for reproducible plans.

6.4 Policy as code

At scale, Sentinel (Terraform Enterprise/Cloud) or OPA (Open Policy Agent) enforces rules like “no public S3” or “mandatory tags.”


7. Hands-on infrastructure example

Below is a shortened AWS example for learning. Real operations add subnet CIDR design, NAT cost, load balancer health checks, WAF, backup policies, and more.

7.1 Target shape

  • VPC with public and private subnets
  • Application tier in private subnets; exposure only via load balancer (conceptual)
  • Security groups allowing only required ports

7.2 Network and ALB skeleton (example)

# Example: for conceptual understanding. Adjust CIDR, AZs, and names to your environment before real deployment.

data "aws_availability_zones" "available" {
  state = "available"
}

resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  tags = {
    Name = "app-vpc"
  }
}

resource "aws_subnet" "public" {
  count                   = 2
  vpc_id                  = aws_vpc.main.id
  cidr_block              = cidrsubnet(aws_vpc.main.cidr_block, 4, count.index)
  availability_zone       = data.aws_availability_zones.available.names[count.index]
  map_public_ip_on_launch = true
  tags = {
    Name = "public-${count.index}"
  }
}

resource "aws_subnet" "private" {
  count             = 2
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(aws_vpc.main.cidr_block, 4, count.index + 8)
  availability_zone = data.aws_availability_zones.available.names[count.index]
  tags = {
    Name = "private-${count.index}"
  }
}

resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.main.id
}

resource "aws_lb" "app" {
  name               = "app-alb"
  load_balancer_type = "application"
  subnets            = aws_subnet.public[*].id
  security_groups    = [aws_security_group.alb.id]
}

resource "aws_security_group" "alb" {
  name        = "alb-sg"
  description = "ALB ingress"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

This uses cidrsubnet to carve subnets and count for per-AZ repetition. An internet gateway alone does not give private subnets outbound internet; add NAT gateways and route tables (with cost). In production, trade off single-AZ NAT vs multi-AZ NAT cost against availability.

Application tiers often run on aws_instance or container orchestration (ECS/EKS). Whether Terraform owns clusters, node groups, and IRSA in one stack depends on where you draw the line between platform and application teams.

7.3 Moving to Azure or GCP

  • Azure: Map to azurerm_virtual_network, azurerm_subnet, azurerm_lb, etc., with consistent resource groups and locations.
  • GCP: Similar patterns with google_compute_network, google_compute_subnetwork, google_compute_forwarding_rule, or global load balancer resources.

Even for the same architecture, load balancers, health checks, and firewall models differ—per-cloud modules plus a reference architecture doc usually beats forcing one fake abstraction layer.


8. Operations checklist

  • Remote state, locking, encryption, and least-privilege IAM in place?
  • Production applies only through CI and approval?
  • Module and provider versions pinned for reproducibility?
  • Cost alerts, tag policies, and deletion protection (S3 versioning, RDS deletion protection, etc.) defined?
  • Process to investigate when terraform plan diverges from expectations (manual changes, drift)?

Closing thoughts

Terraform lets you treat infrastructure like software. Clear structure with HCL and modules, safe collaboration with state and backends, and deliberate workspace or directory strategies for environments raise operational maturity quickly. Whichever of AWS, Azure, or GCP you use, the habit of reading provider docs alongside organizational security standards matters most.

If you already have a shorter practical post (e.g. terraform-practical-guide), treat this article as the more systematic reference tying multi-cloud, security, backends, and environment separation together.