LLM WikiAccess-protected knowledge portal
← 스터디 홈
1편 · 약 17분

Terraform 기초와 IaC 개념

수동 관리에서 IaC로

서버 한두 대는 콘솔에서 직접 클릭하거나 SSH로 접속해 명령어를 입력해도 된다. 그러나 환경이 개발·스테이징·프로덕션 세 개로 늘어나고, 각 환경에 수십 개의 리소스가 생기면 수작업 관리는 한계에 부딪힌다.

Infrastructure as Code(IaC)는 인프라 상태를 코드로 선언하고, 코드를 실행해 원하는 상태를 만들어 내는 방식이다. 세 가지 문제를 해결한다.

  1. 재현성: 코드 한 번으로 동일한 환경을 여러 번 만들 수 있다. 개발 환경과 프로덕션이 구성상 동일하다고 보장된다.
  2. 버전 관리: Git에 커밋하면 누가 언제 무엇을 바꿨는지 추적된다. 인프라 변경도 코드 리뷰 프로세스를 거친다.
  3. 협업: 변경 전 plan 결과를 PR에 올리면 팀원이 리소스 추가·삭제 여부를 사전에 검토할 수 있다.

IaC 도구는 크게 두 방식으로 나뉜다.

방식특징대표 도구
선언형(Declarative)원하는 최종 상태를 기술; 도구가 현재 상태와 비교해 필요한 작업을 결정Terraform, Pulumi, CloudFormation
명령형(Imperative)실행할 절차를 순서대로 기술Ansible, Bash 스크립트

Terraform은 선언형이다. "EC2 인스턴스가 2개 있어야 한다"고 선언하면, 현재 0개라면 2개를 만들고, 이미 2개라면 아무것도 하지 않는다. 멱등성(idempotency)이 보장된다. 같은 코드를 몇 번 실행해도 결과는 항상 같다.

Terraform이란

Terraform은 HashiCorp가 만든 오픈소스 IaC 도구다. 2023년 HashiCorp가 라이선스를 BSL(Business Source License)로 변경하면서 커뮤니티 포크인 OpenTofu가 등장해 현재 두 가지 선택지가 있다. 두 도구의 문법과 핵심 동작은 거의 동일하다.

Provider 생태계가 Terraform의 최대 강점이다. AWS, GCP, Azure 같은 메이저 클라우드뿐 아니라 Kubernetes, Datadog, GitHub, Cloudflare, MySQL 등 수천 개의 Provider가 존재한다. 각 Provider는 해당 플랫폼의 API를 Terraform 리소스 블록으로 감싸 준다. 클라우드 인프라와 그 위에 올라가는 애플리케이션 설정(DNS 레코드, 모니터링 대시보드, GitHub 저장소 설정 등)을 동일한 Terraform 코드베이스에서 관리할 수 있다.

HCL 문법 기초

Terraform 설정은 HashiCorp Configuration Language(HCL)로 작성한다. 확장자는 .tf다. HCL은 JSON보다 사람이 읽기 쉽고, 주석(#, //, /* */)과 표현식을 지원한다.

모든 HCL 코드는 블록(Block)으로 구성된다.

블록_타입 "타입_레이블" "이름_레이블" {
  인수 = 값
}

terraform 블록

Terraform 자체의 동작을 설정한다. 필요한 Provider 버전과 백엔드를 여기서 선언한다.

terraform {
  required_version = ">= 1.6"

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

  backend "s3" {
    bucket = "my-tf-state"
    key    = "prod/terraform.tfstate"
    region = "ap-northeast-2"
  }
}

version = "~> 5.0"은 "5.x 범위는 허용하되 6.0은 허용하지 않는다"는 의미다. Provider 버전을 고정하지 않으면 팀원마다 다른 버전을 쓰는 문제가 생긴다.

provider 블록

Provider를 초기화하고 인증 정보를 설정한다.

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

AWS Provider는 기본적으로 환경변수(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY) 또는 ~/.aws/credentials를 읽는다. 인증 정보를 코드에 직접 쓰지 않는다.

resource 블록

실제로 생성·관리할 인프라 리소스를 선언한다. 가장 많이 쓰는 블록이다.

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

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

resource "aws_instance" "web"에서 aws_instance는 Provider가 정의한 리소스 타입, web은 이 설정 파일 내에서 해당 리소스를 가리키는 로컬 이름이다. 다른 블록에서 aws_instance.web.id처럼 속성을 참조할 수 있다.

variable 블록

설정에서 변경될 수 있는 값을 외부에서 주입받는다.

variable "instance_type" {
  description = "EC2 인스턴스 타입"
  type        = string
  default     = "t3.micro"
}

resource "aws_instance" "web" {
  instance_type = var.instance_type
  # ...
}

변수는 terraform apply -var="instance_type=t3.large" 또는 terraform.tfvars 파일, 환경변수(TF_VAR_instance_type)로 전달한다. 민감한 변수는 sensitive = true를 추가하면 plan 출력에서 가려진다.

output 블록

apply 후 생성된 리소스의 속성을 출력하거나 다른 모듈에서 참조할 수 있게 노출한다.

output "instance_public_ip" {
  value       = aws_instance.web.public_ip
  description = "웹 서버의 퍼블릭 IP"
}

terraform output instance_public_ip로 언제든 값을 확인할 수 있다.

data 블록

Terraform 외부에 이미 존재하는 리소스를 조회한다. 새로 만들지 않고 읽기만 한다.

data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]   # Canonical

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-*-22.04-amd64-server-*"]
  }
}

