자세한 프로젝트 내용은 포트폴리오 — 모니 - 초보 투자자를 위한 AI 모의투자 플랫폼 에서 확인할 수 있습니다.
프로젝트 전까지 Bastion Host가 뭔지도 몰랐다. 팀원 덕분에 네트워크 개념을 잡았고, 결국 Terraform으로 AWS 인프라 전체를 코드로 짜게 됐다.
AWS 콘솔에서 EC2 만들고, VPC 설정하고, 보안 그룹 만들고… 클릭으로 하면 되긴 한다. 근데 그걸 다시 똑같이 만들어야 하면? 처음부터 다시 클릭해야 한다. 뭘 어떻게 설정했는지 기억도 안 난다.
Terraform은 인프라를 코드로 선언한다. .tf 파일에 "이런 EC2가 있어야 한다", "이런 네트워크 구조여야 한다"를 써두면 Terraform이 AWS에 반영한다. 파일이 곧 인프라 상태가 된다.
module "ec2" {
source = "./ec2"
security_group_id = module.security_group.security_group_id
public_subnet_ids = module.subnet.public_subnet_ids
private_subnet_ids = module.subnet.private_subnet_ids
key_name = module.key_pair.key_name
}module "ec2" {
source = "./ec2"
security_group_id = module.security_group.security_group_id
public_subnet_ids = module.subnet.public_subnet_ids
private_subnet_ids = module.subnet.private_subnet_ids
key_name = module.key_pair.key_name
}이게 EC2 인스턴스를 만드는 선언이다. terraform apply 한 번으로 실제 AWS에 반영된다.
앱 서비스와 DB, 모니터링을 전부 프라이빗 서브넷에 넣었다. 외부에서 직접 접근할 수 없고, ALB를 통해서만 들어올 수 있다.
처음에 이 두 개 개념을 전혀 몰랐다.
Bastion Host는 프라이빗 서브넷에 있는 EC2에 SSH로 접근하기 위한 중간 서버다. 프라이빗 EC2는 외부에서 직접 SSH가 안 되니까, 퍼블릭 서브넷에 있는 Bastion을 거쳐서 들어가는 구조다. 팀원 전체가 DB에 접근해야 할 때 Bastion을 통해 SSH 터널링으로 들어갔다.
NAT Instance는 반대 방향이다. 프라이빗 서브넷 안의 EC2가 외부 인터넷에 요청을 보내야 할 때 — 예를 들어 Docker 이미지를 pull하거나 패키지를 설치할 때 — 직접 나갈 수가 없다. NAT Instance가 대신 외부로 나가서 응답을 받아다 준다.
NAT Gateway를 쓰면 더 간단하지만 비용이 나온다. 월 예산이 $200이었고, NAT Instance로 비용을 아꼈다.
Terraform 코드를 기능별로 쪼개서 모듈로 관리했다.
Terraform도 코드랑 똑같이 모놀리식으로 짜면 나중에 문제가 생긴다.
하나의 .tf 파일에 VPC, EC2, ALB, Route53을 전부 때려넣으면 처음엔 편하지만, 규모가 커질수록 어디서 뭘 건드려야 하는지 찾기 어려워진다. 변경 범위도 불필요하게 넓어지고, terraform plan 결과도 읽기 힘들어진다. 그래서 기능 단위로 모듈을 쪼개는 게 중요하다.
moni-infra-terraform/
├── main.tf # 루트 — 모듈 호출 + Route53 레코드
├── vpc/ # VPC + IGW + 라우트 테이블
├── subnet/ # 퍼블릭 2개 / 프라이빗 2개
├── security-group/ # 보안 그룹
├── ec2/ # EC2 5개 (서비스/인프라/모니터링/Bastion/NAT)
├── alb/ # ALB + 타겟그룹 + 호스트 기반 라우팅
├── acm/ # HTTPS 인증서 (SAN 방식)
├── route53/ # DNS + ACM validation 레코드
├── ecr/ # ECR 리포지토리 11개 + IAM
├── s3/ # 로그 버킷
└── cloudfront/ # CDNmoni-infra-terraform/
├── main.tf # 루트 — 모듈 호출 + Route53 레코드
├── vpc/ # VPC + IGW + 라우트 테이블
├── subnet/ # 퍼블릭 2개 / 프라이빗 2개
├── security-group/ # 보안 그룹
├── ec2/ # EC2 5개 (서비스/인프라/모니터링/Bastion/NAT)
├── alb/ # ALB + 타겟그룹 + 호스트 기반 라우팅
├── acm/ # HTTPS 인증서 (SAN 방식)
├── route53/ # DNS + ACM validation 레코드
├── ecr/ # ECR 리포지토리 11개 + IAM
├── s3/ # 로그 버킷
└── cloudfront/ # CDN모듈 구조가 잡히고 나서 문서화도 자동화했다. terraform-docs를 쓰면 .tf 파일을 읽어서 각 모듈의 inputs, outputs, resources를 자동으로 README로 만들어준다. 모듈이 많아질수록 손으로 문서 관리하는 게 현실적으로 안 되는데, 코드가 바뀌면 문서도 같이 갱신되는 구조가 훨씬 낫다.
인프라 다이어그램은 Brainboard로 자동화했다. Terraform 코드를 연결하면 리소스 간 관계를 시각적으로 그려줘서, 구조 변경이 있을 때마다 다이어그램을 직접 그릴 필요가 없었다.
모듈끼리 outputs으로 값을 넘겨받는다. 예를 들어 security_group 모듈이 만든 보안 그룹 ID를 ec2 모듈이 받아서 쓴다. 의존 관계가 코드에 명시적으로 드러난다.
재현 가능하다. 인프라를 날려도 terraform apply 한 번이면 똑같이 올라온다. 콘솔에서 뭘 어떻게 설정했는지 기억할 필요가 없다.
변경 사항이 추적된다. Git으로 관리하니까 누가 뭘 바꿨는지 기록이 남는다. 인프라도 코드처럼 PR로 리뷰할 수 있다.
terraform plan이 있다. 실제로 적용하기 전에 "이 명령을 실행하면 어떤 리소스가 생기고 바뀌고 삭제된다"를 미리 볼 수 있다. 적용 전에 검토할 수 있다는 게 콘솔 클릭과 완전히 다른 점이다.
편한 만큼 위험하다.
terraform destroy 한 줄이면 인프라 전체가 사라진다. 실수로 모듈 하나를 잘못 건드리면 운영 중인 서비스가 같이 날아갈 수 있다. 콘솔에서 실수하면 그 리소스 하나만 문제가 되는데, Terraform에서 실수하면 파급 범위가 훨씬 넓다.
그래서 PR 규칙이 중요해진다. 이번 프로젝트에서는 리포지토리에 인프라 담당 팀원만 추가해서 PR 자체가 가능한 인원을 제한했다. 인프라 코드를 앱 코드랑 똑같이 리뷰하는 문화가 필요하다.
실제 프로덕션에서는 Terraform Cloud나 Atlantis 같은 도구로 plan/apply를 자동화하고, state 파일도 원격으로 관리하는 게 일반적이라고 한다. 이번엔 로컬에서 운영했는데, 팀원이 많아질수록 state 충돌이 생기는 구조라 다음엔 원격 백엔드부터 세팅하고 시작할 것 같다.