목록으로
Terraform

Terraform로 다루는 클라우드 인프라

김민석2026년 06월 17일11분 읽기

Terraform로 다루는 클라우드 인프라

코드로 인프라를 선언하고, 한 번의 명령으로 만들고 지우는 법 — 그리고 프로젝트에 적용된 내용

목차

  1. Terraform이란 무엇인가
  2. AWS CLI와 무엇이 다른가
  3. 장점과 단점
  4. 기본 문법 한눈에
  5. 꼭 쓰는 명령어
  6. State 관리 — Remote Backend
  7. 팀 협업 방식
  8. 우리 프로젝트 구조
  9. 실제 정의된 AWS 리소스
  10. 실제 코드 예시
  11. 전체 아키텍처
  12. [주의사항]

1. Terraform이란 무엇인가

HashiCorp가 만든 Infrastructure as Code(IaC) 도구다. 한 줄로 풀면, 클라우드 인프라를 코드로 적어두고 명령어 한 번으로 만들고·고치고·지우는 도구다. 콘솔에서 마우스로 클릭하던 작업을 텍스트 파일(.tf)로 옮긴다고 보면 된다.

text
1코드 작성  →  plan(미리보기)  →  apply(실제 반영)  →  인프라 완성

먼저 잡고 갈 세 가지 개념

  • 선언형 (Declarative) — "어떻게 만들어라"가 아니라 **"최종 상태가 이래야 한다"**고 적는다. 그러면 Terraform이 현재 상태와 비교해서 달라진 부분만 알아서 반영한다.
  • State 파일 — 지금 인프라가 어떤 상태인지 추적하는 JSON 파일이다(terraform.tfstate). Terraform이 "내가 뭘 만들어놨는지" 기억하는 장부라고 보면 된다.
  • Provider — AWS·GCP·Azure 같은 클라우드 API에 연결해주는 플러그인이다. "AWS를 쓰겠다"고 선언하면 해당 provider가 실제 API 호출을 담당한다.

2. AWS CLI와 무엇이 다른가

둘 다 AWS를 다루지만 접근 방식이 정반대다. CLI는 명령형(하나씩 시킨다), Terraform은 선언형(원하는 결과를 적는다).

항목AWS CLITerraform
방식명령형 — 하나씩 실행선언형 — 최종 상태 정의
예시aws ec2 run-instances …resource "aws_instance" "web" {…}
상태 추적✗ 없음 · 직접 확인✓ state 파일로 자동 추적
변경 미리보기✗ 불가terraform plan
롤백수동으로 원복코드 되돌리고 apply
재현성스크립트 따로 작성코드 자체가 곧 문서
의존성순서를 직접 관리의존성 그래프 자동 생성
삭제리소스 하나씩terraform destroy 한 번

언제 뭘 쓰나

  • AWS CLI — 빠른 조회, 디버깅, 한 번 쓰고 마는 작업.
  • Terraform — 인프라를 짓고 관리할 때, 환경을 복제할 때, 여럿이 협업할 때.

3. 장점과 단점

좋기만 한 도구는 없다. 도입 전에 트레이드오프를 알고 가는 게 중요하다.

장점

  • 인프라를 코드로 버전 관리 — Git으로 변경 이력 추적
  • plan으로 적용 전 변경사항 확인 → 실수 방지
  • 같은 코드로 dev/prod 환경 복제
  • 모듈화로 재사용 — VPC 모듈 한 번 만들면 dev·prod 공유
  • 멀티 클라우드 — AWS·GCP·Azure 동일 문법
  • 의존성 자동 해결 — 서브넷 전에 VPC부터 생성

단점

  • State 파일 관리 필요 — 분실·충돌 시 위험
  • 콘솔에서 수동 변경하면 drift 발생(코드와 실제가 어긋남)
  • 학습 곡선 — HCL 문법, 모듈 구조 이해 필요
  • 일부 리소스는 삭제 후 재생성 → downtime 가능
  • 민감정보(비밀번호) 관리에 주의 필요

