본문 바로가기
카테고리 없음

[AEWS-3기][7주차 도전과제] DeepSeek을 EKS Auto Mode에서 셀프 호스팅

by james_janghun 2025. 3. 22.
현재 이 주제로 마무리 짓지 못했습니다. 이유는 AWS에서 GPU를 계열의 EC2의 할당량이 0개이기 때문입니다.
AWS Quotas에서 할당량 증가신청을 했으나 거절당하네요. 결론은 지금 모든 리전에서 gpu서버는 1대도 못만드는 상태입니다.

 

 

분명 ap-northeast-2에서 g5.xlarge 인스턴스 생성이 가능한 것을 확인했는데, 계속 생성이 안되서 엄청 고전했습니다.

원인은 바로 G 인스턴스를 만들수가 없는 상태였던거죠.

따라서 반드시 실습을 시작하기전 quotas를 확인해봅시다.

aws service-quotas get-service-quota \
  --service-code ec2 \
  --quota-code L-DB2E81BA \
  --region ap-northeast-2

 

 

이 값이 0이신 분들은 반드시 증가요청을 하시기 바랍니다.

aws service-quotas request-service-quota-increase \
  --service-code ec2 \
  --quota-code L-DB2E81BA \
  --desired-value 4 \
  --region ap-northeast-2

 

 

 

 

 


 

 

이 주제는 너무 재밌어보이기도하고, 제가 요즘 AI에 푹빠져있어서 바로 해보고 싶어 포스팅을 해봅니다.

 

 

일단 DeepSeek은 요즘 AI에 관심이 있으신 분이라면 모르는 분이 아마 없을겁니다.

LLM모델 중 오픈소스로 풀려있는 모델이며, 중국쪽에서 개발을 했다고 하죠. 이 언어모델을 EKS 노드에 올려서 활용하는 방안을 다른 포스팅을 참조해서 작성해보았습니다.

 

EKS Auto Mode는 요즘 EKS에서 핫한 주제입니다. 기존 EKS는 AWS에서 Control Plane을 통제하고, 사용자는 컴퓨팅 노드(워커 노드)를 통제하는 방식으로 사용했습니다. 여기에 Auto Mode가 생기면서 노드 관리를 자동으로 해주도록 하여 불필요한 노드는 삭제하고, 필요한 노드는 생성하는 오토스케일링의 방식을 가져가고 있습니다. 이를 통해 운영자의 운영피로도를 감소시키고, 휴먼 에러를 방지하고, 비용효율화도 시킬 수 있는 등 다양한 방식으로 좋은 모드 입니다.

 

EKS Auto Mode는 eksctl을 통해서 만들수 있으며 다음과 같은 역할이 필요합니다.

AmazonEKSComputePolicy
AmazonEKSBlockStoragePolicy
AmazonEKSLoadBalancingPolicy
AmazonEKSNetworkingPolicy
AmazonEKSClusterPolicy

 

 

그리고 신뢰관계 선택시 EKS- Auto Cluster로 선택되어야 합니다.

 

실질적으로 이제 DeepSeek를 EKS Auto Mode로 사용해 보겠습니다. 아래 블로그를 참조해서 인프라를 구성할건데, 문제는 이 블로그에서 사용하는 스펙이 너무 커서 저는 아주 경량화된 모델로 진행하겠습니다.

 

https://dev.classmethod.jp/articles/deepseek-eks-auto-mode-self-hosting/

 

DeepSeek を EKS Auto Mode でセルフホストしてみた | DevelopersIO

© Classmethod, Inc. All rights reserved.

dev.classmethod.jp

 

구성할 아키텍처는 위와 같습니다.

 

EKS AutoMode 테라폼 배포

VPC 배포

VPC 배포시에 우리가 알아둬야할 점은 만약 운영자가 큰 언어모델을 사용한다면 분명 스펙이 매우 큰 인스턴스를 사용해야할 것입니다. 그리고 GPU서버를 이용한다면 특히나 사용할 수 있는 인스턴스 유형이 그렇게 많지 않을 겁니다.

따라서 내가 사용할 인스턴스가 해당 AZ에 존재하는지를 반드시 체크하시기 바랍니다.

