coredot.today
Terraform 입문: 인프라를 코드로 관리하는 시대
블로그로 돌아가기
TerraformIaCInfrastructure as CodeHashiCorpDevOps클라우드

Terraform 입문: 인프라를 코드로 관리하는 시대

AWS 콘솔에서 클릭 100번 vs HCL 코드 한 파일. Infrastructure as Code의 핵심 도구 Terraform의 개념부터 실전 예제, State 관리, 모듈화, 그리고 BSL 라이선스 논란까지 — 클라우드 입문자를 위한 완전 가이드.

코어닷투데이2026-04-0175

들어가며: 콘솔에서 클릭 100번 vs 코드 한 파일

AWS를 처음 써본 날을 떠올려보자.

EC2 인스턴스 하나 만드는 데 VPC를 만들고, 서브넷을 만들고, 인터넷 게이트웨이를 연결하고, 라우팅 테이블을 설정하고, 보안 그룹을 만들고, 키 페어를 생성하고... 콘솔 화면을 수십 번 클릭한다. 마침내 인스턴스가 뜨면 뿌듯하다.

그런데 이제 같은 환경을 10개 더 만들어야 한다면? 아니면 3개월 뒤에 "그때 설정 어떻게 했더라?" 싶은 순간이 온다면?

P
문제: 수동 인프라 관리의 한계
콘솔 클릭으로 만든 인프라는 기록이 남지 않는다. 누가, 언제, 왜 이 설정을 바꿨는지 추적할 수 없다. 환경을 복제하려면 처음부터 다시 클릭해야 하고, 실수가 하나라도 생기면 장애로 이어진다.
S
해결: 인프라를 코드로 선언한다
"서버 2대, 데이터베이스 1대, 로드밸런서 1대"를 코드 파일 하나에 적는다. Git으로 버전 관리하고, 코드 리뷰를 거쳐 배포한다. 동일한 코드를 실행하면 항상 동일한 인프라가 만들어진다.
R
결과: 재현 가능하고, 추적 가능하고, 자동화 가능
개발 환경과 프로덕션 환경이 정확히 같은 코드에서 나온다. 변경 이력은 Git 커밋에 남고, CI/CD 파이프라인으로 자동 배포된다. 이것이 Infrastructure as Code(IaC)다.

이 글은 IaC의 대표 도구 Terraform을 처음 접하는 사람을 위한 가이드다. HCL 문법 기초부터 실전 예제, State 관리, 모듈화, 그리고 라이선스 논란까지 한 번에 정리한다.

AWS 콘솔 클릭 100번 vs Terraform 코드 한 파일 비교 일러스트


1. IaC(Infrastructure as Code) 개념: 왜 인프라를 코드로?

수동 관리 vs 코드 관리

인프라를 관리하는 방식은 크게 두 가지다.

비교 항목수동 관리 (ClickOps)코드 관리 (IaC)
환경 복제처음부터 수동 반복같은 코드 실행
변경 추적CloudTrail 로그 뒤지기Git 커밋 히스토리
코드 리뷰불가능PR에서 diff 확인
롤백수동 복원 (가능하면)이전 커밋으로 apply
일관성사람마다 다른 결과항상 동일한 결과
문서화별도로 작성해야 함코드 자체가 문서

IaC의 두 가지 접근법

IaC 도구들은 크게 선언형(Declarative)명령형(Imperative)으로 나뉜다.

  • 선언형: "나는 EC2 인스턴스 3대가 필요해." 도구가 현재 상태를 확인하고, 부족한 만큼만 만든다. Terraform, CloudFormation이 이 방식이다.
  • 명령형: "EC2 인스턴스를 1대 생성해. 또 1대 생성해. 또 1대 생성해." 절차를 직접 지시한다. Ansible, Pulumi(부분적)가 이 방식이다.

선언형의 장점: "원하는 최종 상태"만 기술하면 도구가 알아서 현재 상태와의 차이를 계산한다. 이미 3대가 있으면 아무것도 하지 않고, 2대만 있으면 1대를 추가한다. 이것을 멱등성(idempotency)이라 한다.


2. Terraform의 탄생: HashiCorp와 Mitchell Hashimoto

창시자 이야기

Mitchell Hashimoto는 대학 시절부터 인프라 자동화에 관심이 많았다. 2010년에 Vagrant(로컬 개발 환경 자동화 도구)를 만들었고, 이 경험을 바탕으로 2012년에 HashiCorp를 공동 창업한다.

