EKS Terraform 실전 코드 분석 (timedeal-k8s 기준)
목차
1. 전체 구조 개요
리소스 블록 13개 요약
| # | 리소스 | 역할 |
|---|---|---|
| 1 | aws_vpc | VPC |
| 2 | aws_internet_gateway | IGW |
| 3 | aws_subnet.public (count=2) | ALB용 퍼블릭 서브넷 |
| 4 | aws_subnet.private (count=2) | 노드/Redis용 프라이빗 서브넷 |
| 5 | aws_nat_gateway | 프라이빗 → 인터넷 아웃바운드 |
| 6 | 라우팅 테이블 | public(IGW), private(NAT) |
| 7 | SG 3개 + rule 2개 | ALB / EKS 노드 / 컨트롤 플레인 |
| 8 | aws_iam_role.eks_cluster | 컨트롤 플레인 IAM |
| 9 | aws_iam_role.eks_nodes | 노드 IAM |
| 10 | aws_eks_cluster | EKS 컨트롤 플레인 |
| 11 | aws_eks_node_group | 워커 노드 |
| 12 | aws_elasticache_cluster | Redis |
| 13 | aws_ecr_repository × 2 | 컨테이너 이미지 저장소 |
pposiraegi(단일 EC2)와 주요 차이점
| 항목 | pposiraegi | timedeal-k8s |
|---|---|---|
| 서브넷 선언 | 개별 리소스 (public_a/b/c/d) | count로 반복 생성 |
| 앱 실행 | EC2 직접 | EKS 노드그룹 |
| 이미지 관리 | 없음 | ECR |
| IAM | SSM용 1개 | 클러스터용 + 노드용 2개 |
| Provider | aws만 | aws + kubernetes |
2. VPC/서브넷 - EKS 특화 부분
count로 서브넷 만드는 이유
resource "aws_subnet" "public" {
count = length(var.public_subnet_cidrs) # 2개
cidr_block = var.public_subnet_cidrs[count.index]
availability_zone = var.availability_zones[count.index]
}“pposiraegi에서는 서브넷마다 역할이 달라서 개별 리소스로 선언했는데, EKS는 public 2개, private 2개가 동일한 역할이라
count로 반복 생성했습니다.”
참조할 때:
aws_subnet.public[0].id # 첫 번째 퍼블릭
aws_subnet.public[*].id # 전체 퍼블릭 리스트EKS 전용 서브넷 태그 - 왜 필요한가
tags = {
"kubernetes.io/cluster/${var.eks_cluster_name}" = "shared"
"kubernetes.io/role/elb" = "1" # 퍼블릭
"kubernetes.io/role/internal-elb" = "1" # 프라이빗
}핵심
AWS Load Balancer Controller가 어느 서브넷에 ALB/NLB를 만들지 판단하는 기준이 이 태그입니다.
elb = 1: 인터넷 facing ALB는 이 서브넷에internal-elb = 1: 내부 ALB는 이 서브넷에 태그 없으면 ALB 자동 생성 안 됨
3. SG 설계 - 컨트롤 플레인 ↔ 노드 통신
트래픽 흐름
인터넷
↓ 80/443
ALB SG
↓ 30000-32767 (NodePort)
EKS Nodes SG ←→ (self: 노드 간 전체 통신)
↕ 443 / 10250
EKS Cluster SG (컨트롤 플레인)
↓ 6379
Redis SG
포트별 의미
| 포트 | 방향 | 이유 |
|---|---|---|
| 30000-32767 | ALB → 노드 | NodePort 범위 (K8s 기본값) |
| 10250 | 컨트롤 플레인 → 노드 | kubelet API (컨트롤 플레인이 노드 상태 조회) |
| 443 | 노드 ↔ 컨트롤 플레인 | API Server 통신 |
self = true 의미
ingress {
description = "All traffic between nodes"
protocol = "-1"
self = true # 같은 SG에 속한 리소스끼리만 허용
}“노드 간 Pod-to-Pod 통신, CNI 플러그인 통신 등을 허용하기 위해
self = true를 사용했습니다. CIDR 대신 SG 자기 자신을 source로 참조하는 방식이라 노드가 늘어나도 규칙 수정이 필요 없습니다.”
SG rule을 별도 리소스로 분리한 이유
resource "aws_security_group_rule" "cluster_to_nodes" { ... }
resource "aws_security_group_rule" "nodes_to_cluster" { ... }“컨트롤 플레인 SG와 노드 SG가 서로를 참조하는 순환 의존성이 생깁니다. SG 리소스 안에 inline으로 쓰면 순환 참조 오류가 나서
aws_security_group_rule로 별도 분리했습니다.”
4. IAM 역할 2개 - 클러스터 vs 노드
왜 2개가 필요한가
| 역할 | Principal | 역할 |
|---|---|---|
eks_cluster | eks.amazonaws.com | 컨트롤 플레인이 AWS API 호출 |
eks_nodes | ec2.amazonaws.com | 워커 노드(EC2)가 AWS API 호출 |
클러스터 역할에 붙은 정책
AmazonEKSClusterPolicy # VPC, EC2, ELB 등 제어
AmazonEKSVPCResourceController # ENI 생성/관리 (Pod 네트워크)“컨트롤 플레인이 노드 등록, 서브넷에 ENI 붙이기, 로드밸런서 관리 등을 하려면 이 권한이 필요합니다.”
노드 역할에 붙은 정책
AmazonEKSWorkerNodePolicy # 클러스터 API 조회, 노드 등록
AmazonEKS_CNI_Policy # VPC CNI: Pod에 VPC IP 할당
AmazonEC2ContainerRegistryReadOnly # ECR에서 이미지 pull
AmazonSSMManagedInstanceCore # SSH 없이 SSM으로 접속면접 포인트
“노드에
ECRReadOnly가 붙는 이유는 kubelet이 Pod 스펙의 이미지를 ECR에서 직접 pull하기 때문입니다. 이 권한 없으면ImagePullBackOff에러가 납니다.”
5. EKS 클러스터 핵심 설정
resource "aws_eks_cluster" "main" {
vpc_config {
subnet_ids = concat(aws_subnet.public[*].id, aws_subnet.private[*].id)
security_group_ids = [aws_security_group.eks_cluster.id]
endpoint_public_access = true
endpoint_private_access = true
}
enabled_cluster_log_types = ["api", "audit", "authenticator"]
}subnet_ids에 public + private 둘 다 넣는 이유
“컨트롤 플레인의 ENI가 어느 서브넷에 배치될지 지정하는 것입니다. public을 포함시키면 ALB Controller가 퍼블릭 서브넷 태그를 인식할 수 있고, private 포함으로 노드와 컨트롤 플레인 간 내부 통신 경로가 확보됩니다.”
endpoint 접근 설정
| 설정 | 값 | 의미 |
|---|---|---|
endpoint_public_access | true | 외부(로컬 PC)에서 kubectl 가능 |
endpoint_private_access | true | VPC 내부(노드)에서도 API 접근 가능 |
“둘 다 true인 이유: 개발 중에는 로컬에서 kubectl을 써야 하고(public), 노드가 API Server에 접근할 때는 VPC 내부 경로를 쓰게 해서 트래픽이 인터넷을 타지 않게 하려고(private) 둘 다 활성화했습니다.”
로그 타입 3개
| 로그 | 내용 |
|---|---|
api | API Server 요청/응답 |
audit | 누가 무엇을 했는지 감사 로그 |
authenticator | IAM 인증 로그 |
6. 노드그룹 설계 의도
노드를 프라이빗 서브넷에만 배치하는 이유
subnet_ids = aws_subnet.private[*].id # 프라이빗만“노드(EC2)는 외부에서 직접 접근할 필요가 없습니다. 인터넷 트래픽은 ALB → NodePort로만 들어오고, 노드 자체는 인터넷에 노출시키지 않는 게 보안상 맞습니다. 아웃바운드는 NAT Gateway로 처리합니다.”
ignore_changes 이유
lifecycle {
ignore_changes = [scaling_config[0].desired_size]
}“Cluster Autoscaler나 콘솔에서 노드 수를 조정하면
desired_size가 바뀝니다. 이 상태에서terraform apply를 하면 Terraform이 원래 값으로 되돌리려 합니다.ignore_changes로 Terraform이 이 값을 건드리지 않게 했습니다.”
SSH 없이 SSM 접속 이유
remote_access {
ec2_ssh_key = null
source_security_group_ids = []
}“노드가 프라이빗 서브넷에 있어서 SSH 직접 접근이 안 됩니다. SSM은 아웃바운드 443만 있으면 되므로, Bastion 없이도 안전하게 접속할 수 있습니다. 노드 역할에
AmazonSSMManagedInstanceCore정책을 붙인 이유입니다.”
스케일링 설정
scaling_config {
desired_size = 2
min_size = 1
max_size = 3
}“찍먹용 구성이라 최소 1개로 비용을 줄이되, Cluster Autoscaler가 최대 3개까지 늘릴 수 있게 했습니다.”
7. ECR + lifecycle policy
scan_on_push 의미
image_scanning_configuration {
scan_on_push = true
}“이미지 push 시 자동으로 취약점 스캔을 실행합니다. CVE 데이터베이스 기준으로 이미지 레이어의 보안 이슈를 감지합니다.”
lifecycle policy - 이미지 10개만 보관
selection = {
tagStatus = "any"
countType = "imageCountMoreThan"
countNumber = 10
}
action = { type = "expire" }“CI/CD로 배포할 때마다 이미지가 쌓이면 ECR 스토리지 비용이 발생합니다. 최근 10개만 보관하고 나머지는 자동 삭제하도록 했습니다.”
ECR 이미지 사용 흐름
로컬/CI → docker build → ECR push
↓
EKS 노드 kubelet → ECR pull → Pod 실행
(노드 IAM에 ECRReadOnly 필요)
8. providers.tf - kubernetes provider 연결 구조
provider "kubernetes" {
host = aws_eks_cluster.main.endpoint
cluster_ca_certificate = base64decode(aws_eks_cluster.main.certificate_authority[0].data)
exec {
api_version = "client.authentication.k8s.io/v1beta1"
command = "aws"
args = ["eks", "get-token", "--cluster-name", aws_eks_cluster.main.name]
}
}닭/달걀 문제
“kubernetes provider가 EKS 클러스터의 endpoint와 CA 인증서를 참조합니다. 즉 EKS가 먼저 생성되어야 kubernetes provider가 연결될 수 있습니다. Terraform은 이 참조를 보고 자동으로 EKS를 먼저 생성합니다.”
exec 블록 의미
“kubectl처럼
aws eks get-token으로 임시 토큰을 발급받아 인증합니다. 하드코딩된 토큰 대신 IAM 기반 인증을 쓰므로, AWS 자격증명이 유효한 한 항상 인증됩니다.”
default_tags - 공통 태그 자동 적용
provider "aws" {
default_tags {
tags = {
Project = "timedeal"
Environment = var.environment
ManagedBy = "terraform"
}
}
}“모든 리소스에 공통 태그를 자동으로 붙입니다. 각 리소스에
tags를 반복 작성할 필요 없고, 비용 분석 시 태그 기준으로 필터링할 수 있습니다.”
9. 면접 빈출 Q&A
Q. EKS 노드그룹과 Fargate 차이?
| 항목 | 노드그룹 | Fargate |
|---|---|---|
| 서버 관리 | EC2 직접 관리 | 서버리스 |
| 비용 | EC2 비용 | Pod 단위 과금 |
| 커스터마이징 | OS 수준 접근 가능 | 제한적 |
| 사용 시기 | 일반 워크로드 | 간헐적/배치성 |
“timedeal은 상시 실행되는 백엔드 서버라 노드그룹을 선택했습니다.”
Q. 컨트롤 플레인은 어디 있나요?
“EKS 컨트롤 플레인은 AWS가 완전 관리합니다. 우리 VPC에 없고 AWS 관리 VPC에 있으며, ENI를 통해 우리 VPC와 통신합니다. 그래서 컨트롤 플레인 EC2를 직접 볼 수 없습니다.”
Q. 서브넷 태그를 왜 Terraform으로 관리하나요?
“AWS Load Balancer Controller가 Kubernetes Service/Ingress 생성 시 이 태그를 보고 ALB를 어느 서브넷에 만들지 결정합니다. 태그가 없으면 ALB 자동 생성이 실패합니다. 인프라와 앱 배포가 연결되는 지점이라 코드로 관리하는 게 중요합니다.”
Q. ECR 대신 DockerHub 쓰면 안 되나요?
“됩니다. 다만 ECR을 쓰면 노드 IAM 역할로 인증이 자동화되고, VPC 내부에서 이미지를 pull하므로 속도와 비용이 유리합니다. DockerHub는 rate limit도 있어서 CI/CD가 잦으면 문제가 됩니다.”
Q. terraform destroy 하면 순서가 어떻게 되나요?
“Terraform이 의존성 그래프를 역순으로 삭제합니다. EKS 노드그룹 → EKS 클러스터 → IAM → SG → NAT → 서브넷 → VPC 순서입니다. 단, RDS/Redis에
prevent_destroy나deletion_protection이 있으면 거기서 멈춥니다.”
Q. 현재 구조의 개선점은?
1. Remote State (S3 + DynamoDB) 없음 → 팀 협업 시 문제
2. 모듈화 안 됨 → dev/prod 환경 분리 불가
3. IRSA (IAM Roles for Service Accounts) 미적용
→ Pod 단위 IAM 권한 제어가 안 됨
4. NAT Gateway 1개 → 고가용성 없음 (찍먹용이라 의도적)