이는 인스턴스 유형에서 가용영역을 살펴보면 확인할 수 있습니다.

 

 

provider.tf

provider는 aws의 provider만 가지고 인프라를 생성하고 eks의 경우 kubectl을 통해 통제하겠습니다.

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "5.84.0"
    }
  }
}

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

 

iam.tf

잠시 후에 사용할 MyNodeRole에 대해서 노드가 사용할 Role을 정의하겠습니다.

resource "aws_iam_role" "eks_node_role" {
  name = "MyNodeRole"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Service = "ec2.amazonaws.com"
        }
        Action = "sts:AssumeRole"
      }
    ]
  })
}

# 사용자 정의 정책 생성 (AmazonEKSAutoNodePolicy 대체)
resource "aws_iam_policy" "custom_eks_auto_node_policy" {
  name        = "CustomEKSAutoNodePolicy"
  description = "Custom policy to replace AmazonEKSAutoNodePolicy"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect: "Allow"
        Action: [
          "ec2:DescribeInstances",
          "ec2:DescribeTags",
          "ec2:DescribeNetworkInterfaces",
          "ec2:CreateNetworkInterface",
          "ec2:DeleteNetworkInterface",
          "ec2:AttachNetworkInterface",
          "ec2:DetachNetworkInterface",
          "ec2:ModifyNetworkInterfaceAttribute",
          "ec2:AssignPrivateIpAddresses",
          "ec2:UnassignPrivateIpAddresses"
        ]
        Resource = "*"
      },
      {
        Effect: "Allow"
        Action: [
          "ecr:GetAuthorizationToken",
          "ecr:BatchCheckLayerAvailability",
          "ecr:GetDownloadUrlForLayer",
          "ecr:BatchGetImage"
        ]
        Resource = "*"
      },
      {
        Effect: "Allow"
        Action: [
          "logs:CreateLogStream",
          "logs:PutLogEvents"
        ]
        Resource = "*"
      }
    ]
  })
}

# 역할에 정책 연결
resource "aws_iam_role_policy_attachment" "eks_worker_node_minimal" {
  role       = aws_iam_role.eks_node_role.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonEKSWorkerNodeMinimalPolicy"
}

resource "aws_iam_role_policy_attachment" "ecr_pull_only" {
  role       = aws_iam_role.eks_node_role.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPullOnly"
}

resource "aws_iam_role_policy_attachment" "custom_auto_node_policy_attachment" {
  role       = aws_iam_role.eks_node_role.name
  policy_arn = aws_iam_policy.custom_eks_auto_node_policy.arn
}

 

vpc.tf

nat와 vpn gw를 오픈하였습니다.

ALB의 경우 EKS Auto Mode로 만들어진 애플리케이션(deepseek)에 접근하기 위한 용도로 public_subnet_tags를 부여합니다.

module "vpc" {
  source = "terraform-aws-modules/vpc/aws"

  name = "my-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c", "ap-northeast-2d"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24", "10.0.4.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24", "10.0.104.0/24"]

  enable_nat_gateway = true
  enable_vpn_gateway = true

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

  tags = {
    Environment = "deepseek-inference"
  }
}

eks.tf

eks 클러스터는 1.32를 이용하였습니다. 설치가 끝나면 자동으로 eks의 kubeconfig를 로컬 컴퓨터에 가져갈 수 있도록 명령어를 설정합니다. 

module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 20.31.6"
  cluster_name                   = "deepseek-inference"
  cluster_version                = "1.32"
  cluster_endpoint_public_access = true
  vpc_id     = module.vpc.vpc_id
  subnet_ids = module.vpc.private_subnets
  enable_cluster_creator_admin_permissions = true
  
  cluster_compute_config = {
    enabled    = true
    node_pools = ["general-purpose"]
    
    node_pool_configs = {
      general-purpose = {
        instance_types           = ["g5.xlarge"]
        capacity_type            = "ON_DEMAND"
        min_size                 = 1
        max_size                 = 3
        desired_size             = 1
        ami_type                 = "AL2_x86_64_GPU"
      }
    }
  }
}

 

 

EKS AutoMode도 활성화 되었습니다.