HashiCorp의 비전은 명확했다 — "클라우드 인프라 자동화의 모든 단계를 도구로 만든다."

2010 Vagrant 출시 — 로컬 개발 환경을 코드로 정의 (VirtualBox 기반)
2012 HashiCorp 설립 — Mitchell Hashimoto & Armon Dadgar가 공동 창업
2013 Packer (머신 이미지 빌드), Serf (클러스터 멤버십) 출시
2014.07 Terraform v0.1 출시 — "Write, Plan, Create Infrastructure as Code"
2017 Terraform v0.10 — Provider를 별도 바이너리로 분리, 생태계 확장 시작
2021 Terraform v1.0 — 안정성 보증(Stable). 7년 만의 정식 1.0 릴리스
2024 IBM이 HashiCorp를 인수 — 약 64억 달러 규모

Terraform이 출시될 당시(2014년), AWS의 CloudFormation은 이미 존재했다. 하지만 CloudFormation은 AWS에서만 작동했다. Terraform은 처음부터 멀티 클라우드를 목표로 설계되었다. AWS, Azure, GCP를 하나의 도구로 관리한다는 것 — 이것이 Terraform의 킬러 피처였다.


3. Terraform vs CloudFormation vs Pulumi vs CDK

IaC 도구는 여러 가지가 있다. 각각의 특징을 비교해보자.

비교 항목TerraformCloudFormationPulumiCDK
개발사HashiCorp (IBM)AWSPulumi Inc.AWS
언어HCL (전용 DSL)JSON / YAMLPython, TS, Go 등Python, TS 등
멀티 클라우드지원 (3,000+ Provider)AWS 전용지원AWS 전용
State 관리자체 tfstate 파일AWS가 자동 관리Pulumi Cloud 또는 자체CloudFormation 위임
접근 방식선언형선언형명령형 + 선언형명령형 → CFn 변환
학습 곡선중간 (HCL 학습 필요)높음 (JSON/YAML 장황)낮음 (기존 언어 사용)중간
커뮤니티가장 큼AWS 생태계 내성장 중AWS 생태계 내
라이선스BSL 1.1 (2023~)프로프리어터리Apache 2.0Apache 2.0

Terraform을 선택하는 이유: 멀티 클라우드 지원, 방대한 Provider 생태계(AWS, Azure, GCP, Kubernetes, Datadog, GitHub 등 3,000개 이상), 그리고 업계에서 가장 큰 커뮤니티와 레퍼런스. 한국 채용 공고에서 IaC를 언급하면 대부분 Terraform이다.

CloudFormation을 선택하는 이유: AWS 올인(all-in) 환경에서는 State 관리를 AWS가 대신 해주므로 운영 부담이 적다. AWS 신규 서비스를 가장 빨리 지원한다.

Pulumi를 선택하는 이유: HCL을 새로 배우기 싫고, Python이나 TypeScript 같은 기존 프로그래밍 언어로 인프라를 정의하고 싶을 때. 조건문, 반복문, 테스트를 자연스럽게 쓸 수 있다.


4. HCL 문법 기초: Terraform의 언어

Terraform은 HCL(HashiCorp Configuration Language)이라는 전용 언어를 사용한다. JSON보다 읽기 쉽고, 프로그래밍 언어보다 단순하다. 핵심 블록 5가지를 알면 기본은 끝이다.

4.1 Provider — 어떤 클라우드를 쓸 것인가

Provider는 Terraform이 어떤 API와 통신할지 정의한다. AWS, Azure, GCP, Kubernetes 등 각 서비스마다 Provider가 있다.

hljs language-hcl
# main.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
  required_version = ">= 1.7.0"
}

provider "aws" {
  region = "ap-northeast-2"  # 서울 리전
}

~> 5.0은 "5.x 범위 내에서 최신 버전"이라는 의미다. 5.1, 5.23은 허용하지만, 6.0은 허용하지 않는다.

4.2 Resource — 무엇을 만들 것인가

Resource는 실제로 생성할 인프라 리소스를 정의한다. Terraform의 핵심 블록이다.

hljs language-hcl
resource "aws_instance" "web" {
  ami           = "ami-0c9c942bd7bf113a2"
  instance_type = "t3.micro"

  tags = {
    Name = "my-first-terraform-server"
  }
}

