목차
21 장

EKS 클러스터 셋업

AWS EKS 위에 진짜 운영 클러스터를 처음부터 띄우는 흐름을 다룹니다. Terraform으로 VPC · EKS 컨트롤 플레인 · 노드 그룹 · IRSA · 필수 애드온 (VPC CNI · CoreDNS · kube-proxy · EBS CSI)을 한 코드베이스로 선언하고, eksctl의 빠른 셋업 옵션, Karpenter의 노드 오토스케일링, 그리고 첫 점검 · 비용 모델까지 한 사이클로 정리합니다.

4부 (EKS 실전)의 첫 챕터입니다. 1~3부의 20장이 매니페스트 한 장의 모델부터 정책 엔진과 옵저버빌리티까지 K8s를 객체 차원에서 따라간 길이었다면, 4부의 6장은 그 위에 진짜 서비스를 하나 올리고 운영하는 흐름입니다. 가상의 백엔드 서비스 myshop-api를 EKS에 올리고, RDS와 연결하고, CI/CD로 배포하고, 모니터링 · 운영 단계까지 한 묶음으로 따라갑니다. 이번 챕터는 그 흐름의 출발점 — EKS 클러스터를 처음부터 띄우는 단계입니다. Terraform으로 VPC와 클러스터를 선언하고, 노드 그룹과 IRSA를 셋업하고, 필수 애드온까지 얹는 흐름을 정리합니다.

이번 챕터의 끝에서는 빈 EKS 클러스터 한 대 + IRSA / OIDC provider / 필수 애드온 / kubeconfig가 준비된 상태가 손에 들어옵니다. 4부의 나머지 5장이 이 빈 클러스터 위에 차근차근 한 서비스를 올려 가는 흐름입니다.

myshop-api — 4부를 관통하는 가상 서비스 #

4부를 묶는 시나리오를 한 줄로 잡아 둡니다. 가상의 회사 myshop이 자체 백엔드 API 서비스 myshop-api를 EKS에 올린다고 가정합니다. 사양은 단순합니다.

  • Python (FastAPI) 또는 Go로 짠 작은 REST API
  • RDS PostgreSQL을 데이터 저장소로 사용
  • HTTPS로 외부 노출
  • prod / dev 두 환경
  • 트래픽 변동에 따라 Pod 개수 자동 조정

이 시나리오를 21장의 클러스터부터 26장의 운영 사이클까지 일관되게 따라갑니다. 각 챕터가 다음 챕터의 입력이 되는 구조이고, 마지막 26장까지 따라온 시점이라면 실제 운영 클러스터의 한 사이클이 손에 들어옵니다.

셋업 도구의 선택 — Terraform vs eksctl #

EKS 클러스터를 만드는 선택지는 여럿입니다.

도구모델적합한 곳
AWS 콘솔클릭학습 / 한 번 보기
eksctlYAML 한 장 + CLIPoC / 빠른 셋업 / 학습
TerraformHCL로 선언운영 클러스터 / 멀티 환경 / IaC 표준
AWS CDK / Pulumi코드 (TypeScript, Python 등)로 선언코드 친화 팀 / 복잡한 분기

운영 클러스터의 사실상 표준은 Terraform입니다. VPC · IAM · EKS 컨트롤 플레인 · 노드 그룹 · 애드온 · RDS · Route53 까지를 한 코드베이스로 선언하고 git에 보관할 수 있어서, 클러스터 자체가 코드로 재현 가능해집니다. 같은 매니페스트가 dev / prod 두 환경에 동일하게 적용되고, 변경은 PR 리뷰를 거쳐 들어갑니다. 20장 GitOps의 패턴이 K8s 매니페스트뿐 아니라 클러스터 자체의 인프라에도 그대로 확장됩니다.

eksctl이 그 위치를 대체하기에는 추상화의 결이 EKS에 너무 한정적이지만, 빠른 셋업과 학습에는 가장 직관적입니다. 본 챕터에서는 Terraform을 메인으로 다루고, eksctl을 비교 옵션으로 짧게 짚습니다.