다음은 EKS 내 매니페스트 파일입니다.

 

네임스페이스와 노드풀을 생성해보겠습니다.

apiVersion: v1
kind: Namespace
metadata:
  name: inference
---
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: inference
  namespace: inference
spec:
  template:
    spec:
      requirements:
        - key: karpenter.sh/capacity-type
          operator: In
          values: ['on-demand']
        - key: eks.amazonaws.com/instance-family
          operator: In
          values: ['g5']
        - key: node.kubernetes.io/instance-type
          operator: In
          values: ['g5.xlarge', 'g5.2xlarge']  # 구체적인 인스턴스 타입 지정
      nodeClassRef:
        group: eks.amazonaws.com
        kind: NodeClass
        name: default

 

애플리케이션을 배포하겠습니다.

deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: deepseek-r1-1-4b
  namespace: inference
  labels:
    app: deepseek-r1-1-4b
spec:
  replicas: 1
  selector:
    matchLabels:
      app: deepseek-r1-1-4b
  template:
    metadata:
      labels:
        app: deepseek-r1-1-4b
    spec:
      nodeSelector:
        'node.kubernetes.io/instance-type': 'g5.xlarge' # 경량화된 인스턴스 타입으로 변경
      tolerations:
        - key: nvidia.com/gpu
          operator: Exists
          effect: NoSchedule
      volumes:
        - emptyDir:
            medium: Memory
            sizeLimit: 2Gi # 캐시 볼륨 크기 축소
          name: cache-volume
      containers:
        - name: vllm
          image: vllm/vllm-openai:v0.7.0
          resources:
            limits:
              memory: '16Gi' # 메모리 요구사항 축소
              cpu: '4'       # CPU 요구사항 축소
              nvidia.com/gpu: 1 # GPU 요구사항 축소 (단일 GPU)
            requests:
              memory: '8Gi' # 요청 메모리 축소
              cpu: '1.5'      # 요청 CPU 축소
              nvidia.com/gpu: 1
          readinessProbe:
            httpGet:
              path: /health
              port: 8000
            periodSeconds: 5
          livenessProbe:
            httpGet:
              path: /health
              port: 8000
            periodSeconds: 10
          startupProbe:
            httpGet:
              path: /health
              port: 8000
            periodSeconds: 30
            successThreshold: 1
            failureThreshold: 30
          args:
            - --model=cyberagent/DeepSeek-R1-Distill-Qwen-1.4B # 모델 이름 변경
            - --tensor-parallel-size=1 # 병렬 처리 크기 조정 (단일 GPU)
            - --max-model-len=2048 # 최대 입력 길이 조정 (경량 모델에 적합)
            - --enforce-eager
          volumeMounts:
            - mountPath: /dev/shm
              name: cache-volume
          ports:
            - containerPort: 8000
              name: http

---
apiVersion: v1
kind: Service
metadata:
  name: deepseek-r1-1-4b-service # 서비스 이름 변경
  namespace: inference
  labels:
    app: deepseek-r1-1-4b-service # 라벨 이름 변경
spec:
  selector:
    app: deepseek-r1-1-4b # 셀렉터 이름 변경에 맞춤 적용 
  ports:
    - protocol: TCP 
      port: 8000 
      name: http 
  type: ClusterIP

 

ingress.yaml

apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
  namespace: inference
  labels:
    app.kubernetes.io/name: LoadBalancerController
  name: alb
spec:
  controller: eks.amazonaws.com/alb

---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  namespace: inference
  name: deepseek-r1-1-4b
  annotations:
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip 
spec:
  ingressClassName: alb
  rules:
    - http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: deepseek-r1-1-4b-service
                port:
                  number: 8000

 

확인

만약 잘 되었다면 ingress를 통해서 호출해볼 수 있을 것 입니다.

kubectl get ingress -n inference

 

 

curl -X POST http://k8s-inferenc-deepseek-hogehoge-hogehoge.us-east-1.elb.amazonaws.com/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
      "model": "cyberagent/DeepSeek-R1-Distill-Qwen-1.4B",
      "messages": [{"role": "user", "content": "안녕하세요가 일본어로 뭐야"}]
    }'