"aws_instance"는 리소스 타입(AWS EC2 인스턴스), "web"은 Terraform 내부에서 사용하는 이름이다. 이 이름으로 다른 블록에서 aws_instance.web.id처럼 참조한다.

4.3 Variable — 값을 외부에서 주입하기

하드코딩을 피하기 위해 변수를 사용한다.

hljs language-hcl
# variables.tf
variable "instance_type" {
  description = "EC2 인스턴스 타입"
  type        = string
  default     = "t3.micro"
}

variable "environment" {
  description = "환경 (dev, staging, prod)"
  type        = string
  
  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "environment는 dev, staging, prod 중 하나여야 합니다."
  }
}
hljs language-hcl
# 사용
resource "aws_instance" "web" {
  instance_type = var.instance_type
  # ...
}

변수 값은 terraform.tfvars 파일, 환경 변수(TF_VAR_instance_type), 또는 CLI 플래그(-var="instance_type=t3.small")로 전달한다.

4.4 Output — 결과값을 출력하기

생성된 리소스의 정보를 외부로 노출한다.

hljs language-hcl
# outputs.tf
output "instance_public_ip" {
  description = "EC2 인스턴스의 퍼블릭 IP"
  value       = aws_instance.web.public_ip
}

output "instance_id" {
  description = "EC2 인스턴스 ID"
  value       = aws_instance.web.id
}

terraform apply 후 콘솔에 출력되고, 다른 모듈에서 참조할 수도 있다.

4.5 Data — 이미 존재하는 리소스 조회하기

Data 블록은 리소스를 만드는 것이 아니라, 이미 존재하는 리소스를 읽어오는 블록이다.

hljs language-hcl
# 최신 Amazon Linux 2023 AMI를 자동으로 조회
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-*-x86_64"]
  }
}

resource "aws_instance" "web" {
  ami           = data.aws_ami.amazon_linux.id  # 조회 결과 사용
  instance_type = var.instance_type
}

AMI ID를 하드코딩하지 않고, 항상 최신 AMI를 자동으로 사용할 수 있다.

HCL 5대 블록 요약

Terraform HCL 핵심 블록
provider 어디에 AWS, Azure, GCP 등 대상 지정
resource 무엇을 실제 인프라 리소스 생성
variable 입력 외부에서 값을 주입
output 출력 생성 결과를 노출
data 조회 기존 리소스 정보 읽기

5. Terraform 워크플로우: init → plan → apply → destroy

Terraform plan을 건축 설계도에 비유한 일러스트

Terraform의 작업 흐름은 단 4개의 명령어로 돌아간다. 이 흐름을 이해하면 Terraform의 절반을 이해한 것이다.

terraform init
terraform plan
terraform apply
terraform destroy

5.1 terraform init — 초기화

프로젝트 디렉토리에서 가장 먼저 실행하는 명령어다. Provider 플러그인을 다운로드하고, Backend를 설정하고, 모듈을 가져온다.

hljs language-bash
$ terraform init

Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 5.0"...
- Installing hashicorp/aws v5.82.2...
- Installed hashicorp/aws v5.82.2 (signed by HashiCorp)

Terraform has been successfully initialized!

.terraform/ 디렉토리가 생기는데, 이 디렉토리는 .gitignore에 넣어야 한다. Provider 바이너리가 수백 MB에 달하기 때문이다.

5.2 terraform plan — 실행 계획 미리보기

실제로 아무것도 변경하지 않고, 현재 상태와 코드의 차이를 보여준다. "이걸 적용하면 무슨 일이 벌어질까?"를 미리 확인하는 단계다.

hljs language-bash
$ terraform plan

Terraform will perform the following actions:

  # aws_instance.web will be created
  + resource "aws_instance" "web" {
      + ami                    = "ami-0c9c942bd7bf113a2"
      + instance_type          = "t3.micro"
      + id                     = (known after apply)
      + public_ip              = (known after apply)
      + tags                   = {
          + "Name" = "my-first-terraform-server"
        }
    }

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

+는 생성, ~는 수정, -는 삭제를 의미한다. PR을 올릴 때 terraform plan 결과를 같이 첨부하면 코드 리뷰어가 "이 코드가 실제로 무슨 변경을 일으키는지" 바로 확인할 수 있다.

5.3 terraform apply — 실제 적용

plan에서 보여준 변경을 실제로 실행한다. yes를 입력하면 클라우드에 리소스가 생성된다.

