
서론
기존에 수동으로 관리되던 "그로블" 서비스의 클라우드 인프라를 비용 최적화 및 보안을 고려하여 재설계하는 업무를 맡았습니다. 대표적인 IaC 툴인 Terraform을 도입하여 이번 업무를 체계적으로 수행하고자 했습니다.
Terraform은 형상 관리를 통해 인프라를 체계적으로 관리를 할 수 있으며, 의존성 관리를 통해 리소스를 안정적으로 배포 및 삭제하는 등 여러 가지 장점이 존재하는 것으로 알고 있습니다. 하지만, Terraform을 쓰면서 개인적으로 느낀 가장 큰 장점은 코드베이스를 통해 AI Agent와 자유롭게 논의할 수 있다는 점이었습니다. Claude Code, Gemeni CLI와 같은 AI Agent와 현재 내 인프라의 구성 정보에 대해 논의를 나누며 인프라를 발전해 나갈 수 있다는 점이 가장 좋았습니다. 하지만, 안타깝게도 AI Agent가 모든 것을 알려주는 것은 아닙니다... 이번 블로그에서는 수동으로 관리되던 AWS 인프라를 Terraform으로 재설계하며 겪은 시행착오 및 IaC 툴로 프로젝트를 "잘" 구성하는 방법에 대해서 소개해드리겠습니다!
Terraform을 쓰면서 주의할 점 (상태 드리프트)