Terraform 프로젝트 구조 #

myshop-api 인프라의 Terraform 코드 구조부터 잡습니다.

terraform/ 디렉터리 구조
terraform/
├── modules/
│   ├── network/         # VPC, 서브넷, NAT, 라우팅
│   ├── eks/             # EKS 클러스터 + 노드 그룹
│   └── addons/          # VPC CNI, EBS CSI, IRSA roles
├── envs/
│   ├── dev/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── terraform.tfvars
│   └── prod/
│       ├── main.tf
│       └── ...
└── versions.tf

modules/에 재사용 가능한 단위를 두고, envs/에서 dev / prod를 다르게 인스턴스화하는 구조입니다. dev / prod의 차이는 인스턴스 타입, 노드 개수, 멀티 AZ 정도이고 모듈 자체는 공유합니다. 7장 Namespace와 라벨의 환경별 매니페스트 분기가 K8s 매니페스트의 결이었다면, 본 챕터의 envs/ 구조는 인프라의 결입니다.

Provider와 backend #

envs/prod/main.tf — provider와 state backend
terraform {
  required_version = ">= 1.6.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }

  backend "s3" {
    bucket         = "myshop-tfstate"
    key            = "eks/prod/terraform.tfstate"
    region         = "ap-northeast-2"
    dynamodb_table = "myshop-tfstate-lock"
    encrypt        = true
  }
}

provider "aws" {
  region = "ap-northeast-2"

  default_tags {
    tags = {
      Project     = "myshop"
      Environment = "prod"
      ManagedBy   = "terraform"
    }
  }
}

state 파일을 S3에, 잠금을 DynamoDB에 두는 표준 패턴입니다. default_tags가 이 provider로 만든 모든 자원에 자동으로 태그를 붙여 주므로 28장 비용 최적화의 Cost Allocation Tags 기반 비용 추적이 자연스럽게 굴러갑니다.

VPC — EKS의 토대 #

EKS는 자기 VPC를 만들지 않습니다. 사용자가 만든 VPC 위에 컨트롤 플레인의 ENI를 꽂는 구조입니다. 따라서 클러스터를 만들기 전에 VPC와 서브넷부터 정해야 합니다.

VPC 모듈 — terraform-aws-modules/vpc/aws #

VPC를 처음부터 직접 정의하지 않고 커뮤니티 모듈을 쓰는 게 표준입니다.

modules/network/main.tf
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"

  name = "${var.project}-${var.env}"
  cidr = "10.10.0.0/16"

  azs              = ["ap-northeast-2a", "ap-northeast-2c"]
  private_subnets  = ["10.10.1.0/24",  "10.10.2.0/24"]
  public_subnets   = ["10.10.101.0/24", "10.10.102.0/24"]
  database_subnets = ["10.10.201.0/24", "10.10.202.0/24"]

  enable_nat_gateway     = true
  single_nat_gateway     = var.env == "dev"
  one_nat_gateway_per_az = var.env == "prod"

  enable_dns_hostnames = true
  enable_dns_support   = true

  public_subnet_tags = {
    "kubernetes.io/role/elb" = "1"
  }

  private_subnet_tags = {
    "kubernetes.io/role/internal-elb" = "1"
  }
}

EKS와 묶일 때 핵심은 마지막의 두 태그입니다.

  • kubernetes.io/role/elb = 1 — public 서브넷에 붙여, AWS Load Balancer Controller가 외부용 LB를 이 서브넷에 만듭니다.
  • kubernetes.io/role/internal-elb = 1 — private 서브넷에 붙여, 내부용 LB (클러스터 내부 + VPC 안)를 이쪽에 만듭니다.

이 태그가 없으면 LB가 어느 서브넷에 들어갈지 알 수 없어 22장 앱 배포 골격에서 만들 Ingress가 동작하지 않습니다.

single NAT vs NAT per AZ #

single_nat_gateway는 dev에서만 켭니다. NAT Gateway 한 개의 비용이 적지 않아서 (시간당 + 데이터 전송량당) dev 환경에서는 한 개로 묶고, prod는 AZ 별로 한 개씩 둬서 한 AZ가 죽어도 다른 AZ의 워크로드가 NAT를 잃지 않게 합니다.