hljs language-bash
$ terraform apply

# ... plan 결과 출력 ...

Do you want to perform these actions?
  Enter a value: yes

aws_instance.web: Creating...
aws_instance.web: Still creating... [10s elapsed]
aws_instance.web: Creation complete after 32s [id=i-0abc123def456]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Outputs:

instance_public_ip = "3.34.123.45"

5.4 terraform destroy — 전체 삭제

코드로 만든 모든 리소스를 삭제한다. 테스트 환경을 깔끔하게 정리할 때 유용하다.

hljs language-bash
$ terraform destroy

# 삭제될 리소스 목록 출력

Do you want to destroy all resources?
  Enter a value: yes

aws_instance.web: Destroying... [id=i-0abc123def456]
aws_instance.web: Destruction complete after 40s

Destroy complete! Resources: 1 destroyed.

주의: 프로덕션 환경에서 terraform destroy를 실행하면 서비스가 전부 내려간다. 실수를 방지하려면 prevent_destroy = true 라이프사이클 설정을 사용한다.

워크플로우 요약

TERRAFORM 워크플로우핵심 4단계
INITProvider 다운로드, Backend 설정, 모듈 초기화 — 프로젝트당 1회 (또는 설정 변경 시)
PLAN현재 상태 vs 코드 비교 → 변경 계획 출력 — 실제 변경 없음 (dry-run)
APPLYPlan 결과를 실제 실행 → 클라우드에 리소스 생성/수정/삭제
DESTROY모든 리소스 삭제 — 테스트 환경 정리용. 프로덕션에서는 극히 드물게 사용

6. State 관리: terraform.tfstate의 세계

Terraform State 파일을 창고의 마스터 재고 장부에 비유한 일러스트

State란 무엇인가

Terraform의 가장 중요한 개념 중 하나가 State다. terraform apply를 실행하면 Terraform은 terraform.tfstate라는 JSON 파일을 생성한다. 이 파일은 "내가 지금까지 만든 인프라의 현재 상태"를 기록한다.

hljs language-json
{
  "version": 4,
  "terraform_version": "1.9.5",
  "resources": [
    {
      "type": "aws_instance",
      "name": "web",
      "instances": [
        {
          "attributes": {
            "id": "i-0abc123def456",
            "ami": "ami-0c9c942bd7bf113a2",
            "instance_type": "t3.micro",
            "public_ip": "3.34.123.45"
          }
        }
      ]
    }
  ]
}

Terraform은 plan이나 apply를 실행할 때 이 State 파일과 실제 클라우드 상태를 비교한다.

!
State 파일을 잃어버리면?
Terraform은 자기가 관리하는 리소스를 더 이상 인식하지 못한다. 클라우드에 리소스는 존재하지만, Terraform은 "나는 아무것도 만든 적 없다"라고 생각한다. terraform import로 하나하나 복구해야 하는데, 리소스가 수백 개면 악몽이다.
절대 로컬에만 저장하지 않는다
State 파일은 반드시 Remote Backend에 저장한다. 가장 일반적인 구성은 S3 + DynamoDB(AWS 환경) 또는 Terraform Cloud다.

Remote Backend: S3 + DynamoDB

팀에서 Terraform을 사용한다면, State 파일을 S3 버킷에 저장하고 DynamoDB로 잠금(Lock)을 관리하는 것이 표준 패턴이다.