resource "aws_instance" "web" {
  ami = data.aws_ami.ubuntu.id
  # ...
}

기존에 수동으로 만든 VPC, 보안 그룹, AMI 등을 data 블록으로 참조하면 코드를 처음부터 다시 쓰지 않아도 된다.

핵심 워크플로우

Terraform의 실행은 네 단계로 이루어진다.

terraform init Provider 플러그인 다운로드 · 초기화 terraform plan State와 비교 → 변경 예측 (읽기 전용) terraform apply 리소스 생성·수정·삭제 State 파일 업데이트 terraform destroy 모든 리소스 삭제 (신중하게 사용)
Terraform 핵심 워크플로우

terraform init: Provider 플러그인을 Terraform Registry에서 다운로드하고 백엔드를 초기화한다. .terraform/ 디렉터리와 .terraform.lock.hcl 파일이 생성된다. 새 Provider를 추가하거나 백엔드 설정을 바꿀 때마다 다시 실행해야 한다. .terraform.lock.hcl은 Git에 커밋해 팀 전체가 동일한 Provider 버전을 쓰게 한다.

terraform plan: 코드의 선언 상태와 현재 State 파일을 비교해 무엇이 만들어지고(+), 변경되고(~), 삭제될지(-)를 미리 보여준다. 실제 인프라를 건드리지 않는다. 코드 리뷰처럼 변경 사항을 확인하는 단계다. 예상치 않은 삭제(-)가 보이면 즉시 멈추고 원인을 확인해야 한다.

Terraform will perform the following actions:

  + aws_instance.web    (신규 생성)
      ami           = "ami-0c2d3e23e757b5d84"
      instance_type = "t3.micro"

  ~ aws_security_group.web    (수정)
      - ingress.0.from_port = 80
      + ingress.0.from_port = 443

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

terraform apply: plan의 결과를 실제 인프라에 적용한다. 기본적으로 변경 사항을 다시 보여주고 "yes"를 입력받는다. -auto-approve 플래그로 CI/CD에서 자동화할 수 있다. apply 완료 후 State 파일이 새 상태로 업데이트된다.

terraform destroy: 모든 리소스를 삭제한다. 개발 환경을 통째로 종료할 때 유용하지만, 프로덕션에서는 매우 신중하게 써야 한다.

State 파일: 인프라의 현재 상태

terraform.tfstate는 Terraform이 관리하는 리소스의 현재 상태를 JSON으로 기록한 파일이다. plan을 실행할 때 Terraform은 코드와 State 파일을 비교해 변경해야 할 것을 계산한다.

State 파일에는 민감 정보(비밀번호, 인증서)가 포함될 수 있어 절대 Git에 커밋하지 않는다. .gitignore*.tfstate, *.tfstate.backup을 추가하는 것이 기본이다.

로컬 State의 문제

로컬 State(terraform.tfstate가 작업 디렉터리에 저장)는 혼자 쓸 때는 문제가 없다. 팀 작업에서는 두 사람이 동시에 apply하면 State 파일이 충돌하고 인프라가 꼬인다.