drift(드리프트)란 코드에 적힌 상태와 실제 클라우드 상태가 달라진 것을 말한다. 누군가 콘솔에서 손으로 바꾸면 생긴다. 7장 협업 규칙이 이걸 막기 위한 것이다.


4. 기본 문법 한눈에

Terraform은 HCL(HashiCorp Configuration Language)이라는 문법을 쓴다. 자주 쓰는 7가지 블록만 알면 대부분 읽힌다.

4-1. Provider — 어떤 클라우드를 쓸지 선언

hcl
1terraform {
2  required_providers {
3    aws = {
4      source  = "hashicorp/aws"
5      version = "~> 5.0"
6    }
7  }
8}
9
10provider "aws" {
11  region = "ap-northeast-2"  # 서울 리전
12}

4-2. Resource — 실제로 만들 자원

hcl
1resource "aws_instance" "web" {
2  ami           = "ami-0c55b159cbfafe1f0"
3  instance_type = "t3.micro"
4
5  tags = {
6    Name = "my-web-server"
7  }
8}

구조는 항상 resource "리소스타입" "이름" { 설정값 } 형태다.

4-3. Variable — 값을 바깥에서 주입

hcl
1# 변수 선언 (variables.tf)
2variable "db_password" {
3  type      = string
4  sensitive = true   # plan 출력 시 마스킹
5}
6
7# 변수 사용
8resource "aws_db_instance" "main" {
9  password = var.db_password
10}

실제 값은 terraform.tfvars에 적는다 — db_password = "my-secret-password"

4-4. Output — 결과값 꺼내기

hcl
1output "alb_dns_name" {
2  value = aws_lb.main.dns_name
3}

apply 후 값을 출력하고, 다른 모듈에서 참조할 수도 있다.

4-5. Module — 재사용 가능한 코드 묶음

hcl
1module "vpc" {
2  source = "../../modules/vpc"   # 모듈 경로
3
4  env      = "dev"
5  region   = "ap-northeast-2"
6  vpc_cidr = "10.0.0.0/16"
7}

한 번 정의해두고 dev/prod에서 다른 값만 넣어 재사용한다.

4-6. Data Source — 기존 리소스 읽어오기

hcl
1data "aws_route53_zone" "main" {
2  name = "farmily.info"
3}
4# 사용: data.aws_route53_zone.main.zone_id

Terraform이 만들지 않은 기존 리소스 정보를 가져올 때 쓴다.

4-7. locals — 반복 표현식 정리

hcl
1locals {
2  app_environment = [
3    { name = "S3_BUCKET", value = module.s3.bucket_id },
4    { name = "S3_REGION", value = var.region },
5  ]
6}

5. 꼭 쓰는 명령어

명령어설명
terraform init프로바이더 다운로드, 백엔드 연결 (최초 1회)
terraform plan변경사항 미리보기 (실제 반영 X)
terraform apply실제 인프라에 반영
terraform destroy모든 리소스 삭제 (주의!)
terraform state list관리 중인 리소스 목록
terraform state show특정 리소스 상세 정보
terraform fmt코드 포맷팅
terraform validate문법 검증
text
1코드 수정  →  plan  →  변경 확인  →  apply  →  완료

6. State 관리 — Remote Backend

State 파일을 각자 노트북에 두면 팀원끼리 충돌한다. 그래서 S3에 두고 공유한다. 동시에 두 명이 apply하지 못하도록 **DynamoDB로 잠금(Lock)**을 건다.

hcl
1# backend.tf
2terraform {
3  backend "s3" {
4    bucket         = "farmily-terraform-state"
5    key            = "prod/terraform.tfstate"
6    region         = "ap-northeast-2"
7    dynamodb_table = "terraform-locks"   # 동시 수정 방지 (Lock)
8    encrypt        = true
9  }
10}
구성 요소역할
S3 버킷State 파일 저장소
DynamoDB 테이블동시에 apply 못 하게 Lock
encrypt저장 시 암호화

7. 팀 협업 방식