hljs language-hcl
# backend.tf
terraform {
  backend "s3" {
    bucket         = "my-terraform-state"
    key            = "prod/vpc/terraform.tfstate"
    region         = "ap-northeast-2"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}
Remote Backend가 해결하는 문제들
공유 접근
팀원 전원이 같은 State 파일을 참조한다. "내 로컬에서는 되는데?"가 사라진다.
동시 수정 방지 (Lock)
DynamoDB 테이블이 잠금(Lock) 역할을 한다. A가 apply 중이면 B는 기다려야 한다. 동시 수정으로 인한 State 충돌을 방지한다.
암호화 + 버전 관리
S3 서버 사이드 암호화로 민감 정보를 보호하고, S3 버전 관리를 켜면 State 파일의 이전 버전으로 롤백할 수 있다.

State 파일에는 비밀번호, 키 같은 민감 정보가 평문으로 저장될 수 있다. 예를 들어 aws_db_instancepassword 값이 State에 그대로 기록된다. S3 암호화는 반드시 켜고, State 파일을 Git에 커밋하면 절대 안 된다. .gitignore*.tfstate*.tfstate.backup을 추가한다.


7. Modules: 재사용 가능한 인프라 컴포넌트

모듈이 필요한 이유

프로젝트가 커지면 main.tf 파일 하나에 수백 줄의 코드가 쌓인다. VPC 설정, EC2 설정, RDS 설정, S3 설정이 뒤섞여서 읽기도, 관리하기도 어려워진다.

모듈(Module)은 리소스의 논리적 그룹을 재사용 가능한 패키지로 만드는 것이다. React의 컴포넌트, Python의 패키지와 같은 개념이다.

모듈 구조

hljs language-bash
modules/
├── vpc/
│   ├── main.tf          # VPC, 서브넷, IGW 리소스
│   ├── variables.tf     # 입력 변수 (CIDR, 서브넷 수 등)
│   └── outputs.tf       # 출력 (VPC ID, 서브넷 ID 등)
├── ec2/
│   ├── main.tf
│   ├── variables.tf
│   └── outputs.tf
└── rds/
    ├── main.tf
    ├── variables.tf
    └── outputs.tf

모듈 사용

hljs language-hcl
# 루트 main.tf
module "vpc" {
  source = "./modules/vpc"

  vpc_cidr        = "10.0.0.0/16"
  public_subnets  = ["10.0.1.0/24", "10.0.2.0/24"]
  private_subnets = ["10.0.10.0/24", "10.0.20.0/24"]
  environment     = "prod"
}

module "web_server" {
  source = "./modules/ec2"

  instance_type = "t3.medium"
  subnet_id     = module.vpc.public_subnet_ids[0]  # VPC 모듈의 출력 참조
  environment   = "prod"
}

module "database" {
  source = "./modules/rds"

  instance_class = "db.t3.medium"
  subnet_ids     = module.vpc.private_subnet_ids    # 프라이빗 서브넷에 배치
  vpc_id         = module.vpc.vpc_id
  environment    = "prod"
}

모듈의 장점을 정리하면 다음과 같다.

모듈화의 이점
재사용 같은 VPC 모듈을 dev, staging, prod에 사용
캡슐화 내부 구현을 숨기고 입출력만 노출
일관성 모든 환경이 같은 모듈에서 생성
테스트 모듈 단위로 검증 가능

Terraform Registry

직접 모듈을 만들 필요 없이, Terraform Registry(registry.terraform.io)에서 커뮤니티가 만든 모듈을 가져다 쓸 수 있다. 예를 들어 AWS VPC 모듈은 GitHub 스타가 5,000개가 넘는다.

hljs language-hcl
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.16.0"

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

  azs             = ["ap-northeast-2a", "ap-northeast-2c"]
  public_subnets  = ["10.0.1.0/24", "10.0.2.0/24"]
  private_subnets = ["10.0.10.0/24", "10.0.20.0/24"]

  enable_nat_gateway = true
  single_nat_gateway = true  # 비용 절약 (dev 환경)

  tags = {
    Environment = "dev"
    Terraform   = "true"
  }
}

8. 실전 예제: VPC + EC2 + RDS를 코드 하나로 만들기

이론은 충분하다. 실제로 웹 서비스 인프라 한 세트를 Terraform으로 만들어보자. 구성은 다음과 같다.

사용자
ALB (로드밸런서)
EC2 (웹 서버)
RDS (데이터베이스)

프로젝트 구조

hljs language-bash
my-web-infra/
├── main.tf           # 메인 설정
├── variables.tf      # 변수 정의
├── outputs.tf        # 출력 정의
├── terraform.tfvars  # 변수 값 (환경별)
└── .gitignore        # tfstate, .terraform 제외

main.tf — 전체 인프라 정의

hljs language-hcl
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
  required_version = ">= 1.7.0"
}

provider "aws" {
  region = var.aws_region
}

# ─── VPC ───
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = { Name = "${var.project}-vpc" }
}

resource "aws_subnet" "public" {
  count                   = 2
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.${count.index + 1}.0/24"
  availability_zone       = data.aws_availability_zones.available.names[count.index]
  map_public_ip_on_launch = true

  tags = { Name = "${var.project}-public-${count.index + 1}" }
}

resource "aws_subnet" "private" {
  count             = 2
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.${count.index + 10}.0/24"
  availability_zone = data.aws_availability_zones.available.names[count.index]

  tags = { Name = "${var.project}-private-${count.index + 1}" }
}

resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id
  tags   = { Name = "${var.project}-igw" }
}

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main.id
  }

  tags = { Name = "${var.project}-public-rt" }
}

resource "aws_route_table_association" "public" {
  count          = 2
  subnet_id      = aws_subnet.public[count.index].id
  route_table_id = aws_route_table.public.id
}

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

# ─── Security Groups ───
resource "aws_security_group" "alb" {
  name_prefix = "${var.project}-alb-"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port   = 80
    to_port     = 80
    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"]
  }
}

resource "aws_security_group" "web" {
  name_prefix = "${var.project}-web-"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port       = 80
    to_port         = 80
    protocol        = "tcp"
    security_groups = [aws_security_group.alb.id]
  }

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

resource "aws_security_group" "db" {
  name_prefix = "${var.project}-db-"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port       = 3306
    to_port         = 3306
    protocol        = "tcp"
    security_groups = [aws_security_group.web.id]
  }
}

# ─── EC2 ───
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-*-x86_64"]
  }
}

resource "aws_instance" "web" {
  ami                    = data.aws_ami.amazon_linux.id
  instance_type          = var.instance_type
  subnet_id              = aws_subnet.public[0].id
  vpc_security_group_ids = [aws_security_group.web.id]

  user_data = <<-EOF
    #!/bin/bash
    dnf install -y httpd
    systemctl enable --now httpd
    echo "<h1>Hello from Terraform!</h1>" > /var/www/html/index.html
  EOF

  tags = { Name = "${var.project}-web" }
}

# ─── RDS ───
resource "aws_db_subnet_group" "main" {
  name       = "${var.project}-db-subnet"
  subnet_ids = aws_subnet.private[*].id
}

resource "aws_db_instance" "main" {
  identifier           = "${var.project}-db"
  engine               = "mysql"
  engine_version       = "8.0"
  instance_class       = "db.t3.micro"
  allocated_storage    = 20
  db_name              = "appdb"
  username             = var.db_username
  password             = var.db_password
  db_subnet_group_name = aws_db_subnet_group.main.name
  vpc_security_group_ids = [aws_security_group.db.id]
  skip_final_snapshot  = true

  tags = { Name = "${var.project}-db" }
}

variables.tf

hljs language-hcl
variable "aws_region" {
  description = "AWS 리전"
  type        = string
  default     = "ap-northeast-2"
}

variable "project" {
  description = "프로젝트 이름"
  type        = string
  default     = "my-web"
}

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

variable "db_username" {
  description = "RDS 관리자 계정명"
  type        = string
  sensitive   = true
}

variable "db_password" {
  description = "RDS 관리자 비밀번호"
  type        = string
  sensitive   = true
}

실행

hljs language-bash
# 1. 초기화
$ terraform init

# 2. 계획 확인
$ terraform plan -var="db_username=admin" -var="db_password=MySecureP@ss123"

# Plan: 12 to add, 0 to change, 0 to destroy.
# VPC, 서브넷 4개, IGW, 라우팅 테이블, SG 3개, EC2, RDS = 12개 리소스

# 3. 적용
$ terraform apply -var="db_username=admin" -var="db_password=MySecureP@ss123"

# 4. 결과 확인
$ terraform output
# instance_public_ip = "3.34.xxx.xxx"
# rds_endpoint = "my-web-db.xxxx.ap-northeast-2.rds.amazonaws.com:3306"

코드 한 파일로 VPC, 서브넷, 인터넷 게이트웨이, 보안 그룹, EC2 인스턴스, RDS 데이터베이스가 모두 만들어진다. 같은 코드를 다른 리전이나 다른 AWS 계정에 적용하면 동일한 환경이 복제된다.


9. Terraform Cloud와 협업

로컬 Terraform의 한계

지금까지의 예제는 모두 로컬 머신에서 terraform CLI를 실행하는 방식이었다. 혼자 작업할 때는 괜찮지만, 팀으로 일하면 문제가 생긴다.

문제로컬 실행Terraform Cloud
State 공유S3 Backend 직접 설정자동 관리 + 암호화
시크릿 관리tfvars 파일 공유 (위험)Workspace 변수 (암호화)
코드 리뷰Plan 결과 수동 복붙PR에 Plan 자동 코멘트
권한 제어AWS 자격증명 공유RBAC + Team 기능
실행 이력터미널 히스토리뿐모든 Run 기록 보존
정책 적용사람이 직접 확인Sentinel/OPA 자동 정책 검사