EKS 모듈 — 컨트롤 플레인 + 노드 그룹 #

VPC가 준비되면 EKS 클러스터 자체를 정의합니다.

modules/eks/main.tf
module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 20.0"

  cluster_name    = "${var.project}-${var.env}"
  cluster_version = "1.32"

  vpc_id     = var.vpc_id
  subnet_ids = var.private_subnet_ids

  cluster_endpoint_public_access  = true
  cluster_endpoint_private_access = true

  enable_irsa = true

  cluster_addons = {
    coredns = {
      most_recent = true
    }
    kube-proxy = {
      most_recent = true
    }
    vpc-cni = {
      most_recent = true
    }
    aws-ebs-csi-driver = {
      most_recent              = true
      service_account_role_arn = module.ebs_csi_irsa.iam_role_arn
    }
  }

  eks_managed_node_groups = {
    general = {
      desired_size = var.env == "prod" ? 3 : 2
      min_size     = 2
      max_size     = 10

      instance_types = ["t3.medium"]
      capacity_type  = var.env == "prod" ? "ON_DEMAND" : "SPOT"

      labels = {
        role = "general"
      }
    }
  }
}

이 한 모듈이 다음을 모두 만들어 냅니다.

  • EKS 컨트롤 플레인 (관리형 K8s 1.32)
  • 클러스터의 IAM Role
  • OIDC provider (IRSA의 기반 — enable_irsa = true)
  • Managed Node Group (EC2 인스턴스로 워커 노드 생성)
  • 표준 애드온 4종 (VPC CNI, CoreDNS, kube-proxy, EBS CSI Driver)

16장 RBAC / ServiceAccount 깊이의 IRSA 절에서 다룬 OIDC provider가 여기서 자동으로 활성화되고, 이후 23장 DB 연동에서 ServiceAccount를 IAM Role과 묶을 때 그 OIDC가 신뢰의 기반이 됩니다.

Managed Node Group이라는 표준 #

EKS의 워커 노드는 세 갈래로 구성할 수 있습니다.

종류모델
Managed Node GroupEKS가 EC2 노드의 라이프사이클을 관리. 가장 표준적인 길
Self-managed Node Group사용자가 직접 EC2 그룹 운영. 고급 커스터마이징 시
Fargate서버리스. 노드 자체를 신경 쓰지 않음. 비용 · 제약 있음

Managed Node Group이 첫 도입의 표준입니다. 인스턴스 타입 · 크기 · capacity_type (ON_DEMAND / SPOT)을 매니페스트로 정의하면 EKS가 노드의 가입 · 제거 · 업그레이드를 자동으로 관리합니다. 노드 자체의 OS는 Amazon Linux 2 또는 Bottlerocket이고, kubelet · containerd · CNI 에이전트가 미리 설치된 AMI를 받습니다.

클러스터 endpoint의 두 모드 #

cluster_endpoint_public_accesscluster_endpoint_private_access 두 플래그의 조합이 클러스터 API 서버 접근을 제어합니다.

publicprivate의미
truefalse인터넷에서 누구든 접근 (RBAC 만이 보안 경계)
truetrue인터넷 + VPC 내부 모두 (가장 흔한 설정)
falsetrueVPC 내부에서만. 가장 엄격. bastion이나 VPN 필요

prod 환경의 보안 가이드는 보통 마지막 (private only) 이지만, GitHub Actions에서 직접 kubectl을 부르려면 public이 켜져 있어야 합니다. 일반적으로 public + IP 제한 (cluster_endpoint_public_access_cidrs)을 같이 거는 절충이 자주 쓰입니다. 24장 CI / CD 파이프라인에서는 이 endpoint 모드를 GitHub Actions와 ArgoCD 중 어느 쪽이 부를지에 따라 갈라 잡는 흐름을 다룹니다.

IRSA — 애드온의 IAM Role 부여 #