브랜치 전략

text
1main (운영 반영 코드)
2 └── feat-xxx (작업 브랜치)
3       → PR 생성 → 리뷰 → main 머지 → apply

협업 규칙

  1. 콘솔에서 직접 수정 금지 — 코드와 실제가 어긋남(drift)
  2. main에 직접 커밋 금지 — PR로만 머지
  3. plan 결과를 PR에 공유 — 뭐가 바뀌는지 팀원이 확인
  4. tfvars는 gitignore — 비밀번호 등 민감정보 보호
  5. 모듈 수정은 dev 먼저 테스트 — prod 반영 전 검증

.gitignore 설정

gitignore
1.terraform/
2*.tfstate
3*.tfstate.backup
4*.tfplan
5.terraform.lock.hcl
6*.tfvars
7!*.tfvars.example
8.DS_Store

8. 우리 프로젝트 구조

text
1Desktop/VPC/
2├── environments/
3│   ├── dev/                  # 개발 환경
4│   │   ├── main.tf           # 모듈 호출 (dev 설정)
5│   │   ├── variables.tf      # 변수 선언
6│   │   ├── backend.tf        # S3 state 설정
7│   │   └── terraform.tfvars  # 변수 값 (gitignore)
8│   └── prod/                 # 운영 환경
9│       ├── main.tf
10│       ├── variables.tf
11│       ├── backend.tf
12│       └── terraform.tfvars
13└── modules/                  # 재사용 모듈
14    ├── vpc/   ├── ecs/   ├── rds/   ├── elasticache/
15    ├── s3/    ├── cloudfront/   ├── cloudwatch/   ├── sg/
16    ├── acm/   ├── route53/   ├── ecs-scheduler/   └── waf/

환경 분리 방식

같은 모듈을 dev/prod에서 다른 파라미터로 호출하는 게 핵심이다.

hcl
1# dev: NAT 1개, RDS 소규모, Replica 없음
2module "vpc" {
3  source           = "../../modules/vpc"
4  enable_multi_nat = false
5}
6
7# prod: NAT 2개, RDS Multi-AZ, Replica 있음
8module "vpc" {
9  source           = "../../modules/vpc"
10  enable_multi_nat = true
11}

9. 실제 정의된 AWS 리소스

Prod 환경 — 84개 리소스

서비스리소스설명
VPCVPC, Subnet×4, IGW, NAT×2, Route Table×3, S3 Endpoint네트워크 기반
ECS FargateCluster, Task Definition, Service, ALB, Listener(HTTP+HTTPS), Target Group, Auto Scaling백엔드 컨테이너
RDSPostgreSQL(Multi-AZ) + Read Replica, Subnet Group, Monitoring Role데이터베이스
ElastiCacheRedis Replication Group(노드 2개), Subnet Group캐시
S3s3-bucket, prod-frontend-web, templates저장소 3개
CloudFront프론트엔드 CDN + 이미지 CDN(OAC)정적 배포
Route53api → ALB, 루트 → CF, www → CFDNS
ACMALB용(서울) + CloudFront용(버지니아)SSL 인증서
WAFWeb ACL + Rate Limit + AWS Managed RulesL7 보안
CloudWatchECS CPU/Mem 알람, RDS CPU 알람, SNS Topic모니터링
Security GroupALB, ECS, RDS, Redis, Lambda, Noti Lambda, AgentCore (7개)접근 제어
IAMECS Execution/Task Role, S3 정책, Secrets Manager 정책권한

Dev 환경 — 65개 리소스 (Prod 대비 차이)

서비스Prod과의 차이
VPCNAT 1개 (비용 절감)
ECSScheduler로 평일 09–18시만 운영
RDSSingle-AZ, Replica 없음
ElastiCache노드 1개
S31개만 (dev-s3-bucket)
CloudFront / WAF없음
Route53api.dev.farmily.info → ALB

10. 실제 코드 예시

VPC 모듈 — modules/vpc/main.tf