Terraform Cloud 워크플로우

Terraform Cloud(app.terraform.io)는 HashiCorp가 운영하는 SaaS 플랫폼이다. 무료 플랜으로 최대 500개 리소스를 관리할 수 있다.

PR 개발자가 Terraform 코드를 수정하고 PR을 올린다
Plan Terraform Cloud가 자동으로 plan을 실행하고, PR에 결과를 코멘트로 남긴다
Review 팀원이 코드 diff + plan 결과를 함께 리뷰한다
Policy Sentinel 정책이 자동 검사된다 (예: "t2.xlarge 이상 인스턴스 금지")
Apply 머지 후 자동 또는 수동으로 apply 실행 — 모든 기록이 보존된다
hljs language-hcl
# Terraform Cloud 설정
terraform {
  cloud {
    organization = "my-company"

    workspaces {
      name = "my-web-prod"
    }
  }
}

이렇게 설정하면 terraform planterraform apply가 로컬이 아닌 Terraform Cloud의 서버에서 실행된다. AWS 자격증명은 Terraform Cloud의 Workspace 변수에 저장되므로, 개발자 로컬에 AWS 키가 없어도 된다.


10. BSL 라이선스 논란과 OpenTofu 포크

무슨 일이 있었나

2023년 8월, HashiCorp는 Terraform을 포함한 모든 제품의 라이선스를 MPL 2.0(Mozilla Public License)에서 BSL 1.1(Business Source License)으로 변경했다. 이 결정은 IaC 커뮤니티를 뒤흔들었다.

!
BSL 1.1이 뭐길래?
BSL 1.1은 소스 코드를 볼 수는 있지만, 경쟁 제품을 만드는 데 사용할 수 없다. 구체적으로 HashiCorp의 "경쟁 제품"에 해당하는 서비스(Terraform Cloud의 대안 등)를 만들어 상업적으로 운영하는 것이 금지된다. 일반 사용자가 자기 인프라를 관리하는 것은 문제없다.
커뮤니티의 대응: OpenTofu 탄생
라이선스 변경 발표 4일 만에 OpenTofu 프로젝트(원래 이름: OpenTF)가 시작되었다. Terraform v1.5.x(마지막 MPL 버전)를 포크하여, Linux Foundation 산하에서 MPL 2.0 라이선스로 유지한다. Spacelift, Env0, Gruntwork 등이 적극 참여한다.
?
2026년 현재 상황
Terraform과 OpenTofu는 분리되어 발전 중이다. 대부분의 기업은 여전히 Terraform을 사용하지만, OpenTofu도 State 암호화 같은 독자 기능을 추가하며 차별화하고 있다. 2024년 IBM의 HashiCorp 인수 이후, 라이선스 정책의 변화 가능성이 점쳐지기도 한다.

Terraform vs OpenTofu

비교 항목TerraformOpenTofu
라이선스BSL 1.1MPL 2.0 (진정한 오픈소스)
관리 주체HashiCorp (IBM)Linux Foundation
CLI 명령어terraformtofu
HCL 호환원본완전 호환
Provider 호환Terraform Registry동일 Provider 사용 가능
State 암호화미지원 (Cloud에서만)클라이언트 사이드 암호화 지원
생태계 성숙도10년+ 레퍼런스아직 성장 중
마이그레이션tofu init만 실행하면 전환

결론: 일반 사용자 입장에서 BSL 라이선스는 실질적 영향이 거의 없다. Terraform을 사용하여 자사 인프라를 관리하는 것은 완전히 허용된다. 하지만 "진정한 오픈소스"를 중시하거나, Terraform을 기반으로 상업 서비스를 구축하는 경우에는 OpenTofu가 대안이 된다.

입문 단계에서는 Terraform으로 시작하는 것을 권장한다. 레퍼런스와 커뮤니티 자료가 압도적으로 많다. OpenTofu로의 전환은 나중에 필요할 때 해도 tofu init 한 번이면 된다.


11. 실전 팁: Terraform을 잘 쓰기 위한 Best Practices

파일 구성 컨벤션