EBS CSI Driver, AWS Load Balancer Controller, External Secrets 같은 애드온은 클러스터 안에서 AWS API를 호출합니다. 이 호출에 IRSA로 권한을 부여하는 패턴을 짚습니다. 16장에서 본 모델이 본격적인 Terraform 셋업으로 이어지는 단계입니다.

modules/eks/irsa-ebs-csi.tf
module "ebs_csi_irsa" {
  source  = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"
  version = "~> 5.0"

  role_name = "${var.project}-${var.env}-ebs-csi"

  attach_ebs_csi_policy = true

  oidc_providers = {
    main = {
      provider_arn               = module.eks.oidc_provider_arn
      namespace_service_accounts = ["kube-system:ebs-csi-controller-sa"]
    }
  }
}

이 모듈이 다음을 자동으로 만듭니다.

  • IAM Role (이름이 myshop-prod-ebs-csi)
  • 정책 (EBS 디스크 생성 · 삭제 · attach 권한)
  • Trust policy (16장의 그 trust policy — kube-system 네임스페이스의 ebs-csi-controller-sa ServiceAccount만 이 Role을 가져갈 수 있도록 제한)

이 IAM Role의 ARN이 위 cluster_addons.aws-ebs-csi-driver.service_account_role_arn에 전달되어, EKS가 EBS CSI 애드온의 ServiceAccount에 자동으로 annotation을 붙여 줍니다. 결과적으로 9장 PV / PVC / StorageClass에서 다룬 PVC를 만들면 EBS 볼륨이 자동으로 프로비저닝되는 흐름이 동작합니다.

같은 패턴이 23장의 External Secrets에도, 25장 모니터링·알람의 CloudWatch 쪽에도 적용됩니다. 워크로드별로 ServiceAccount + IAM Role 1:1 매핑이 K8s 실전의 표준 보안 구조입니다.

kubeconfig — 클러스터에 접근 #

Terraform으로 클러스터를 만들고 나면 로컬에서 kubectl이 그 클러스터를 부를 수 있도록 kubeconfig를 받아와야 합니다. 2장 로컬 환경에서 본 kubeconfig 모델이 그대로 EKS에 적용됩니다.

kubeconfig 갱신
aws eks update-kubeconfig \
  --region ap-northeast-2 \
  --name myshop-prod
확인
kubectl get nodes
kubectl get pods -A
기대 출력
NAME                                            STATUS   ROLES    AGE   VERSION
ip-10-10-1-145.ap-northeast-2.compute.internal  Ready    <none>   3m    v1.32.0
ip-10-10-2-201.ap-northeast-2.compute.internal  Ready    <none>   3m    v1.32.0
ip-10-10-2-83.ap-northeast-2.compute.internal   Ready    <none>   3m    v1.32.0

세 노드가 떠 있고 kube-system 네임스페이스의 시스템 Pod (coredns, kube-proxy, aws-node, ebs-csi-controller)가 모두 Running 상태이면 클러스터가 정상입니다.

접근 권한 — aws-auth ConfigMap의 역할 #

EKS 클러스터를 만든 IAM 사용자 / Role이 자동으로 system:masters 권한을 갖습니다. 다른 IAM 사용자에게 접근 권한을 주려면 aws-auth ConfigMap을 수정하거나, 1.23+ 의 새 모델인 EKS Access Entries를 사용합니다. Access Entries가 더 표준이고 Terraform 모듈에서도 권장됩니다. 14장 RBAC / NetworkPolicy / ResourceQuota의 RBAC 표준 ClusterRole (view / edit / admin)이 EKS Access Entries와 자연스럽게 묶입니다.

EKS Access Entries — Terraform
access_entries = {
  developers = {
    principal_arn = "arn:aws:iam::123456789012:role/Developer"

    policy_associations = {
      view = {
        policy_arn = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSViewPolicy"
        access_scope = {
          type = "cluster"
        }
      }
    }
  }
}

IAM Role 한 개를 클러스터 전체의 view 권한과 묶는 매니페스트입니다. K8s RBAC의 view ClusterRole이 자동으로 매핑되어, 이 Role을 가진 사용자는 클러스터의 모든 객체를 읽을 수 있게 됩니다.