hcl
1resource "aws_vpc" "main" {
2  cidr_block           = var.vpc_cidr
3  enable_dns_support   = true
4  enable_dns_hostnames = true
5  tags = { Name = "${var.env}-vpc" }
6}
7
8resource "aws_subnet" "public_a" {
9  vpc_id                  = aws_vpc.main.id
10  cidr_block              = var.public_subnet_a_cidr
11  availability_zone       = "${var.region}a"
12  map_public_ip_on_launch = true
13  tags = { Name = "${var.env}-public-subnet-a" }
14}

모듈 호출 — environments/prod/main.tf

hcl
1module "ecs" {
2  source = "../../modules/ecs"
3
4  env                 = "prod"
5  region              = var.region
6  vpc_id              = module.vpc.vpc_id          # VPC 모듈의 출력값 참조
7  private_subnet_ids  = [module.vpc.private_subnet_a_id, module.vpc.private_subnet_c_id]
8  container_image     = var.container_image
9  desired_count       = 1
10  max_count           = 8
11  task_cpu            = "512"
12  task_memory         = "1024"
13  enable_https        = true
14  alb_certificate_arn = module.acm_alb.certificate_arn
15}

민감정보 관리 — Secrets Manager 연동

비밀번호를 코드에 적지 않고, Secrets Manager에서 ECS 컨테이너로 직접 주입한다.

hcl
1data "aws_secretsmanager_secret" "app" {
2  name = "farmily/prod/app"
3}
4
5locals {
6  app_secrets = [for k in ["DB_PASSWORD", "JWT_SECRET", "KAKAO_CLIENT_SECRET"] : {
7    name      = k
8    valueFrom = "${data.aws_secretsmanager_secret.app.arn}:${k}::"
9  }]
10}

11. 전체 아키텍처

요청이 들어와서 데이터까지 닿는 전체 흐름이다. 위에서 아래로 읽으면 된다.

mermaid
1flowchart TD
2    Internet([Internet])
3    R53["Route 53<br/><small>farmily.info · api.farmily.info</small>"]
4    CF["CloudFront<br/><small>정적 웹 CDN</small>"]
5    ALB["ALB<br/><small>HTTPS · L7</small>"]
6    WAF[["WAF"]]
7    S3F["S3<br/><small>frontend 정적 호스팅</small>"]
8    ECS["ECS Fargate<br/><small>Spring Boot 컨테이너</small>"]
9    RDS[("RDS<br/>PostgreSQL")]
10    REDIS[("ElastiCache<br/>Redis")]
11    S3I["S3<br/><small>이미지</small>"]
12
13    Internet --> R53
14    R53 --> CF
15    R53 --> ALB
16    WAF -.보호.-> ALB
17    CF --> S3F
18    ALB --> ECS
19    ECS --> RDS
20    ECS --> REDIS
21    ECS --> S3I

Route 53(DNS)가 트래픽을 둘로 나눠 — 정적 웹은 CloudFront·S3로, API는 WAF를 거친 ALB·ECS로 보낸다. ECS는 다시 RDS·Redis·S3 세 데이터 계층과 통신한다.


12. 주의사항

실수하기 쉬운 지점

  • prod에서 terraform destroy 절대 금지 — 모든 리소스가 삭제된다.
  • apply 전 항상 plan 확인 — 뭐가 바뀌는지 반드시 눈으로 체크.
  • 콘솔 수정 후엔 terraform import — 코드에 반영해야 drift를 막는다.
  • tfvars는 절대 git에 올리지 않기 — 비밀번호 노출 사고로 이어진다.
  • 모듈 수정은 영향 범위 확인 — 모듈 하나 바꾸면 dev·prod 둘 다 영향.

CI/CD가 바꾸는 값은 lifecycle 블록으로 무시하게 둔다.

hcl
1lifecycle {
2  ignore_changes = [task_definition]  # CI/CD가 이미지 태그 바꾸는 건 무시
3}

Further Reading

댓글 0

첫 번째 댓글을 남겨보세요

댓글 작성