원격 백엔드(Remote Backend)는 State를 팀이 공유할 수 있는 외부 저장소에 보관한다. AWS S3가 가장 흔한 선택이다.

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

State 잠금(Locking): apply가 실행 중인 동안 다른 사람이 동시에 apply하면 State가 망가진다. DynamoDB 테이블을 잠금 메커니즘으로 쓴다. apply를 시작하면 DynamoDB에 잠금 레코드를 기록하고, 완료되면 해제한다. 잠금 상태에서 다른 사람이 apply를 시도하면 "State is currently locked" 오류가 발생한다.

Terraform 1.10부터 S3 네이티브 잠금이 지원돼 DynamoDB 없이도 S3만으로 잠금이 가능하다.

개발자 A
terraform apply
개발자 B
apply → 대기
S3 Remote Backend terraform.tfstate
인프라 현재 상태 (JSON)
+
DynamoDB (Lock Table) LockID
동시 apply 방지
State 관리 흐름: 팀 협업

모듈: 재사용 가능한 블록

반복되는 인프라 패턴을 모듈(Module)로 추출하면 코드 중복을 줄일 수 있다. 모듈은 variable, resource, output을 담은 디렉터리다.

modules/
└── web-server/
    ├── main.tf        # resource 블록들
    ├── variables.tf   # variable 블록들
    └── outputs.tf     # output 블록들

호출 측에서는 module 블록으로 사용한다.

module "web_prod" {
  source        = "./modules/web-server"
  instance_type = "t3.large"
  env           = "production"
}

module "web_staging" {
  source        = "./modules/web-server"
  instance_type = "t3.micro"
  env           = "staging"
}

source는 로컬 경로, Git URL, Terraform Registry 주소를 모두 지원한다. registry.terraform.io에는 AWS VPC, EKS 클러스터 같은 검증된 커뮤니티 모듈이 올라와 있다.

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"

  name = "my-vpc"
  cidr = "10.0.0.0/16"
}

워크스페이스

terraform workspace로 동일한 코드를 여러 State와 대응시킬 수 있다.

terraform workspace new staging
terraform workspace select production
terraform workspace list
# * production
#   staging

각 워크스페이스는 독립된 State를 가진다. 코드에서 현재 워크스페이스 이름을 참조할 수도 있다.

locals {
  instance_type = terraform.workspace == "production" ? "t3.large" : "t3.micro"
}

소규모 팀에서 환경 분리를 빠르게 구현하는 방법이지만, 규모가 커지면 환경별 디렉터리와 별도 백엔드로 분리하는 것이 관리가 더 쉽다.

실무 체크리스트

항목확인 내용
State 파일.gitignore에 추가; S3 + DynamoDB 원격 백엔드로 팀 공유
plan 습관apply 전 plan 출력 확인; 예상치 않은 삭제(-)가 없는지 검토
민감 변수sensitive = true; 값은 TF_VAR_xxx 환경변수 또는 Vault
Provider 버전 고정~> 5.0으로 마이너 버전 고정; .terraform.lock.hcl Git 커밋
모듈화반복 패턴은 모듈로 추출; 모듈 버전은 Git 태그로 관리
CI/CD 통합PR 단계에서 plan 출력 자동 게시; main 머지 시 apply
destroy 보호중요 리소스에 prevent_destroy = true lifecycle 설정

한 줄 정리

Terraform은 HCL로 원하는 인프라 상태를 선언하면 init → plan → apply 워크플로우로 클라우드 리소스를 생성·변경·삭제하며, State 파일과 원격 백엔드로 팀이 인프라를 안전하게 협업 관리한다.

References

  • https://developer.hashicorp.com/terraform/docs
  • https://developer.hashicorp.com/terraform/language
  • https://developer.hashicorp.com/terraform/language/state
  • https://developer.hashicorp.com/terraform/language/backend
  • https://developer.hashicorp.com/terraform/language/modules
  • https://spacelift.io/blog/terraform-s3-backend
  • https://spacelift.io/blog/terraform-remote-state
  • https://mvineetsharma.medium.com/terraform-core-the-iac-engine-configuration-language-providers-state-iac-unlocked-745620a79779
  • https://www.geeksforgeeks.org/devops/what-is-terraform-configuration-language-hcl/
  • https://scalr.com/learning-center/what-is-terraform