eksctl — 빠른 셋업의 길 #

학습이나 PoC에서 빠른 클러스터가 필요할 때 eksctl이 한 줄에 끝납니다.

cluster.yaml — eksctl 매니페스트
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig

metadata:
  name: myshop-dev
  region: ap-northeast-2
  version: "1.32"

vpc:
  nat:
    gateway: Single

managedNodeGroups:
  - name: general
    instanceType: t3.medium
    desiredCapacity: 2
    minSize: 2
    maxSize: 5
    spot: true

addons:
  - name: vpc-cni
  - name: coredns
  - name: kube-proxy
  - name: aws-ebs-csi-driver

iam:
  withOIDC: true
클러스터 생성
eksctl create cluster -f cluster.yaml

이 한 명령으로 VPC · EKS · 노드 그룹 · OIDC provider · 기본 애드온까지 자동으로 만들어집니다. 15~20분 정도 걸리고, 끝나면 kubeconfig도 자동으로 갱신됩니다.

eksctl의 결을 정리하면 다음과 같습니다.

  • 장점 — 학습 곡선이 가장 낮음, 한 매니페스트로 클러스터 한 대 통째로 만듭니다.
  • 단점 — 멀티 환경 / RDS / Route53 같은 주변 자원과 한 묶음으로 관리하기 어렵습니다. CloudFormation을 내부적으로 쓰므로 Terraform과 state가 분리됩니다.

운영 클러스터의 목표는 거의 항상 Terraform 이지만, 첫 학습이나 일회성 PoC에는 eksctl이 가장 빠릅니다.

Karpenter — 노드 오토스케일링의 새 길 #

Managed Node Group은 자체 오토스케일링 (Cluster Autoscaler)이 가능하지만, 인스턴스 타입이 미리 정해진 몇 가지로 제한됩니다. 트래픽 변동이 크고 워크로드의 자원 요구가 다양한 환경에서는 Karpenter가 새 표준으로 자리 잡고 있습니다.

13장 오토스케일링 §“Karpenter — EKS의 더 빠른 대안"에서 짚었던 모델이 여기서 본격적인 매니페스트로 이어집니다. Pending 상태인 Pod를 보고, 그 Pod의 자원 요구에 맞는 인스턴스 타입을 실시간으로 골라 새 노드를 띄웁니다. 정해진 인스턴스 풀이 아니라 AWS의 모든 EC2 타입에서 가장 적합한 것을 선택합니다.

Karpenter NodePool — 노드 자동 프로비저닝 정책
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: default
spec:
  template:
    spec:
      requirements:
        - key: kubernetes.io/arch
          operator: In
          values: ["amd64"]
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["spot", "on-demand"]
        - key: node.kubernetes.io/instance-type
          operator: In
          values: ["t3.medium", "t3.large", "m5.large", "m5.xlarge"]
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: default
  limits:
    cpu: 100
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized

Karpenter 도입은 21장의 출발점에서는 부담스러우므로, Managed Node Group으로 시작해서 트래픽 패턴이 정착되면 Karpenter로 전환하는 흐름이 자연스럽습니다. 26장 운영 체크리스트28장 비용 최적화에서 다시 짚습니다.

클러스터 셋업 후 첫 점검 #

클러스터가 떠오른 직후 한 번씩 돌려 두면 좋은 점검 명령들입니다.

버전과 노드 헬스
kubectl version --short
kubectl get nodes -o wide
시스템 Pod
kubectl get pods -n kube-system
OIDC provider 확인 (IRSA가 활성화되었는가)
aws eks describe-cluster \
  --name myshop-prod \
  --region ap-northeast-2 \
  --query "cluster.identity.oidc.issuer" \
  --output text
EKS 애드온 상태
aws eks list-addons --cluster-name myshop-prod --region ap-northeast-2
aws eks describe-addon --cluster-name myshop-prod \
  --addon-name vpc-cni --region ap-northeast-2

