[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 planshows the blast radius before you apply.
1.2 Core building blocks
| Concept | Description |
|---|---|
| Provider | Plugin that talks to cloud APIs (AWS, Azure, GCP, etc.) |
| Resource | Infrastructure object to create and manage (e.g. VPC, VM, bucket) |
| Data source | Read-only lookup of existing resources without creating them |
| State | Snapshot of resources and attributes Terraform is tracking |
| Module | Reusable 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
- Author: Define
resourceblocks (and more) in.tffiles. - Initialize: Run
terraform initto download providers and backend modules. - Plan: Run
terraform planto see what will change. - Apply: Run
terraform applyto reconcile real infrastructure. - Destroy (optional): In lab or validation environments,
terraform destroytears 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.tfandoutputs.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 example | Locking | Notes |
|---|---|---|
| AWS S3 + DynamoDB | DynamoDB table for locks | Very common pattern |
| Azure Storage | Blob locking | See azurerm backend docs |
| GCS | Locking provided by GCS | Design IAM alongside |
| Terraform Cloud / HCP Terraform | SaaS locks and RBAC | Strong 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.gitignoreand 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 plandiverges 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.