도입
팀 프로젝트 CGV에서 개발계 인프라 일부를 Terraform으로 담당했다. 어떤 역할을 맡았고 각 리소스를 어떻게 설계했는지 정리한다.
이번 섹션에서는 CGV 개발계 인프라의 Terraform 코드를 파일별로 분석한다. VPC-Subnet.tf부터 routing.tf, sg.tf, endpoints.tf, client_vpn.tf까지 모든 코드를 검토하고, 각 리소스의 설계 의도와 트러블슈팅 과정을 다룬다.
1. CGV 개발계 Terraform 담당 범위
내가 Terraform으로 맡은 개발계 인프라의 범위는 다음과 같다.
| 항목 | CGV 개발계 |
|---|---|
| Terraform 범위 | 개발계 일부 (VPC, SG, Endpoints) |
| EKS 생성 | eksctl (YAML 기반) |
| State 관리 | 단일 디렉토리 |
| VPC Endpoint | ECR 4개 + S3 Gateway 1개 |
| Client VPN | 있음 (개발자 접근용) |
| GitLab | 자체 호스팅 (Private Subnet) |
CGV는 왜 eksctl을 선택했는가? 팀 프로젝트에서는 빠른 구축이 우선이었다. eksctl은 한 줄 명령어로 EKS 클러스터와 함께 OIDC Provider, IAM Role을 자동 생성한다. 단점은 Terraform State와 분리되어 관리가 복잡해진다는 점이다. 나는 Terraform으로 VPC, Subnet, Security Group, VPC Endpoint를 구축했고, 팀원이 그 인프라 위에 eksctl로 EKS 클러스터와 GitLab CI + ArgoCD 기반 CI/CD 파이프라인을 구축했다. 나는 Kinesis, Redis 등을 설정하고 연결하여 백엔드 코드를 Pod로 EKS에 배포하는 역할을 담당했다.
2. VPC 네트워크 설계
CGV 개발계는 용도별로 서브넷을 분리했다.
10.0.0.0/16 (VPC CIDR)
├── 10.0.1.0/24 : Public Subnet - NAT Gateway (ap-northeast-2a)
├── 10.0.2.0/24 : Public Subnet - GitLab 테스트용 (ap-northeast-2a)
├── 10.0.20.0/24 : Private Subnet - Database (RDS, ElastiCache)
├── 10.0.30.0/24 : Private Subnet - EKS Fargate (ap-northeast-2a)
├── 10.0.31.0/24 : Private Subnet - EKS Fargate (ap-northeast-2c)
└── 10.0.40.0/24 : Private Subnet - GitLab
코드: infra/VPC-Subnet.tf
## VPC
resource "aws_vpc" "dev_vpc" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "dev-vpc"
}
}
# 인터넷 게이트웨이 (IGW) 생성 및 VPC에 연결
resource "aws_internet_gateway" "dev_igw" {
vpc_id = aws_vpc.dev_vpc.id
tags = {
Name = "dev-igw"
}
}
enable_dns_hostnames와 enable_dns_support를 왜 활성화했는가?
VPC Endpoint가 정상 동작하려면 두 옵션이 모두 true여야 한다. Interface 타입 VPC Endpoint는 Private DNS를 사용해서 ecr.ap-northeast-2.amazonaws.com 같은 서비스 도메인을 VPC 내부 IP로 해석한다. DNS 옵션이 꺼져 있으면 Endpoint가 있어도 퍼블릭 IP로 해석되어 NAT Gateway를 거치게 된다.
## 가용영역 a
# NAT용 퍼블릭 서브넷 생성
resource "aws_subnet" "public_subnet_a" {
vpc_id = aws_vpc.dev_vpc.id
cidr_block = "10.0.1.0/24"
availability_zone = "ap-northeast-2a"
map_public_ip_on_launch = true # EC2 생성 시 퍼블릭 IP 자동 할당
tags = {
Name = "dev-public-subnet-a"
}
}
# GitLab용 임시 퍼블릭 서브넷 생성
resource "aws_subnet" "test_public_gitlab_subnet" {
vpc_id = aws_vpc.dev_vpc.id
cidr_block = "10.0.2.0/24"
availability_zone = "ap-northeast-2a"
map_public_ip_on_launch = true
tags = {
Name = "test-public-gitlab-subnet"
}
}
# DB Private Subnet
resource "aws_subnet" "db_private_subnet" {
vpc_id = aws_vpc.dev_vpc.id
cidr_block = "10.0.20.0/24"
availability_zone = "ap-northeast-2a"
tags = {
Name = "dev-db-private-subnet"
}
}
# EKS Fargate Private Subnet (ap-northeast-2a)
resource "aws_subnet" "eks_fargate_private_subnet_a" {
vpc_id = aws_vpc.dev_vpc.id
cidr_block = "10.0.30.0/24"
availability_zone = "ap-northeast-2a"
tags = {
Name = "dev-eks-fargate-private-subnet-a"
}
}
# GitLab Private Subnet
resource "aws_subnet" "gitlab_private_subnet" {
vpc_id = aws_vpc.dev_vpc.id
cidr_block = "10.0.40.0/24"
availability_zone = "ap-northeast-2a"
tags = {
Name = "dev-gitlab-private-subnet"
}
}
## 가용영역 2c
# EKS Fargate Private Subnet (ap-northeast-2c)
resource "aws_subnet" "eks_fargate_private_subnet_c" {
vpc_id = aws_vpc.dev_vpc.id
cidr_block = "10.0.31.0/24"
availability_zone = "ap-northeast-2c"
tags = {
Name = "dev-eks-fargate-private-subnet-c"
}
}
서브넷 분리 설계 의도
첫째, GitLab과 EKS를 분리했다. CI/CD 파이프라인(GitLab)과 운영 환경(EKS)은 서로 다른 보안 요구사항을 가진다. GitLab은 개발자만 접근하고, EKS는 외부 트래픽을 받는다. 같은 서브넷에 두면 보안 그룹 설계가 복잡해진다.
둘째, Database를 전용 서브넷에 격리했다. RDS와 ElastiCache는 인터넷 라우팅이 필요 없다. 외부 통신 경로 자체를 없애면 공격 표면(attack surface)이 줄어든다.
셋째, EKS는 2개 AZ에 서브넷을 생성했다. AWS EKS는 클러스터 생성 시 최소 2개 가용영역의 서브넷을 요구한다. 개발 환경에서 비용을 줄이기 위해 실제 Pod는 2a에만 배포하고, 2c는 요구사항 충족용으로만 사용했다.
test_public_gitlab_subnet은 왜 만들었는가?
GitLab을 Private Subnet에 배치하기 전에 먼저 퍼블릭에서 테스트했다. Client VPN 설정 없이 SSH 접속과 GitLab 웹 UI를 검증하기 위함이다. 테스트 완료 후 Private Subnet으로 이전하고, 이 서브넷은 삭제 예정이었지만 코드에 남겨두었다.
3. 라우팅 설계
코드: infra/routing.tf
Private Subnet의 리소스가 인터넷에 접근하려면 NAT Gateway가 필요하다. NAT Gateway는 퍼블릭 서브넷에 위치하며, EIP(Elastic IP)를 통해 고정 IP를 갖는다.
## 가용영역 a
# NAT 게이트웨이 생성을 위한 EIP (Elastic IP) 할당
resource "aws_eip" "nat_gateway_eip_a" {
domain = "vpc"
tags = {
Name = "dev-nat-gateway-eip-a"
}
}
# NAT 게이트웨이 생성
resource "aws_nat_gateway" "dev_nat_gateway_a" {
allocation_id = aws_eip.nat_gateway_eip_a.id
subnet_id = aws_subnet.public_subnet_a.id
tags = {
Name = "dev-nat-gateway-a"
}
}
NAT Gateway를 왜 2a에만 생성했는가?
NAT Gateway는 시간당 $0.045, 데이터 처리당 $0.045/GB를 부과한다. AZ마다 NAT Gateway를 생성하면 비용이 2배가 된다. 개발 환경에서는 고가용성보다 비용 절감이 우선이므로, 2a에만 NAT Gateway를 생성하고 2c의 트래픽도 2a NAT를 공유하도록 설계했다.
# NAT 들어갈 퍼블릭 서브넷의 퍼블릭 라우팅 테이블
resource "aws_route_table" "public_route_table_a" {
vpc_id = aws_vpc.dev_vpc.id
tags = {
Name = "dev-public-route-table-a"
}
}
# 퍼블릭 라우팅 테이블에 인터넷 게이트웨이 라우팅 추가
resource "aws_route" "public_internet_route_a" {
route_table_id = aws_route_table.public_route_table_a.id
destination_cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.dev_igw.id
}
# NAT 들어갈 퍼블릭 서브넷과 퍼블릭 라우팅 테이블a 연결
resource "aws_route_table_association" "public_subnet_association_a" {
subnet_id = aws_subnet.public_subnet_a.id
route_table_id = aws_route_table.public_route_table_a.id
}
# GitLab용 테스트 퍼블릭 서브넷과 퍼블릭 라우팅 테이블a 연결
resource "aws_route_table_association" "test_public_gitlab_subnet_association" {
subnet_id = aws_subnet.test_public_gitlab_subnet.id
route_table_id = aws_route_table.public_route_table_a.id
}
aws_route와 aws_route_table의 차이는?
aws_route_table은 빈 라우팅 테이블을 생성하고, aws_route는 그 테이블에 라우팅 규칙을 추가한다. 분리하면 특정 라우트만 수정할 때 전체 테이블을 재생성하지 않아도 된다.
# 프라이빗 라우팅 테이블 (GitLab, DB용 - NAT 연결 없음)
resource "aws_route_table" "private_route_table" {
vpc_id = aws_vpc.dev_vpc.id
tags = {
Name = "dev-private-route-table"
}
}
# EKS Fargate 프라이빗 서브넷 전용 라우팅 테이블(a)
resource "aws_route_table" "eks_fargate_private_route_table_a" {
vpc_id = aws_vpc.dev_vpc.id
tags = {
Name = "dev-eks-fargate-private-route-table-a"
}
}
# EKS Fargate 프라이빗 서브넷 전용 라우팅 테이블에 NAT 게이트웨이 라우팅 추가
resource "aws_route" "eks_fargate_nat_route_a" {
route_table_id = aws_route_table.eks_fargate_private_route_table_a.id
destination_cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.dev_nat_gateway_a.id
}
# EKS Fargate 프라이빗 서브넷과 라우팅 테이블 연결
resource "aws_route_table_association" "eks_fargate_subnet_association_a" {
subnet_id = aws_subnet.eks_fargate_private_subnet_a.id
route_table_id = aws_route_table.eks_fargate_private_route_table_a.id
}
# GitLab 프라이빗 서브넷과 라우팅 테이블 연결
resource "aws_route_table_association" "gitlab_subnet_association" {
subnet_id = aws_subnet.gitlab_private_subnet.id
route_table_id = aws_route_table.private_route_table.id
}
# DB 프라이빗 서브넷과 라우팅 테이블 연결
resource "aws_route_table_association" "db_subnet_association" {
subnet_id = aws_subnet.db_private_subnet.id
route_table_id = aws_route_table.private_route_table.id
}
왜 EKS와 GitLab/DB의 라우팅 테이블을 분리했는가?
EKS는 NAT Gateway를 통해 외부 API 호출, 이미지 Pull이 필요하다. 반면 GitLab과 DB는 VPC Endpoint만으로 충분하고, 불필요한 인터넷 경로를 막아야 한다. private_route_table은 NAT 라우트가 없어서 0.0.0.0/0 트래픽이 차단된다.
## 가용영역 c
# EKS Fargate 프라이빗 서브넷 전용 라우팅 테이블(c)
resource "aws_route_table" "eks_fargate_private_route_table_c" {
vpc_id = aws_vpc.dev_vpc.id
tags = {
Name = "dev-eks-fargate-private-route-table-c"
}
}
# EKS Fargate 프라이빗 서브넷 전용 라우팅 테이블에 NAT 게이트웨이 라우팅 추가
# 비용 절감을 위해 NAT 게이트웨이 연결은 a의 퍼블릭 서브넷으로
resource "aws_route" "eks_fargate_nat_route_c" {
route_table_id = aws_route_table.eks_fargate_private_route_table_c.id
destination_cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.dev_nat_gateway_a.id # 2a NAT 공유
}
# EKS Fargate 프라이빗 서브넷과 라우팅 테이블 연결(c)
resource "aws_route_table_association" "eks_fargate_subnet_association_c" {
subnet_id = aws_subnet.eks_fargate_private_subnet_c.id
route_table_id = aws_route_table.eks_fargate_private_route_table_c.id
}
2c 서브넷이 2a NAT를 사용하면 Cross-AZ 비용이 발생하지 않는가?
발생한다. AZ간 데이터 전송은 $0.01/GB가 부과된다. 하지만 개발 환경에서 2c 서브넷의 실제 트래픽은 거의 없다. EKS 요구사항 충족용으로만 존재하기 때문이다. 운영 환경에서는 AZ별로 NAT Gateway를 생성해야 한다.
4. 보안 그룹 설계
코드: infra/sg.tf
CGV 개발계는 5개 보안 그룹을 정의했다.
# Client VPN 엔드포인트용 보안 그룹
resource "aws_security_group" "client_vpn_sg" {
name = "dev-client-vpn-sg"
description = "Security group for Client VPN endpoint"
vpc_id = aws_vpc.dev_vpc.id
# 개발자 공인 IP에서 VPN 포트 443으로의 인바운드 트래픽 허용
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["211.105.80.175/32"] # 특정 IP만 허용
}
# VPN 클라이언트가 VPC 내부로 통신할 수 있도록 아웃바운드 트래픽 허용
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = [aws_vpc.dev_vpc.cidr_block] # VPC 내부만
}
tags = {
Name = "dev-client-vpn-sg"
}
}
client_vpn_sg의 egress를 왜 VPC CIDR로 제한했는가?
VPN은 VPC 내부 리소스(GitLab) 접근용이다. VPN을 통해 인터넷에 나가는 것은 설계 목적이 아니다. egress를 VPC CIDR로 제한하면 VPN 사용자가 NAT Gateway를 통해 인터넷에 접근하는 것을 방지한다.
# GitLab EC2 인스턴스를 위한 보안 그룹
resource "aws_security_group" "gitlab_sg" {
name = "dev-gitlab-sg"
description = "Security group for Gitlab instance"
vpc_id = aws_vpc.dev_vpc.id
# ICMP (ping) - Client VPN에서만 허용
ingress {
from_port = -1
to_port = -1
protocol = "icmp"
security_groups = [aws_security_group.client_vpn_sg.id]
}
# SSH 접속 허용 - Client VPN에서만
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
security_groups = [aws_security_group.client_vpn_sg.id]
}
# HTTP 접속 허용 - Client VPN에서만
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = [aws_security_group.client_vpn_sg.id]
}
# HTTPS 접속 허용 - Client VPN에서만
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
security_groups = [aws_security_group.client_vpn_sg.id]
}
# 모든 아웃바운드 트래픽 허용 (ECR Push 등)
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "dev-gitlab-sg"
}
}
security_groups 참조 vs cidr_blocks의 차이는?
cidr_blocks = ["10.8.0.0/22"]처럼 IP 대역을 직접 지정하면, VPN 클라이언트 IP가 바뀔 때마다 규칙을 수정해야 한다. security_groups = [aws_security_group.client_vpn_sg.id]는 해당 보안 그룹에 속한 모든 트래픽을 허용한다. VPN ENI가 client_vpn_sg를 사용하므로, VPN 연결자만 GitLab에 접근 가능하다.
# EKS Fargate 클러스터를 위한 보안 그룹
resource "aws_security_group" "eks_fargate_sg" {
name = "dev-eks-fargate-sg"
description = "Security group for EKS Fargate cluster"
vpc_id = aws_vpc.dev_vpc.id
# VPC 내부 통신만 허용
ingress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = [aws_vpc.dev_vpc.cidr_block] # 10.0.0.0/16
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "dev-eks-fargate-sg"
}
}
EKS 보안 그룹의 ingress를 왜 VPC 전체로 열었는가?
EKS Pod는 ALB, 다른 Pod, RDS, ElastiCache 등 VPC 내 여러 리소스와 통신한다. 포트별로 세분화하면 관리가 복잡해진다. VPC 내부는 신뢰 영역으로 간주하고 전체 허용한 뒤, 외부 접근은 ALB에서 제어한다.
# DB 인스턴스를 위한 보안 그룹
resource "aws_security_group" "db_sg" {
name = "dev-db-sg"
description = "Security group for DB instances"
vpc_id = aws_vpc.dev_vpc.id
# EKS 서브넷에서만 DB 포트로의 접근을 허용
ingress {
from_port = 5432
to_port = 5432
protocol = "tcp"
cidr_blocks = [aws_subnet.eks_fargate_private_subnet_a.cidr_block]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "dev-db-sg"
}
}
DB 보안 그룹이 PostgreSQL 포트(5432)인 이유는?
초기 설계에서 PostgreSQL을 고려했으나, 실제 구축에서는 MySQL(3306)을 사용했다. 코드가 업데이트되지 않은 상태다. 실제 RDS 보안 그룹은 콘솔에서 3306으로 설정되어 있다.
# GitLab Test Public용 보안 그룹
resource "aws_security_group" "test_public_gitlab_sg" {
name = "test-public-gitlab-sg"
description = "Security group for Bastion Host"
vpc_id = aws_vpc.dev_vpc.id
# GitLab SSH 접속 허용
ingress {
from_port = 2222
to_port = 2222
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# SSH 접속 허용
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# 웹 인터페이스 접속 허용
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# HTTPS 접속 허용
ingress {
from_port = 443
to_port = 443
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"]
}
tags = {
Name = "test-public-gitlab-sg"
}
}
test_public_gitlab_sg에 0.0.0.0/0 허용은 보안 위험 아닌가?
맞다. 이 보안 그룹은 GitLab을 Public Subnet에서 테스트할 때만 사용했다. 테스트 완료 후 Private Subnet으로 이전하면서 gitlab_sg로 교체했다. 코드에는 남아있지만 실제로 사용하지 않는다.
5. VPC Endpoint 설계
코드: infra/endpoints.tf
Private Subnet의 리소스가 ECR에서 이미지를 Pull하려면 NAT Gateway를 거쳐야 한다. NAT Gateway는 시간당 요금($0.045/hour)과 데이터 처리 요금($0.045/GB)을 부과한다. 컨테이너 이미지가 수백 MB일 때, 배포할 때마다 비용이 쌓인다.
VPC Endpoint는 AWS PrivateLink를 통해 AWS 서비스에 접근하는 방법이다. NAT Gateway를 거치지 않으므로 데이터 전송 비용이 없고, 트래픽이 인터넷을 타지 않아 보안도 강화된다.
NAT Gateway vs VPC Endpoint 비교
Private Subnet에서 AWS 서비스에 접근하는 방법은 두 가지다.
| 구분 | NAT Gateway | VPC Endpoint |
|---|---|---|
| 동작 방식 | Private → NAT → IGW → 인터넷 → AWS 서비스 | Private → VPC 내부 → AWS 서비스 (인터넷 안 탐) |
| 트래픽 경로 | 퍼블릭 인터넷 경유 | AWS 백본 네트워크 (PrivateLink) |
| 비용 (시간당) | $0.045/hour (월 ~$32) | Interface: $0.01/hour, Gateway: 무료 |
| 비용 (데이터) | $0.045/GB | Interface: $0.01/GB, Gateway: 무료 |
| 보안 | 인터넷 노출 (암호화 필수) | VPC 내부만 통신 (더 안전) |
| 지원 대상 | 모든 인터넷 통신 | 특정 AWS 서비스만 |
언제 무엇을 쓰는가?
AWS 서비스는 VPC Endpoint로, 외부 인터넷은 NAT Gateway로 분리
| 상황 | 선택 | 이유 |
|---|---|---|
| ECR 이미지 Pull | VPC Endpoint | 트래픽 많음, 비용 절감 + 보안 |
| S3 버킷 접근 | VPC Endpoint (Gateway) | 무료, 설정 간단 |
| CloudWatch Logs 전송 | VPC Endpoint | 로그 양 많음, 비용 절감 |
| 외부 API 호출 (GitHub, npm) | NAT Gateway | VPC Endpoint 미지원 |
| ECR Public 이미지 Pull | NAT Gateway | AWS Public Registry는 Endpoint 미지원 |
# GitLab 서브넷용 ECR 엔드포인트
resource "aws_vpc_endpoint" "gitlab_ecr_dkr" {
vpc_id = aws_vpc.dev_vpc.id
service_name = "com.amazonaws.ap-northeast-2.ecr.dkr"
vpc_endpoint_type = "Interface"
subnet_ids = [aws_subnet.gitlab_private_subnet.id]
security_group_ids = [aws_security_group.gitlab_sg.id]
tags = {
Name = "dev-gitlab-ecr-dkr-endpoint"
}
}
resource "aws_vpc_endpoint" "gitlab_ecr_api" {
vpc_id = aws_vpc.dev_vpc.id
service_name = "com.amazonaws.ap-northeast-2.ecr.api"
vpc_endpoint_type = "Interface"
subnet_ids = [aws_subnet.gitlab_private_subnet.id]
security_group_ids = [aws_security_group.gitlab_sg.id]
tags = {
Name = "dev-gitlab-ecr-api-endpoint"
}
}
ECR 엔드포인트가 왜 2종류인가?
ECR에 접근하려면 두 가지 엔드포인트가 모두 필요하다.
| 엔드포인트 | 역할 | 사용 시점 |
|---|---|---|
| ecr.api | Control Plane | 리포지토리 목록 조회, 인증 토큰 발급, 이미지 메타데이터 |
| ecr.dkr | Data Plane | docker pull/push 시 실제 이미지 레이어 전송 |
docker pull 명령을 실행하면 먼저 ecr.api로 인증하고 이미지 정보를 가져온 뒤, ecr.dkr로 실제 데이터를 다운로드한다. 하나라도 없으면 이미지 Pull이 실패한다.
# EKS Fargate 서브넷용 ECR 엔드포인트
resource "aws_vpc_endpoint" "eks_fargate_ecr_dkr" {
vpc_id = aws_vpc.dev_vpc.id
service_name = "com.amazonaws.ap-northeast-2.ecr.dkr"
vpc_endpoint_type = "Interface"
subnet_ids = [aws_subnet.eks_fargate_private_subnet_a.id]
security_group_ids = [aws_security_group.eks_fargate_sg.id]
tags = {
Name = "dev-eks-fargate-ecr-dkr-endpoint"
}
}
resource "aws_vpc_endpoint" "eks_fargate_ecr_api" {
vpc_id = aws_vpc.dev_vpc.id
service_name = "com.amazonaws.ap-northeast-2.ecr.api"
vpc_endpoint_type = "Interface"
subnet_ids = [aws_subnet.eks_fargate_private_subnet_a.id]
security_group_ids = [aws_security_group.eks_fargate_sg.id]
tags = {
Name = "dev-eks-fargate-ecr-api-endpoint"
}
}
왜 4개인가? (GitLab 2개 + EKS 2개)
GitLab과 EKS가 서로 다른 서브넷에 위치하기 때문이다. Interface 타입 VPC Endpoint는 특정 서브넷에 ENI(Elastic Network Interface)를 생성하는 방식으로 동작한다. 각 서브넷에서 가장 가까운 ENI로 트래픽이 라우팅된다.
GitLab(10.0.40.0/24)에서 ECR에 접근하면 gitlab_ecr_dkr의 ENI로 가고, EKS(10.0.30.0/24)에서 접근하면 eks_fargate_ecr_dkr의 ENI로 간다. 하나의 엔드포인트를 공유하면 Cross-Subnet 트래픽이 발생할 수 있다.
# EKS Fargate 서브넷용 S3 게이트웨이 엔드포인트
resource "aws_vpc_endpoint" "s3_gateway_endpoint" {
vpc_id = aws_vpc.dev_vpc.id
service_name = "com.amazonaws.ap-northeast-2.s3"
route_table_ids = [aws_route_table.eks_fargate_private_route_table_a.id]
tags = {
Name = "dev-s3-gateway-endpoint"
}
}
S3 Gateway Endpoint를 왜 추가했는가?
EKS Fargate는 ECR 외에도 S3와 통신해야 할 수 있다.
- 애플리케이션 데이터 저장: 업로드 파일을 S3에 저장
- CloudWatch 로그: Fargate Pod의 로그가 S3 버킷에 저장될 수 있음
- Terraform 상태 파일: Fargate Pod이 Terraform 상태 파일을 S3에 저장하는 경우
Gateway Endpoint는 무료이고 라우팅 테이블에 경로만 추가하는 방식이므로, 미리 구성해두면 NAT Gateway 비용을 절감할 수 있다.
Gateway Endpoint vs Interface Endpoint
| 구분 | Gateway Endpoint | Interface Endpoint |
|---|---|---|
| 지원 서비스 | S3, DynamoDB만 | ECR, CloudWatch, Secrets Manager 등 |
| 구현 방식 | 라우팅 테이블에 경로 추가 | 서브넷에 ENI 생성 |
| 비용 | 무료 | 시간당 $0.01 + 데이터 전송 |
6. EC2 인스턴스 설계
코드: infra/ec2_instance.tf
GitLab EC2 인스턴스와 관련 리소스를 정의한다.
# EIP 생성
resource "aws_eip" "gitlab_eip" {
}
# EIP와 인스턴스 연결
resource "aws_eip_association" "gitlab_eip_assoc" {
instance_id = aws_instance.public_gitlab.id
allocation_id = aws_eip.gitlab_eip.id
}
# GitLab용 임시 Public 인스턴스
resource "aws_instance" "public_gitlab" {
ami = "ami-0897f20d7e803af8f" # Amazon Linux
instance_type = "t3.medium"
subnet_id = aws_subnet.test_public_gitlab_subnet.id
vpc_security_group_ids = [aws_security_group.test_public_gitlab_sg.id]
key_name = "aws-one393"
tags = {
Name = "test-public-gitlab-host"
}
root_block_device {
volume_size = 50
volume_type = "gp3"
delete_on_termination = false
}
}
왜 EIP를 별도로 생성했는가?
associate_public_ip_address = true로 설정하면 인스턴스 재생성 시 IP가 바뀐다. GitLab은 고정 IP가 필요하다. 개발자들이 Git remote URL을 설정해놓기 때문이다. EIP를 별도 리소스로 생성하면 인스턴스를 재생성해도 IP가 유지된다.
t3.medium을 선택한 이유는?
GitLab은 최소 4GB RAM을 권장한다. t3.micro(1GB)나 t3.small(2GB)로는 GitLab이 느리거나 OOM(Out of Memory)이 발생한다. t3.medium은 4GB RAM을 제공하며, 개발팀 규모(5~10명)에 적합하다.
root_block_device의 delete_on_termination = false는?
인스턴스가 종료되어도 EBS 볼륨을 유지한다. GitLab 데이터(리포지토리, 설정)를 보호하기 위함이다. 단, 이 설정만으로는 완벽하지 않다. terraform destroy 후 apply하면 새 EBS가 생성되는 문제가 있어서, 독립 EBS 리소스로 분리하는 것을 권장한다 (트러블슈팅 섹션 참고).
주석 처리된 코드는?
/*
# 독립적인 EBS 볼륨 리소스 정의
resource "aws_ebs_volume" "gitlab_ebs_volume" {
availability_zone = aws_subnet.test_public_gitlab_subnet.availability_zone
size = 50
type = "gp3"
tags = { Name = "test-public-gitlab-volume" }
}
# 인스턴스와 EBS 볼륨 연결
resource "aws_volume_attachment" "gitlab_volume_attachment" {
instance_id = aws_instance.public_gitlab.id
volume_id = aws_ebs_volume.gitlab_ebs_volume.id
device_name = "/dev/sdh"
}
*/
EBS 유실 문제를 해결하기 위해 작성한 코드다. 기존 EBS가 있다면 terraform import로 가져와서 사용한다. 현재는 주석 처리되어 있지만, 운영 환경에서는 이 방식을 권장한다.
7. Backend 설정
코드: infra/backend.tf
Terraform 상태 파일(tfstate)을 S3에 저장하고, DynamoDB로 State Lock을 관리한다.
terraform {
backend "s3" {
bucket = "iam-root-dev"
key = "global/s3/terraform.tfstate"
region = "ap-northeast-2"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
왜 S3 Backend를 사용했는가?
Local Backend(기본값)는 tfstate 파일이 로컬에 저장된다. 여러 명이 같은 인프라를 관리하면 각자의 tfstate가 달라져서 충돌이 발생한다. S3 Backend는 tfstate를 중앙에서 관리하고, DynamoDB Lock으로 동시 수정을 방지한다.
S3와 DynamoDB의 역할 차이는?
| 구성 요소 | 역할 |
|---|---|
| S3 | tfstate 파일 저장소 (버전 관리 가능) |
| DynamoDB | State Lock (terraform apply 중 다른 사람 apply 차단) |
| encrypt=true | tfstate에 민감 정보(DB 비밀번호 등)가 포함될 수 있으므로 암호화 |
주의사항:
S3 버킷과 DynamoDB 테이블은 Terraform으로 생성하기 전에 미리 만들어둬야 한다. 이 리소스들이 없으면 terraform init이 실패한다. 보통 수동으로 만들거나 별도의 "bootstrap" 스크립트로 먼저 생성한다.
8. Client VPN 설계
코드: infra/client_vpn.tf (주석 처리됨)
GitLab이 Private Subnet(10.0.40.0/24)에 위치한다. 인터넷에서 직접 접근할 수 없다. 개발자가 GitLab에 접속하려면 VPC 내부로 들어가야 하는데, 그 통로가 Client VPN이다.
비용 구조
| 항목 | 비용 | 설명 |
|---|---|---|
| 엔드포인트 요금 | $0.10/시간 | VPN 엔드포인트 유지 비용 |
| 연결 요금 | $0.50/시간 | 클라이언트가 연결된 시간당 |
개발자 1명이 하루 8시간 연결하면 $0.10×24 + $0.50×8 = $6.4/일이다. 사용하지 않을 때는 엔드포인트만 과금된다.
사전 준비: 인증서 생성 (Easy-RSA)
Client VPN은 상호 인증(Mutual Authentication)을 사용한다. 서버와 클라이언트 모두 인증서로 신원을 증명해야 한다.
# 1. Easy-RSA 설치 및 PKI 초기화
./easyrsa init-pki
# 2. CA(인증기관) 인증서 생성
./easyrsa build-ca nopass
# → pki/ca.crt, pki/private/ca.key 생성
# 3. 서버 인증서 생성 (FQDN 형식 필수!)
./easyrsa build-server-full server.domain.tld nopass
# → pki/issued/server.crt, pki/private/server.key 생성
# 4. 클라이언트 인증서 생성
./easyrsa build-client-full client1.domain.tld nopass
# → pki/issued/client1.domain.tld.crt, pki/private/client1.domain.tld.key 생성
# 5. 파일 정리
mkdir ~/vpn-certs
cp pki/ca.crt ~/vpn-certs/
cp pki/issued/server.crt ~/vpn-certs/
cp pki/private/server.key ~/vpn-certs/
cp pki/issued/client1.domain.tld.crt ~/vpn-certs/
cp pki/private/client1.domain.tld.key ~/vpn-certs/
각 인증서의 역할
| 파일 | 역할 |
|---|---|
| ca.crt | 인증기관 인증서. 서버/클라이언트 인증서의 유효성 검증 |
| server.crt/key | VPN 엔드포인트 신원 증명. 클라이언트가 서버를 신뢰 |
| client1.domain.tld.crt/key | 접속자 신원 증명. 서버가 클라이언트를 신뢰 |
ACM에 인증서 가져오기
AWS 콘솔 → ACM → "인증서 가져오기"에서 서버 인증서와 클라이언트 인증서를 각각 등록한다.
인증서 본문: server.crt 내용 (-----BEGIN CERTIFICATE----- 포함)
인증서 프라이빗 키: server.key 내용
인증서 체인: ca.crt 내용
클라이언트용도 동일하게 등록한다. 총 2번 등록해야 한다.
개발자 PC (인터넷)
↓ TLS/443 터널
Client VPN Endpoint (Public Subnet에 ENI 생성)
↓ 172.31.0.0/22 대역 IP 할당
Private Subnet (10.0.40.0/24)
↓
GitLab EC2
resource "aws_ec2_client_vpn_endpoint" "dev_client_vpn" {
description = "Development Client VPN Endpoint"
server_certificate_arn = "arn:aws:acm:ap-northeast-2:732739477448:certificate/70c4a1a6-..."
client_cidr_block = "172.31.0.0/22"
transport_protocol = "tcp"
split_tunnel = true
security_group_ids = [aws_security_group.client_vpn_sg.id]
vpc_id = aws_vpc.dev_vpc.id
authentication_options {
type = "certificate-authentication"
root_certificate_chain_arn = "arn:aws:acm:ap-northeast-2:732739477448:certificate/f4193e57-..."
}
connection_log_options {
enabled = false
}
tags = {
Name = "dev-client-vpn"
}
}
resource "aws_ec2_client_vpn_network_association" "gitlab_subnet_association" {
client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.dev_client_vpn.id
subnet_id = aws_subnet.gitlab_private_subnet.id
}
resource "aws_ec2_client_vpn_authorization_rule" "gitlab_access_rule" {
client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.dev_client_vpn.id
description = "Allow access to Gitlab private subnet"
target_network_cidr = aws_subnet.gitlab_private_subnet.cidr_block
authorize_all_groups = true
}
대상 네트워크 연결과 Authorization Rule (놓치기 쉬움)
엔드포인트만 생성하면 VPN 연결은 되지만 VPC 내부에 접근할 수 없다. 반드시 추가해야 하는 설정:
- 대상 네트워크 연결 (network_association): 엔드포인트를 VPC의 프라이빗 서브넷에 연결한다. 이때 서브넷 안에 ENI(Elastic Network Interface)가 생성되고, 이 ENI가 "사용자 ↔ VPC" 트래픽의 브리지 역할을 한다.
- Authorization Rule (authorization_rule): VPN 사용자가 어떤 CIDR에 접근할 수 있는지 정의한다. 이 규칙이 없으면 VPN 연결은 성공해도 패킷이 차단된다.
split_tunnel = true의 의미는?
VPN 연결 시 모든 트래픽을 VPN으로 보낼지(full tunnel), VPC 대역만 VPN으로 보낼지(split tunnel) 결정한다. split_tunnel = true면 10.0.0.0/16 트래픽만 VPN을 타고, 나머지(인터넷)는 사용자의 기존 경로를 사용한다. 인터넷 속도에 영향을 주지 않으면서 VPC 접근만 허용한다.
client_cidr_block은 왜 172.31.0.0/22인가?
VPN 클라이언트에게 할당할 가상 IP 대역이다. VPC CIDR(10.0.0.0/16)과 겹치면 안 된다. /22는 1024개 IP를 제공하며, 개발자 수를 고려해 충분한 크기로 설정했다. AWS는 /12 ~ /22 범위만 허용한다.
9. 트러블슈팅
9.1 Client VPN 인증서 도메인 문제
문제: Easy-RSA로 서버 인증서 생성 후 ACM에 업로드했는데, 도메인 이름이 할당되지 않았다. VPN 연결 시 TLS 핸드셰이크 오류가 발생했다.
원인: Common Name을 "server"로만 설정했다.
# 잘못된 예
./easyrsa build-server-full server nopass
ACM은 인증서의 Common Name을 도메인 이름으로 인식한다. "server"만 넣으면 유효한 FQDN(Fully Qualified Domain Name)이 아니라서 도메인으로 인정하지 않는다.
해결: FQDN 형식으로 Common Name 지정
# 올바른 예
./easyrsa build-server-full server.domain.tld nopass
./easyrsa build-client-full client1.domain.tld nopass
인증서 재생성 후 ACM에 다시 업로드하니 도메인 이름이 정상 표시되었다.
9.2 Terraform Destroy 시 EBS 유실
문제: GitLab EC2 인스턴스를 Terraform으로 관리하면서 delete_on_termination = false를 설정했다. 인스턴스 삭제 시 EBS가 유지될 거라고 생각했다. 하지만 terraform destroy 후 terraform apply를 실행하니 새 EBS가 새 인스턴스에 붙었다. 기존 EBS는 남아있지만 연결되지 않아 데이터에 접근할 수 없었다.
# 문제가 된 코드
resource "aws_instance" "public_gitlab" {
ami = "ami-0897f20d7e803af8f"
instance_type = "t3.medium"
root_block_device {
volume_size = 50
volume_type = "gp3"
delete_on_termination = false # 인스턴스 삭제 시 EBS 유지
}
}
원인: Terraform은 aws_instance 리소스의 root_block_device를 보고 EBS 볼륨을 생성한다. destroy 시 인스턴스만 삭제하고 EBS는 남기지만, apply 시에는 새 인스턴스와 함께 새 EBS를 생성한다. 기존 EBS를 자동으로 연결하지 않는다.
해결: EBS 볼륨을 인스턴스와 분리하여 독립 리소스로 관리한다.
# 1. 독립 EBS 볼륨 리소스 정의
resource "aws_ebs_volume" "gitlab_ebs_volume" {
availability_zone = aws_subnet.test_public_gitlab_subnet.availability_zone
size = 50
type = "gp3"
tags = { Name = "test-public-gitlab-volume" }
}
# 2. 인스턴스와 볼륨 연결
resource "aws_volume_attachment" "gitlab_volume_attachment" {
instance_id = aws_instance.gitlab.id
volume_id = aws_ebs_volume.gitlab_ebs_volume.id
device_name = "/dev/sdh"
}
기존에 이미 생성된 EBS가 있다면 terraform import로 상태에 등록한다.
terraform import 동작 4단계
- 리소스 정의: Terraform 코드에 aws_ebs_volume 리소스를 정의한다. 속성(size, type, tags)은 AWS에 존재하는 볼륨과 일치해야 한다. 이 코드 자체만으로는 새 볼륨을 생성하지 않는다.
- import 명령 실행: 터미널에서 terraform import [Terraform 리소스 주소] [AWS 리소스 ID] 형식으로 실행한다.
terraform import aws_ebs_volume.gitlab_ebs_volume vol-06460009bb4a044c1
- 상태 파일 업데이트: Terraform이 AWS의 실제 볼륨을 인식하고 terraform.tfstate 파일에 기록한다. 코드의 aws_ebs_volume.gitlab_ebs_volume과 실제 AWS 볼륨이 매핑된다.
- 관리 시작: import 완료 후 terraform apply를 실행하면 코드와 상태 파일 간 차이를 비교하여 리소스를 업데이트할 수 있다.
이렇게 하면 destroy 후 apply해도 기존 EBS가 새 인스턴스에 연결된다.
10. 심화: 개발계 vs 운영계 차이
CGV 프로젝트의 핵심 목표는 "행사 이벤트 시 10만 트래픽을 처리하고, 대기 중인 사용자에게 실시간 정보를 제공하며, 홈페이지가 터지지 않게 유지하는 것"이다. 이 목표를 달성하기 위해 인프라를 어떻게 설계했고, 무엇이 아쉬웠는지 분석한다.
10.1 CGV 최종 아키텍처 구조
CGV 프로젝트는 4개 환경(Dev, Prod, QA, DR)을 구축했다.
리전 및 VPC 구성
서울 리전 (ap-northeast-2)
├── VPC-Dev (개발계) - 내가 담당
│ ├── GitLab EC2 (CI/CD 중앙)
│ ├── EKS Cluster (단일 AZ, 비용 절감)
│ ├── RDS MySQL
│ ├── ElastiCache Redis
│ └── Kinesis Data Streams
│
├── VPC-Prod (운영계) - 팀원 담당
│ ├── EKS Cluster (Multi-AZ, 고가용성)
│ ├── Aurora Global DB (Primary)
│ ├── ElastiCache Serverless
│ └── Kinesis Data Streams
│
└── VPC-QA - 팀원 담당
└── (운영계와 유사한 구조)
도쿄 리전 (ap-northeast-1)
└── VPC-DR
├── Aurora Global DB (Replica)
└── EKS Cluster (Standby)
10만 트래픽 대응을 위한 핵심 설계
[트래픽 흐름]
사용자 10만명 → CloudFront → WAF → ALB → EKS Pod
│
┌──────────────────────┴──────────────────────┐
↓ ↓
Redis (순위) Kinesis (이벤트)
"내 순위가 몇 번인지" "입장/퇴장 알림"
│ │
└──────────────────────┬──────────────────────┘
↓
WebSocket (실시간 알림)
"123번 고객님, 입장하세요"
10.2 현재 Terraform 구성
개발계에서 Terraform으로 관리하는 리소스:
infrastructure/terraform/dev/
├── VPC-Subnet.tf # VPC, IGW, 6개 Subnet
├── routing.tf # NAT Gateway, EIP, Route Table
├── sg.tf # 5개 Security Group
├── endpoints.tf # 5개 VPC Endpoint (ECR, S3)
├── ec2_instance.tf # GitLab EC2
├── client_vpn.tf # Client VPN Endpoint
└── backend.tf # S3 Backend, DynamoDB Lock
Terraform으로 관리하지 않은 리소스:
- EKS Cluster (eksctl로 생성)
- RDS MySQL (콘솔로 생성)
- ElastiCache Redis (콘솔로 생성)
- Kinesis Data Streams (콘솔로 생성)
10.3 아쉬운 점 1: Terraform 관리 범위
현재 상황
VPC, Subnet, Security Group, VPC Endpoint만 Terraform으로 관리했다. EKS, RDS, ElastiCache, Kinesis는 콘솔로 생성했다.
왜 이렇게 했는가?
첫째, 시간 부족이다. 6주 프로젝트에서 Terraform 코드 작성에 시간을 많이 쓰면 핵심 기능(대기열 시스템) 개발이 지연된다.
둘째, 변경 빈도 차이다. VPC/Subnet은 한 번 만들면 거의 안 바뀌지만, RDS 파라미터나 Redis 설정은 테스트하며 자주 바꾼다. 콘솔에서 바꾸면 즉시 반영되지만, Terraform은 코드 수정 → plan → apply 과정이 필요하다.
셋째, 팀원 숙련도다. 모든 팀원이 Terraform에 익숙하지 않았다.
이게 왜 문제인가?
첫째, 운영계 기록이 없다. 콘솔로 만든 RDS, ElastiCache는 "어떤 설정으로 만들었는지" 기록이 없다. 나중에 재구축하거나 DR 환경을 복제할 때 설정을 하나씩 확인해야 한다.
둘째, 환경 간 불일치. 개발계와 운영계가 "같은 구조"인지 확인하려면 양쪽 콘솔을 열어서 비교해야 한다.
셋째, 10만 트래픽 대응 시 문제. 긴급하게 RDS 인스턴스 타입을 올리거나 Redis 노드를 추가해야 할 때, 콘솔 작업은 기록이 안 남고 실수 가능성이 높다.
10.4 직접 구현한다면: 전체 리소스 Terraform 관리 전략
CGV 아키텍처의 모든 리소스를 분류하고 관리 방법을 정의한다.
전체 리소스 분류표
| 카테고리 | 리소스 | 개발계 (실제) | 운영계 (권장) | 관리 방법 근거 |
|---|---|---|---|---|
| 글로벌 | Route53 Hosted Zone | Terraform | Terraform | 한 번 생성 후 거의 안 바뀜 |
| 글로벌 | ACM 인증서 | Terraform | Terraform | 도메인당 1회 발급 |
| 글로벌 | IAM Role/Policy | Terraform | Terraform | IRSA 등 Pod 권한에 필수 |
| 글로벌 | Secrets Manager | Terraform | Terraform | Secret 값은 콘솔에서 |
| 엣지 | CloudFront | 없음 | Terraform | 정적 콘텐츠 캐싱 |
| 엣지 | WAF | 없음 | Terraform | Rate Limit 규칙 |
| 네트워크 | VPC, Subnet, IGW | Terraform | Terraform | 인프라 기반, 필수 IaC |
| 네트워크 | NAT Gateway, EIP | Terraform | Terraform | 비용 추적에 중요 |
| 네트워크 | Route Table | Terraform | Terraform | 라우팅 변경 이력 관리 |
| 네트워크 | VPC Endpoint | Terraform | Terraform | ECR, S3 접근에 필수 |
| 네트워크 | Transit Gateway | Terraform | Terraform | VPC 간 통신 구조 |
| 네트워크 | Client VPN | Terraform | Terraform | 개발자 접근 제어 |
| 보안 | Security Group | Terraform | Terraform | SG 구조 + 규칙 |
| 컴퓨팅 | EKS Cluster | 콘솔 (eksctl) | Terraform | 개발계는 빠른 구축 |
| 컴퓨팅 | EKS Node Group | 콘솔 (eksctl) | Terraform | 운영계는 IaC 필수 |
| 컴퓨팅 | EC2 (GitLab) | Terraform | Terraform | EBS 분리 관리 |
| 데이터베이스 | RDS MySQL | 콘솔 | Terraform | 개발계는 빠른 테스트 |
| 데이터베이스 | Aurora (운영계) | - | Terraform | Multi-AZ, Global DB |
| 데이터베이스 | RDS 파라미터 | 콘솔 | 콘솔 | 튜닝은 즉시 반영 |
| 데이터베이스 | RDS Proxy | 없음 | Terraform | 커넥션 풀링 |
| 캐시 | ElastiCache Redis | 콘솔 | Terraform | 개발계 단일 노드 |
| 메시징 | Kinesis Data Streams | 콘솔 | Terraform | 개발계 샤드 2개 |
| 스토리지 | S3 Bucket | Terraform | Terraform | 버킷 정책 관리 |
| 스토리지 | ECR Repository | Terraform | Terraform | Replication 규칙 |
| 모니터링 | CloudWatch Alarms | 콘솔 | Terraform | 개발계는 수동 설정 |
| 모니터링 | CloudWatch Dashboards | 콘솔 | 콘솔 | 시각적 구성 |
실제 CGV 개발계 현황
[Terraform으로 관리 - 6개 파일]
├── VPC-Subnet.tf # VPC, IGW, 6개 Subnet
├── routing.tf # NAT Gateway, EIP, Route Table
├── sg.tf # 5개 Security Group
├── endpoints.tf # 5개 VPC Endpoint (ECR, S3)
├── ec2_instance.tf # GitLab EC2
└── client_vpn.tf # Client VPN Endpoint
[콘솔로 생성 - 기록 없음]
├── EKS Cluster # eksctl create cluster
├── EKS Node Group # eksctl create nodegroup
├── RDS MySQL # AWS 콘솔에서 클릭
├── ElastiCache Redis # AWS 콘솔에서 클릭
├── Kinesis Streams # AWS 콘솔에서 클릭
└── CloudWatch Alarms # AWS 콘솔에서 클릭
왜 이렇게 나뉘었는가?
| 구분 | Terraform | 콘솔 |
|---|---|---|
| 리소스 | 네트워크, Security Group | EKS, RDS, Redis, Kinesis |
| 특징 | 한 번 만들면 거의 안 바뀜 | 테스트하며 자주 변경 |
| 이유 | IaC로 기록 남기기 좋음 | 빠르게 설정 변경 가능 |
| 문제점 | 없음 | 재구축 시 설정 기억 어려움 |
개발계 vs 운영계 Terraform 관리 범위
[개발계 - 트래픽 검증 목적]
목표: 빠른 구축 + 비용 최소화
Terraform 범위: 네트워크 + 보안 기본 구조
콘솔 범위: EKS, RDS, Redis, Kinesis (빠른 테스트)
장점: 설정 변경하며 빠르게 테스트
단점: 재구축 시 설정 기억 어려움
대응: terraform import로 주기적 동기화
[운영계 - 10만 트래픽 대응 목적]
목표: 안정성 + 재현 가능성 + 감사 추적
Terraform 범위: 전체 인프라 (네트워크 + 컴퓨팅 + 데이터)
콘솔 범위: 긴급 대응 후 import
장점: DR 복제, 환경 복제 용이
단점: 초기 구축 시간 필요
대응: modules + tfvars로 환경 분리
원칙 1: 핵심 인프라는 Terraform, 세부 설정은 콘솔
# Terraform으로 관리 (인프라 구조)
resource "aws_db_instance" "mysql" {
identifier = "cgv-${var.environment}-mysql"
instance_class = var.db_instance_class # tfvars로 환경별 분리
multi_az = var.enable_multi_az # dev: false, prod: true
parameter_group_name = aws_db_parameter_group.mysql.name
# 파라미터 그룹 자체는 Terraform에서 생성
}
resource "aws_db_parameter_group" "mysql" {
family = "mysql8.0"
name = "cgv-${var.environment}-mysql-params"
# 파라미터 값은 콘솔에서 튜닝 (max_connections 등)
}
원칙 2: terraform import로 형상 동기화
콘솔에서 긴급 변경 후 Terraform State에 반영:
# 콘솔에서 RDS 파라미터 변경 후 동기화
terraform import aws_db_parameter_group.mysql cgv-dev-mysql-params
# Kinesis 샤드 추가 후 동기화
terraform import aws_kinesis_stream.queue cgv-dev-queue-stream
# Redis 클러스터 import
terraform import aws_elasticache_replication_group.redis cgv-dev-redis
# State 동기화 확인
terraform plan
# → No changes. Infrastructure is up-to-date.
원칙 3: eksctl 생성 → Terraform import
개발계에서 eksctl로 빠르게 생성 후 Terraform으로 관리 전환:
# 1. eksctl로 빠른 EKS 생성 (15분)
eksctl create cluster -f cluster.yaml
# 2. Terraform 코드 작성 (기존 설정 기반)
# 3. Terraform import로 State 등록
terraform import aws_eks_cluster.main cgv-dev-cluster
terraform import aws_eks_node_group.main cgv-dev-cluster:cgv-dev-nodegroup
# 4. 이후 변경은 Terraform으로 관리
terraform plan # 차이 확인
terraform apply # 필요시 적용
마무리
CGV 개발계 인프라는 VPC Endpoint로 NAT 비용을 줄이고, Client VPN으로 Private Subnet 접근을 통제했다. 보안 그룹은 Security Group 참조 방식으로 설계하여 IP 대역 변경에 대응했다.
Terraform 코드 분석 결과:
- VPC-Subnet.tf: VPC, IGW, 6개 Subnet
- routing.tf: NAT Gateway, EIP, 4개 Route Table, 6개 Association
- sg.tf: 5개 Security Group (client_vpn, gitlab, eks_fargate, db, test_public_gitlab)
- endpoints.tf: 5개 VPC Endpoint (ECR 4개 + S3 Gateway 1개)
- ec2_instance.tf: GitLab EC2, EIP, EIP Association
- client_vpn.tf: VPN Endpoint, Network Association, Authorization Rule
- backend.tf: S3 Backend, DynamoDB Lock 설정
다음 섹션에서는 대기열 시스템을 분석한다. Redis + Kinesis로 동시 접속을 어떻게 처리했는지 살펴본다.
'Cloudwave > cgv-project' 카테고리의 다른 글
| CI/CD (0) | 2026.02.18 |
|---|---|
| 메시징 시스템 (0) | 2026.02.16 |
| WebSocket (0) | 2026.02.12 |
| 3. CI/CD 파이프라인 구축과 자동화 (0) | 2026.02.02 |
| 2. CGV 대기열 시스템 (0) | 2026.02.01 |