Terraform로 다루는 클라우드 인프라
Terraform로 다루는 클라우드 인프라
코드로 인프라를 선언하고, 한 번의 명령으로 만들고 지우는 법 — 그리고 프로젝트에 적용된 내용
목차
- Terraform이란 무엇인가
- AWS CLI와 무엇이 다른가
- 장점과 단점
- 기본 문법 한눈에
- 꼭 쓰는 명령어
- State 관리 — Remote Backend
- 팀 협업 방식
- 우리 프로젝트 구조
- 실제 정의된 AWS 리소스
- 실제 코드 예시
- 전체 아키텍처
- [주의사항]
1. Terraform이란 무엇인가
HashiCorp가 만든 Infrastructure as Code(IaC) 도구다. 한 줄로 풀면, 클라우드 인프라를 코드로 적어두고 명령어 한 번으로 만들고·고치고·지우는 도구다. 콘솔에서 마우스로 클릭하던 작업을 텍스트 파일(.tf)로 옮긴다고 보면 된다.
text1코드 작성 → plan(미리보기) → apply(실제 반영) → 인프라 완성
먼저 잡고 갈 세 가지 개념
- 선언형 (Declarative) — "어떻게 만들어라"가 아니라 **"최종 상태가 이래야 한다"**고 적는다. 그러면 Terraform이 현재 상태와 비교해서 달라진 부분만 알아서 반영한다.
- State 파일 — 지금 인프라가 어떤 상태인지 추적하는 JSON 파일이다(
terraform.tfstate). Terraform이 "내가 뭘 만들어놨는지" 기억하는 장부라고 보면 된다. - Provider — AWS·GCP·Azure 같은 클라우드 API에 연결해주는 플러그인이다. "AWS를 쓰겠다"고 선언하면 해당 provider가 실제 API 호출을 담당한다.
2. AWS CLI와 무엇이 다른가
둘 다 AWS를 다루지만 접근 방식이 정반대다. CLI는 명령형(하나씩 시킨다), Terraform은 선언형(원하는 결과를 적는다).
| 항목 | AWS CLI | Terraform |
|---|---|---|
| 방식 | 명령형 — 하나씩 실행 | 선언형 — 최종 상태 정의 |
| 예시 | 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 — 어떤 클라우드를 쓸지 선언
hcl1terraform { 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 — 실제로 만들 자원
hcl1resource "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 — 값을 바깥에서 주입
hcl1# 변수 선언 (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 — 결과값 꺼내기
hcl1output "alb_dns_name" { 2 value = aws_lb.main.dns_name 3}
apply 후 값을 출력하고, 다른 모듈에서 참조할 수도 있다.
4-5. Module — 재사용 가능한 코드 묶음
hcl1module "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 — 기존 리소스 읽어오기
hcl1data "aws_route53_zone" "main" { 2 name = "farmily.info" 3} 4# 사용: data.aws_route53_zone.main.zone_id
Terraform이 만들지 않은 기존 리소스 정보를 가져올 때 쓴다.
4-7. locals — 반복 표현식 정리
hcl1locals { 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 | 문법 검증 |
text1코드 수정 → plan → 변경 확인 → apply → 완료
6. State 관리 — Remote Backend
State 파일을 각자 노트북에 두면 팀원끼리 충돌한다. 그래서 S3에 두고 공유한다. 동시에 두 명이 apply하지 못하도록 **DynamoDB로 잠금(Lock)**을 건다.
hcl1# 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. 팀 협업 방식
브랜치 전략
text1main (운영 반영 코드) 2 └── feat-xxx (작업 브랜치) 3 → PR 생성 → 리뷰 → main 머지 → apply
협업 규칙
- 콘솔에서 직접 수정 금지 — 코드와 실제가 어긋남(drift)
- main에 직접 커밋 금지 — PR로만 머지
- plan 결과를 PR에 공유 — 뭐가 바뀌는지 팀원이 확인
- tfvars는 gitignore — 비밀번호 등 민감정보 보호
- 모듈 수정은 dev 먼저 테스트 — prod 반영 전 검증
.gitignore 설정
gitignore1.terraform/ 2*.tfstate 3*.tfstate.backup 4*.tfplan 5.terraform.lock.hcl 6*.tfvars 7!*.tfvars.example 8.DS_Store
8. 우리 프로젝트 구조
text1Desktop/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에서 다른 파라미터로 호출하는 게 핵심이다.
hcl1# 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개 리소스
| 서비스 | 리소스 | 설명 |
|---|---|---|
| VPC | VPC, Subnet×4, IGW, NAT×2, Route Table×3, S3 Endpoint | 네트워크 기반 |
| ECS Fargate | Cluster, Task Definition, Service, ALB, Listener(HTTP+HTTPS), Target Group, Auto Scaling | 백엔드 컨테이너 |
| RDS | PostgreSQL(Multi-AZ) + Read Replica, Subnet Group, Monitoring Role | 데이터베이스 |
| ElastiCache | Redis Replication Group(노드 2개), Subnet Group | 캐시 |
| S3 | s3-bucket, prod-frontend-web, templates | 저장소 3개 |
| CloudFront | 프론트엔드 CDN + 이미지 CDN(OAC) | 정적 배포 |
| Route53 | api → ALB, 루트 → CF, www → CF | DNS |
| ACM | ALB용(서울) + CloudFront용(버지니아) | SSL 인증서 |
| WAF | Web ACL + Rate Limit + AWS Managed Rules | L7 보안 |
| CloudWatch | ECS CPU/Mem 알람, RDS CPU 알람, SNS Topic | 모니터링 |
| Security Group | ALB, ECS, RDS, Redis, Lambda, Noti Lambda, AgentCore (7개) | 접근 제어 |
| IAM | ECS Execution/Task Role, S3 정책, Secrets Manager 정책 | 권한 |
Dev 환경 — 65개 리소스 (Prod 대비 차이)
| 서비스 | Prod과의 차이 |
|---|---|
| VPC | NAT 1개 (비용 절감) |
| ECS | Scheduler로 평일 09–18시만 운영 |
| RDS | Single-AZ, Replica 없음 |
| ElastiCache | 노드 1개 |
| S3 | 1개만 (dev-s3-bucket) |
| CloudFront / WAF | 없음 |
| Route53 | api.dev.farmily.info → ALB |
10. 실제 코드 예시
VPC 모듈 — modules/vpc/main.tf
hcl1resource "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
hcl1module "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 컨테이너로 직접 주입한다.
hcl1data "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. 전체 아키텍처
요청이 들어와서 데이터까지 닿는 전체 흐름이다. 위에서 아래로 읽으면 된다.
mermaid1flowchart 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 블록으로 무시하게 둔다.
hcl1lifecycle { 2 ignore_changes = [task_definition] # CI/CD가 이미지 태그 바꾸는 건 무시 3}
Further Reading
댓글 0개
첫 번째 댓글을 남겨보세요