EKS Terraform 실전 코드 분석 (timedeal-k8s 기준)

목차


1. 전체 구조 개요

리소스 블록 13개 요약

#리소스역할
1aws_vpcVPC
2aws_internet_gatewayIGW
3aws_subnet.public (count=2)ALB용 퍼블릭 서브넷
4aws_subnet.private (count=2)노드/Redis용 프라이빗 서브넷
5aws_nat_gateway프라이빗 → 인터넷 아웃바운드
6라우팅 테이블public(IGW), private(NAT)
7SG 3개 + rule 2개ALB / EKS 노드 / 컨트롤 플레인
8aws_iam_role.eks_cluster컨트롤 플레인 IAM
9aws_iam_role.eks_nodes노드 IAM
10aws_eks_clusterEKS 컨트롤 플레인
11aws_eks_node_group워커 노드
12aws_elasticache_clusterRedis
13aws_ecr_repository × 2컨테이너 이미지 저장소

pposiraegi(단일 EC2)와 주요 차이점

항목pposiraegitimedeal-k8s
서브넷 선언개별 리소스 (public_a/b/c/d)count로 반복 생성
앱 실행EC2 직접EKS 노드그룹
이미지 관리없음ECR
IAMSSM용 1개클러스터용 + 노드용 2개
Provideraws만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-32767ALB → 노드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_clustereks.amazonaws.com컨트롤 플레인이 AWS API 호출
eks_nodesec2.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_accesstrue외부(로컬 PC)에서 kubectl 가능
endpoint_private_accesstrueVPC 내부(노드)에서도 API 접근 가능

“둘 다 true인 이유: 개발 중에는 로컬에서 kubectl을 써야 하고(public), 노드가 API Server에 접근할 때는 VPC 내부 경로를 쓰게 해서 트래픽이 인터넷을 타지 않게 하려고(private) 둘 다 활성화했습니다.”

로그 타입 3개

로그내용
apiAPI Server 요청/응답
audit누가 무엇을 했는지 감사 로그
authenticatorIAM 인증 로그

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_destroydeletion_protection이 있으면 거기서 멈춥니다.”

Q. 현재 구조의 개선점은?

1. Remote State (S3 + DynamoDB) 없음 → 팀 협업 시 문제
2. 모듈화 안 됨 → dev/prod 환경 분리 불가
3. IRSA (IAM Roles for Service Accounts) 미적용
   → Pod 단위 IAM 권한 제어가 안 됨
4. NAT Gateway 1개 → 고가용성 없음 (찍먹용이라 의도적)