권장 파일 구조
필수 파일
main.tf — 핵심 리소스 정의
variables.tf — 입력 변수 선언
outputs.tf — 출력값 정의
versions.tf — required_providers + required_version
선택 파일
backend.tf — Remote Backend 설정
data.tf — Data 소스 모음
locals.tf — 로컬 변수 (계산된 값)
terraform.tfvars — 변수 기본값 (환경별)
.gitignore에 반드시 추가
.terraform/ — Provider 바이너리 (수백 MB)
*.tfstate — State 파일 (민감 정보 포함 가능)
*.tfstate.backup — State 백업
*.tfvars — 비밀번호가 포함될 수 있음 (주의)

핵심 규칙 7가지

Terraform Best Practices 중요도
실무 적용 빈도 기준 (10점 만점)
State 보호
10/10 필수
버전 고정
9.5/10 필수
모듈화
9/10 필수
Plan 리뷰
8.5/10 필수
시크릿 분리
8.5/10 강력 권장
네이밍 규칙
7.5/10 강력 권장
자동화 (CI)
7/10 권장
  1. State를 Remote Backend에 저장한다. 로컬 State는 팀 작업에서 재앙의 시작이다.
  2. Provider와 모듈 버전을 고정한다. version = "~> 5.0"처럼 범위를 지정하고, terraform.lock.hcl을 Git에 커밋한다.
  3. 리소스가 10개를 넘으면 모듈로 분리한다. 500줄짜리 main.tf는 아무도 읽고 싶어하지 않는다.
  4. terraform plan을 반드시 확인한 후 apply한다. CI에서 자동으로 plan 결과를 PR에 코멘트하는 것이 이상적이다.
  5. 비밀번호를 코드에 하드코딩하지 않는다. sensitive = true 변수 + Terraform Cloud 변수 또는 AWS Secrets Manager를 사용한다.
  6. 일관된 네이밍 규칙을 정한다. ${project}-${environment}-${component} 패턴이 일반적이다.
  7. CI/CD 파이프라인에 Terraform을 통합한다. GitHub Actions, GitLab CI, 또는 Terraform Cloud를 사용한다.

12. Terraform 학습 로드맵

마지막으로, Terraform을 체계적으로 학습하기 위한 로드맵을 정리한다.

Week 1-2 기초 문법 — HCL 블록 5종(provider, resource, variable, output, data), terraform init/plan/apply 워크플로우 익히기. EC2 인스턴스 하나 만들어보기.
Week 3-4 핵심 개념 — State 관리, Remote Backend(S3), count/for_each 반복문, 조건식, locals 블록. VPC + EC2 + RDS 세트 구성해보기.
Week 5-6 모듈화 — 커스텀 모듈 작성, Terraform Registry 모듈 활용, 모듈 입출력 설계. 실제 프로젝트를 모듈 구조로 리팩토링하기.
Week 7-8 협업과 자동화 — Terraform Cloud 또는 GitHub Actions 연동, 환경별(dev/staging/prod) 워크스페이스 분리, PR 기반 워크플로우 구축.
Week 9+ 고급 주제 — Terragrunt(DRY 설정), Terraform 테스트 프레임워크, 정책 엔진(Sentinel/OPA), 대규모 인프라 관리 패턴, import/moved 블록.

추천 자료:

  • 공식 튜토리얼: developer.hashicorp.com/terraform/tutorials — 무료, 브라우저에서 바로 실습 가능
  • Terraform: Up & Running (Yevgeniy Brikman 저) — IaC와 Terraform의 바이블로 불리는 책
  • HashiCorp Certified: Terraform Associate — 입문자가 목표로 삼기 좋은 자격증. 실무에서도 인정받는다

마치며: 인프라도 코드의 시대

콘솔에서 클릭 100번 하는 것과 코드 한 파일을 apply하는 것. 결과물은 같지만, 관리 가능성은 완전히 다르다.

코드로 작성된 인프라는 재현 가능하고, 버전 관리되고, 코드 리뷰를 받고, 자동화할 수 있다. 소프트웨어 개발에서 이미 입증된 모든 좋은 관행(Git, CI/CD, 코드 리뷰, 테스트)을 인프라에도 적용할 수 있다는 것 — 그것이 IaC의 본질이고, Terraform이 지난 10년간 이끌어온 변화다.

Terraform의 진입 장벽은 높지 않다. AWS 계정 하나, terraform init, 그리고 HCL 파일 하나면 시작할 수 있다. 오늘 EC2 인스턴스 하나를 코드로 만들어보자. 그 작은 경험이 인프라를 바라보는 관점을 완전히 바꿔줄 것이다.