이 네 명령으로 클러스터 · 노드 · 시스템 Pod · OIDC · 애드온이 모두 정상인지 확인됩니다. 이상이 보이면 다음 챕터로 넘어가기 전에 잡아 두는 게 안전합니다.

비용의 첫 인상 #

EKS 클러스터의 비용은 크게 셋입니다.

항목비용 (ap-northeast-2 기준)
EKS 컨트롤 플레인시간당 $0.10 (≈ 월 $73)
EC2 노드 (t3.medium 3대)월 약 $80 (ON_DEMAND) / $25 (SPOT)
NAT Gateway월 약 $35 + 데이터 전송
EBS / Load Balancer / 데이터 전송사용량에 따라

가장 작은 prod 클러스터의 시작 비용이 월 $200~$300 정도입니다. dev 환경에서 SPOT 인스턴스 + Single NAT를 쓰면 그 절반 이하로 떨어집니다. 비용은 28장 비용 최적화에서 본격적으로 다루지만, 클러스터를 띄우는 시점부터 인지하고 시작하는 게 좋습니다.

연습문제 #

  1. 본인이 학습용으로 사용 가능한 AWS 계정에 본 챕터의 Terraform 코드를 적용해 dev EKS 클러스터 한 대를 띄워 봅니다. aws eks update-kubeconfig로 kubeconfig를 갱신한 뒤 kubectl get nodes -o widekubectl get pods -n kube-system 출력을 기록합니다. §“클러스터 셋업 후 첫 점검"의 네 명령을 차례로 돌리고, OIDC provider URL · 애드온 상태 · 시스템 Pod 9개 이상이 모두 정상인지 점검합니다.
  2. cluster_endpoint_public_access / cluster_endpoint_private_access의 세 조합 (public-only / public + private / private-only)의 트레이드오프를 본인의 운영 시나리오에 비춰 한 단락으로 비교합니다. 24장 CI / CD 파이프라인에서 GitHub Actions가 직접 kubectl을 부를 것인지, 아니면 ArgoCD가 git만 watch 하는 pull 모델로 갈 것인지가 이 endpoint 결정과 어떻게 묶이는지 메모합니다.
  3. 본 챕터의 EBS CSI IRSA 모듈을 참고해서 AWS Load Balancer Controller와 External Secrets Operator의 IRSA Terraform 매니페스트 골격을 적어 봅니다. 각 ServiceAccount의 네임스페이스 · 이름 · 필요한 IAM 정책이 무엇인지 16장의 trust policy 모델로 정리하고, “namespace + SA name 둘 다 명시” 원칙이 왜 격리에 중요한지 한 단락으로 설명합니다.

한 줄 요약: EKS 운영 클러스터의 셋업은 Terraform으로 VPC · EKS 컨트롤 플레인 · 노드 그룹 · IRSA · 표준 애드온 (VPC CNI · CoreDNS · kube-proxy · EBS CSI)을 한 코드베이스로 선언하는 흐름이다. eksctl은 빠른 학습 · PoC의 길이고, Karpenter는 트래픽 패턴이 정착된 후 Managed Node Group을 대체하는 노드 오토스케일링의 새 표준이다. 클러스터 자체의 비용은 컨트롤 플레인 + 노드 + NAT의 셋이 주축이고, prod의 시작 비용은 월 $200~$300 수준이다.

다음 챕터 #

이 시점에서 클러스터는 비어 있는 상태입니다 — 노드는 떠 있고 시스템 Pod는 살아 있지만, 우리가 올리려는 myshop-api는 아직 한 줄도 매니페스트로 들어가지 않았습니다. 다음 챕터에서는 그 빈 곳을 채웁니다.

22장 앱 배포 골격에서는 4장 Deployment / 5장 Service / 10장 Ingress / 6장 ConfigMap · Secret의 다섯 객체를 한 묶음으로 정리하고, Helm 차트로 묶어 환경별 배포까지 연결하는 흐름을 다룹니다. AWS Load Balancer Controller와 cert-manager의 본격적인 셋업도 이 챕터에서 이어집니다.

X