<상황 정리>
제가 구축한 클라우드 인프라의 기본 구조는 다음과 같습니다.
- ALB (Application Load Balancer)
ALB는 Public Subnet에 위치해 외부 트래픽을 수신합니다. 이후 리스너 규칙에 따라 트래픽을 대상 그룹(Target Group)에 속한 인스턴스로 라우팅합니다. - ECS + CodeDeploy
API 서버는 무중단 배포를 위해 CodeDeploy Blue/Green 배포 전략을 사용합니다. 따라서, 대상 그룹(Target Group)과 리스너 규칙은 배포 과정에서 동적으로 변경됩니다.
<문제 발생>
서비스는 정상적으로 운영되던 중이었고, 인프라에 사소한 변경 사항이 생겨 terraform apply를 실행했습니다. 그러자 서비스 트래픽이 중단되는 상황이 발생하였고, 제가 분석한 상황은 다음과 같았습니다.
- EC2 내부 컨테이너는 healthy 상태였음
- 하지만 외부에서 접속하면 503 에러 발생
- ALB 설정을 확인해 보니 Target Group과 Listener 의존성이 깨져 있었음
즉, 컨테이너는 정상 동작했지만, ALB 라우팅 경로가 꼬여버린 것입니다.
<원인 분석>
Terraform은 "정의된 상태(desired state)"를 기준으로 리소스를 관리하기 때문에, 외부에서 동적으로 바뀐 상태를 모른 채 다시 덮었어 버리는 문제가 발생합니다. 이러한 현상은 "상태 드리프트"로 불리며 Terraform 사용 시 주의해야 되는 문제 중 하나였습니다. 제가 겪는 문제의 원인은 다음과 같습니다.
- 초기 Target Group은 Blue였음
- 이후 실제 운영 중 새로운 버전이 배포가 되어 CodeDeploy가 새로운 Green Target Group을 생성하고 ALB Listener를 갱신했음
- 하지만 Terraform은 이러한 동적 변화를 모른 채, 제가 정의한 상태대로 다시 Blue를 바라보도록 수정해 버렸음
그 결과, terraform apply 실행 후 실제 컨테이너는 Green 환경에서 실행 중임에도 불구하고 ALB의 Target Group은 Blue로 롤백되어 버렸습니다. 이로 인해 ALB와 컨테이너 간 연결이 꼬이고, 결국 트래픽이 끊기는 문제가 발생했습니다. 즉, Terraform의 선언적 상태 관리와 CodeDeploy의 동적 Blue/Green 배포 방식이 충돌하면서 생긴 문제였던 것입니다.
<해결>
이 문제를 해결하기 위해 다음과 같이 Terraform이 ECS Service의 특정 속성을 관리하지 않도록 설정해야 했습니다.
lifecycle {
ignore_changes = [task_definition, load_balancer]
}
이렇게 ignore_changes를 추가하면, task_definition (배포 시점에 CodeDeploy가 자동으로 갱신), load_balancer (CodeDeploy가 Blue ↔ Green 전환 시 갱신)을 Terraform이 건드리지 않게 됩니다. 즉, 배포 관련 동적 리소스는 CodeDeploy가 책임지고 관리하게 하고, Terraform은 나머지 인프라만 관리하도록 분리한 것이죠.
프로젝트가 확장되면서 고민하게 된 디렉터리 구조
<초기 버전>
project/
├── main.tf
├── variables.tf
├── provider.tf
├── outputs.tf
├── terraform.tfvars
└── README.md
Terraform을 사용하면서 처음에 코드를 기반으로 Terraform이 AWS에 실제로 리소스를 추가/변경/삭제하는 것이 신기했습니다. 처음에는 가장 단순하게 아래와 같은 파일 구조를 사용했습니다.
처음에는 main.tf에 몇 개의 리소스만 정의하고, 변수는 variables.tf로 분리하는 정도로 시작했습니다. 하지만 점차 리소스가 늘어나면서 main.tf 파일이 지나치게 비대해졌습니다. VPC 설정부터 ECS 클러스터, CodeDeploy 설정까지 모든 내용을 한 파일에 몰아넣다 보니, 특정 리소스를 찾는 데 시간이 오래 걸렸고 코드 리뷰 또한 쉽지 않았습니다. 현재는 혼자 프로젝트를 관리하고 있어 큰 불편이 없지만, 향후 협업이 필요해질 경우 이런 구조는 main.tf에서 잦은 충돌이 발생할 것이 뻔했습니다.
따라서, 프로젝트를 보다 체계적으로 관리하기 위한 구조 개선이 필요하다고 판단했습니다.
<환경별 분리>
project/
├── 01-vpc.tf
├── 02-security-groups.tf
├── 03-load-balancer.tf
├── 04-iam-roles.tf
├── 05-ecs-cluster.tf
├── 06-ecs-task-definitions.tf
├── 07-ecs-services.tf
├── 08-codedeploy.tf
├── 09-ecr.tf
├── 10-routes53.tf
├── variables.tf
├── provider.tf
├── outputs.tf
├── terraform.tfvars
└── README.md
이 문제를 해결하기 위해 논리적 단위별로 파일을 분리하는 구조로 전환했습니다. 변경된 파일 이름만 봐도 어떤 리소스에 수정 사항이 있었다는 것을 바로 알 수 있었습니다. 또한, 파일 순서별로 인프라가 어떤 순서로 구축되었는지 직관적으로 확인할 수 있기에 인프라 구축 중에 문제가 생기면 파일 순서대로 배포하면 원복 시킬 수 있는 장점도 있었습니다.
하지만, 이 구조도 완벽하지 않았습니다. 사소한 리소스 변경이 있을 때마다 terraform이 모든 인프라 구조를 확인하고 적용했기에 배포 시간이 오래 걸리는 문제가 발생했습니다. API 서비스의 태스크 정의만 변경하고 싶은데 이것을 적용하기 위해 Terraform은 전체 인프라의 상태를 확인해야 했습니다. 프로젝트가 성장함에 따라 인프라를 수정하는데 소요되는 시간이 증가하는 비효율적인 구조라는 생각이 들었습니다. 또한, 하나의 state에서 의존성이 모두 묶여있어 하나의 잘못된 리소스 변경이 전체 인프라에 영향을 줄 수 있다는 사실도 위험했습니다.
따라서, 대규모 프로젝트에서 Terraform 파일 구조를 어떻게 설계하는지 찾아보고 프로젝트 구조를 리팩터링 하는 작업이 필요했습니다.
<레이어별 분리>
project/
├── README.md # 이 파일
├── .gitignore # Git 무시 파일
├── .terraform.lock.hcl # Terraform 의존성 잠금
│
├── environments/ # 🌍 환경별 설정
│ ├── README.md
│ ├── shared/ # 공유 환경 (인프라 기반 + 플랫폼)
│ │ ├── main.tf # 공유 리소스 메인 설정
│ │ ├── terraform.tfvars # 공유 환경 변수 값
│ │ ├── variables.tf # 공유 환경 변수 정의
│ │ ├── versions.tf # Terraform & Provider 버전
│ │ └── outputs.tf # 다른 환경에서 참조할 출력값
│ ├── monitoring/ # 📊 모니터링 환경 (관측성 스택)
│ │ ├── main.tf # 모니터링 서비스 메인 설정
│ │ ├── terraform.tfvars # 모니터링 환경 변수 값
│ │ ├── variables.tf # 모니터링 환경 변수 정의
│ │ ├── versions.tf # Terraform & Provider 버전
│ │ └── README.md # 상세 모니터링 가이드
│ ├── dev/ # 개발 환경 (서비스 계층)
│ │ ├── main.tf # 개발 환경 메인 설정
│ │ ├── terraform.tfvars # 개발 환경 변수 값
│ │ ├── variables.tf # 개발 환경 변수 정의
│ │ └── versions.tf # Terraform & Provider 버전
│ └── prod/ # 프로덕션 환경 (서비스 계층)
│ ├── main.tf # 프로덕션 환경 메인 설정
│ ├── terraform.tfvars # 프로덕션 환경 변수 값
│ ├── variables.tf # 프로덕션 환경 변수 정의
│ └── versions.tf # Terraform & Provider 버전
│
├── modules/ # 📦 재사용 가능한 모듈들
│ ├── infrastructure/ # 🏗️ 인프라 기반 (변경 빈도: 낮음)
│ │ ├── vpc/ # VPC 및 네트워킹
│ │ ├── security-groups/ # 보안 그룹
│ │ ├── load-balancer/ # Application Load Balancer
│ │ ├── iam-roles/ # IAM 역할 및 정책
│ │ └── route53/ # DNS 및 도메인 관리
│ ├── platform/ # ⚙️ 플랫폼 계층 (변경 빈도: 중간)
│ │ ├── ecs-cluster/ # ECS 클러스터 관리
│ │ ├── ecr/ # 컨테이너 레지스트리
│ │ └── codedeploy/ # Blue/Green 배포
│ └── services/ # 🚀 서비스 계층 (변경 빈도: 높음)
│ ├── monitoring/ # 모니터링 환경 서비스
│ │ ├── grafana/ # Grafana 대시보드
│ │ ├── prometheus/ # Prometheus 메트릭 수집
│ │ ├── loki/ # Loki 로그 수집
│ │ └── otelcol/ # OpenTelemetry Collector
│ ├── development/ # 개발 환경 서비스
│ │ ├── api-service/ # Spring Boot API
│ │ ├── mysql-service/ # MySQL 데이터베이스
│ │ └── redis-service/ # Redis 캐시
│ └── production/ # 프로덕션 환경 서비스
│ ├── api-service/ # Spring Boot API
│ ├── mysql-service/ # MySQL 데이터베이스
│ └── redis-service/ # Redis 캐시
│
├── shared/ # 🔧 공통 설정
│ ├── README.md
│ ├── providers.tf # Terraform 프로바이더 공통 설정
│ ├── variables.tf # 공통 변수 정의
│ └── outputs.tf # 공통 출력 정의
│
├── docs/ # 📚 문서
│ └── README-deployment.md # 배포 가이드
│
├── scripts/ # 🔨 유틸리티 스크립트
│ └── deploy-step.sh # 단계별 배포 스크립트
│
└── backups/ # 💾 백업 파일들
├── *.tf.backup # 기존 Terraform 파일들
└── *.old # 이전 설정 파일들
현재는 위와 같이 인프라를 변경 빈도와 의존성에 따라 레이어로 나누었습니다.
- Infrastructure Layer (변경 빈도: 거의 없음) : VPC, Subnets, Internet Gateway
- Platform Layer (변경 빈도: 드묾) : Security Groups, Load Balancers, IAM Roles
- Service Layer (변경 빈도: 높음) : ECS Services, Task Definitions
이렇게 각 레이어가 독립적인 Terraform 상태를 가지며, 상위 레이어의 출력(outputs)을 하위 레이어가 참조하는 구조로 재설계했습니다. 즉, Service Layer의 변경 사항이 있을 때, Terraform은 변경 사항이 있는 Service Layer만 확인하고 나머지 레이어는 확인하지 않습니다. 그 결과 배포 속도가 향상되었으며 Service Layer에서의 실수가 다른 Layer의 오류로 전파되지 않았습니다.
실제로 Terraform을 만든 HashiCorp의 권장사항에서도 유사한 접근을 권장합니다.
"Separate your infrastructure into multiple, smaller state files to reduce blast radius and improve team collaboration."
인프라 코드도 애플리케이션 코드와 마찬가지로, 변경 빈도와 책임에 따라 적절히 분리해야 한다는 것을 깨달았습니다. 단일 책임 원칙(Single Responsibility Principle)은 애플리케이션 코드뿐만 아니라 Terraform에도 동일하게 적용됩니다.
결론
Terraform을 활용하면서 실제로 비용 효율적인 작업을 할 수 있었습니다. 서비스 출시 전에는 인프라를 구축할 때만 terraform apply를 실행했고, 작업이 끝나면 terraform destroy로 모든 인프라를 삭제해 불필요한 비용을 줄였습니다. 이를 통해 초기 인프라 구축 단계에서 상당한 비용 절감 효과를 얻을 수 있었습니다.
하지만 Terraform이 가져온 진짜 변화는 단순한 비용 절감을 넘어섭니다. 파일 구조를 개선하면서 배포 시간이 단축되었고, 인프라를 레이어별로 분리하면서 작은 변경에도 전체를 검증해야 했던 비효율이 사라졌습니다. 이를 통해 여러 개발자가 동시에 작업할 수 있는 협업 체계의 기반을 마련할 수 있었습니다.
무엇보다 인프라를 코드로 관리하게 되면서 “누가, 어떻게 인프라를 구축했는지”를 명확히 추적할 수 있게 되었고, 새로운 팀원도 AWS 콘솔에서 구조를 일일이 확인할 필요 없이 코드를 통해 전체 인프라를 이해할 수 있었습니다. 또한 문제가 발생했을 때 언제든 이전 상태로 롤백할 수 있다는 안정감 덕분에 자신 있게 새로운 리소스를 추가하고 삭제하며 개선을 시도할 수 있었습니다.
https://github.com/TEAM-LIAISON/groble-infra
GitHub - TEAM-LIAISON/groble-infra: Groble Monitoring Infra
Groble Monitoring Infra. Contribute to TEAM-LIAISON/groble-infra development by creating an account on GitHub.
github.com
'Cloud' 카테고리의 다른 글
| [AWS] Private Subnet으로 EC2 이관 여정 (5) | 2025.08.16 |
|---|